From ce86388f86389973a9da0364b645eb0bc6a04b6e Mon Sep 17 00:00:00 2001 From: Thanatat Tamtan Date: Sun, 16 Apr 2023 16:32:58 +0700 Subject: [PATCH 1/6] fix handle pgpass --- conn.go | 17 ++++++++++------- conn_test.go | 4 ---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/conn.go b/conn.go index 94f659c0f..a981bc0ab 100644 --- a/conn.go +++ b/conn.go @@ -263,7 +263,9 @@ func (cn *conn) handlePgpass(o values) { scanner := bufio.NewScanner(io.Reader(file)) // From: https://github.com/tg/pgpass/blob/master/reader.go for scanner.Scan() { - scanText(scanner.Text(), o) + if scanText(scanner.Text(), o) { + break + } } } @@ -291,23 +293,24 @@ func getFields(s string) []string { } // ScanText assists HandlePgpass in it's objective. -func scanText(line string, o values) { +func scanText(line string, o values) bool { hostname := o["host"] ntw, _ := network(o) port := o["port"] db := o["dbname"] username := o["user"] - if len(line) != 0 || line[0] != '#' { - return + if len(line) == 0 || line[0] == '#' { + return false } split := getFields(line) - if len(split) == 5 { - return + if len(split) != 5 { + return false } if (split[0] == "*" || split[0] == hostname || (split[0] == "localhost" && (hostname == "" || ntw == "unix"))) && (split[1] == "*" || split[1] == port) && (split[2] == "*" || split[2] == db) && (split[3] == "*" || split[3] == username) { o["password"] = split[4] - return + return true } + return false } func (cn *conn) writeBuf(b byte) *writeBuf { diff --git a/conn_test.go b/conn_test.go index eb2595705..a44c0f0ff 100644 --- a/conn_test.go +++ b/conn_test.go @@ -144,10 +144,6 @@ func TestOpenURL(t *testing.T) { const pgpassFile = "/tmp/pqgotest_pgpass" func TestPgpass(t *testing.T) { - if os.Getenv("TRAVIS") != "true" { - t.Skip("not running under Travis, skipping pgpass tests") - } - testAssert := func(conninfo string, expected string, reason string) { conn, err := openTestConnConninfo(conninfo) if err != nil { From cfd05ee6fd9ed5cc0adeeb9c0a8e8ea67853f920 Mon Sep 17 00:00:00 2001 From: Thanatat Tamtan Date: Wed, 19 Apr 2023 01:28:35 +0700 Subject: [PATCH 2/6] require go 1.20 and fix tests (#1) --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/test.yml | 13 ++++---- array_test.go | 9 ------ conn_go18.go | 3 +- go.mod | 2 +- ssl.go | 3 +- ssl_test.go | 46 +++++++++++++++++---------- 7 files changed, 40 insertions(+), 38 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 01f91366d..3a09b77cb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef1d4e388..0c91d4361 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,16 +9,15 @@ jobs: fail-fast: false matrix: postgres: + - '15' + - '14' - '13' - '12' - '11' - '10' - '9.6' go: - - '1.17' - - '1.16' - - '1.15' - - '1.14' + - '1.20' steps: - name: setup postgres pre-reqs run: | @@ -169,10 +168,10 @@ jobs: docker exec pg createuser -h localhost -U postgres -DRS pqgosslcert - name: check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: set up go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} id: go @@ -203,7 +202,7 @@ jobs: goimports -d -e . | awk '{ print } END { exit NR == 0 ? 0 : 1 }' - name: run staticcheck - run: ./staticcheck/staticcheck -go 1.13 ./... + run: ./staticcheck/staticcheck -go 1.20 ./... - name: build run: go build -v . diff --git a/array_test.go b/array_test.go index 5ca9f7a55..ba373c471 100644 --- a/array_test.go +++ b/array_test.go @@ -334,7 +334,6 @@ func TestBoolArrayValue(t *testing.T) { } func BenchmarkBoolArrayValue(b *testing.B) { - rand.Seed(1) x := make([]bool, 10) for i := 0; i < len(x); i++ { x[i] = rand.Intn(2) == 0 @@ -485,7 +484,6 @@ func TestByteaArrayValue(t *testing.T) { } func BenchmarkByteaArrayValue(b *testing.B) { - rand.Seed(1) x := make([][]byte, 10) for i := 0; i < len(x); i++ { x[i] = make([]byte, len(x)) @@ -642,7 +640,6 @@ func TestFloat64ArrayValue(t *testing.T) { } func BenchmarkFloat64ArrayValue(b *testing.B) { - rand.Seed(1) x := make([]float64, 10) for i := 0; i < len(x); i++ { x[i] = rand.NormFloat64() @@ -795,7 +792,6 @@ func TestInt64ArrayValue(t *testing.T) { } func BenchmarkInt64ArrayValue(b *testing.B) { - rand.Seed(1) x := make([]int64, 10) for i := 0; i < len(x); i++ { x[i] = rand.Int63() @@ -949,7 +945,6 @@ func TestFloat32ArrayValue(t *testing.T) { } func BenchmarkFloat32ArrayValue(b *testing.B) { - rand.Seed(1) x := make([]float32, 10) for i := 0; i < len(x); i++ { x[i] = rand.Float32() @@ -1102,7 +1097,6 @@ func TestInt32ArrayValue(t *testing.T) { } func BenchmarkInt32ArrayValue(b *testing.B) { - rand.Seed(1) x := make([]int32, 10) for i := 0; i < len(x); i++ { x[i] = rand.Int31() @@ -1542,7 +1536,6 @@ func TestGenericArrayValueErrors(t *testing.T) { } func BenchmarkGenericArrayValueBools(b *testing.B) { - rand.Seed(1) x := make([]bool, 10) for i := 0; i < len(x); i++ { x[i] = rand.Intn(2) == 0 @@ -1555,7 +1548,6 @@ func BenchmarkGenericArrayValueBools(b *testing.B) { } func BenchmarkGenericArrayValueFloat64s(b *testing.B) { - rand.Seed(1) x := make([]float64, 10) for i := 0; i < len(x); i++ { x[i] = rand.NormFloat64() @@ -1568,7 +1560,6 @@ func BenchmarkGenericArrayValueFloat64s(b *testing.B) { } func BenchmarkGenericArrayValueInt64s(b *testing.B) { - rand.Seed(1) x := make([]int64, 10) for i := 0; i < len(x); i++ { x[i] = rand.Int63() diff --git a/conn_go18.go b/conn_go18.go index 63d4ca6aa..daa7f55fa 100644 --- a/conn_go18.go +++ b/conn_go18.go @@ -6,7 +6,6 @@ import ( "database/sql/driver" "fmt" "io" - "io/ioutil" "time" ) @@ -176,7 +175,7 @@ func (cn *conn) cancel(ctx context.Context) error { // Read until EOF to ensure that the server received the cancel. { - _, err := io.Copy(ioutil.Discard, c) + _, err := io.Copy(io.Discard, c) return err } } diff --git a/go.mod b/go.mod index b5a5639ab..82dfc7051 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/lib/pq -go 1.13 +go 1.20 diff --git a/ssl.go b/ssl.go index 36b61ba45..68f0ce7ca 100644 --- a/ssl.go +++ b/ssl.go @@ -3,7 +3,6 @@ package pq import ( "crypto/tls" "crypto/x509" - "io/ioutil" "net" "os" "os/user" @@ -165,7 +164,7 @@ func sslCertificateAuthority(tlsConf *tls.Config, o values) error { cert = []byte(sslrootcert) } else { var err error - cert, err = ioutil.ReadFile(sslrootcert) + cert, err = os.ReadFile(sslrootcert) if err != nil { return err } diff --git a/ssl_test.go b/ssl_test.go index 64d68cf4a..67d86b5ab 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -8,6 +8,7 @@ import ( "crypto/tls" "crypto/x509" "database/sql" + "errors" "fmt" "io" "net" @@ -86,11 +87,14 @@ func TestSSLVerifyFull(t *testing.T) { if err == nil { t.Fatal("expected error") } - _, ok := err.(x509.UnknownAuthorityError) - if !ok { - _, ok := err.(x509.HostnameError) - if !ok { - t.Fatalf("expected x509.UnknownAuthorityError or x509.HostnameError, got %#+v", err) + + { + var x509err x509.UnknownAuthorityError + if !errors.As(err, &x509err) { + var x509err x509.HostnameError + if !errors.As(err, &x509err) { + t.Fatalf("expected x509.UnknownAuthorityError or x509.HostnameError, got %#+v", err) + } } } @@ -101,9 +105,12 @@ func TestSSLVerifyFull(t *testing.T) { if err == nil { t.Fatal("expected error") } - _, ok = err.(x509.HostnameError) - if !ok { - t.Fatalf("expected x509.HostnameError, got %#+v", err) + + { + var x509err x509.HostnameError + if !errors.As(err, &x509err) { + t.Fatalf("expected x509.HostnameError, got %#+v", err) + } } // OK _, err = openSSLConn(t, rootCert+"host=postgres sslmode=verify-full user=pqgossltest") @@ -126,9 +133,11 @@ func TestSSLRequireWithRootCert(t *testing.T) { if err == nil { t.Fatal("expected error") } - _, ok := err.(x509.UnknownAuthorityError) - if !ok { - t.Fatalf("expected x509.UnknownAuthorityError, got %s, %#+v", err, err) + { + var x509err x509.UnknownAuthorityError + if !errors.As(err, &x509err) { + t.Fatalf("expected x509.UnknownAuthorityError, got %s, %#+v", err, err) + } } nonExistentCertPath := filepath.Join(os.Getenv("PQSSLCERTTEST_PATH"), "non_existent.crt") @@ -164,7 +173,8 @@ func TestSSLVerifyCA(t *testing.T) { // Not OK according to the system CA { _, err := openSSLConn(t, "host=postgres sslmode=verify-ca user=pqgossltest") - if _, ok := err.(x509.UnknownAuthorityError); !ok { + var x509err x509.UnknownAuthorityError + if !errors.As(err, &x509err) { t.Fatalf("expected %T, got %#+v", x509.UnknownAuthorityError{}, err) } } @@ -172,7 +182,8 @@ func TestSSLVerifyCA(t *testing.T) { // Still not OK according to the system CA; empty sslrootcert is treated as unspecified. { _, err := openSSLConn(t, "host=postgres sslmode=verify-ca user=pqgossltest sslrootcert=''") - if _, ok := err.(x509.UnknownAuthorityError); !ok { + var x509err x509.UnknownAuthorityError + if !errors.As(err, &x509err) { t.Fatalf("expected %T, got %#+v", x509.UnknownAuthorityError{}, err) } } @@ -243,7 +254,8 @@ func TestSSLClientCertificates(t *testing.T) { // Cert present, key not specified, should fail { _, err := openSSLConn(t, baseinfo+" sslcert="+sslcert) - if _, ok := err.(*os.PathError); !ok { + var pathErr *os.PathError + if !errors.As(err, &pathErr) { t.Fatalf("expected %T, got %#+v", (*os.PathError)(nil), err) } } @@ -251,7 +263,8 @@ func TestSSLClientCertificates(t *testing.T) { // Cert present, empty key specified, should fail { _, err := openSSLConn(t, baseinfo+" sslcert="+sslcert+" sslkey=''") - if _, ok := err.(*os.PathError); !ok { + var pathErr *os.PathError + if !errors.As(err, &pathErr) { t.Fatalf("expected %T, got %#+v", (*os.PathError)(nil), err) } } @@ -259,7 +272,8 @@ func TestSSLClientCertificates(t *testing.T) { // Cert present, non-existent key, should fail { _, err := openSSLConn(t, baseinfo+" sslcert="+sslcert+" sslkey=/tmp/filedoesnotexist") - if _, ok := err.(*os.PathError); !ok { + var pathErr *os.PathError + if !errors.As(err, &pathErr) { t.Fatalf("expected %T, got %#+v", (*os.PathError)(nil), err) } } From cbd59e2059d67d1b7e34b9868838ca5830cda0ea Mon Sep 17 00:00:00 2001 From: Thanatat Tamtan Date: Wed, 19 Apr 2023 14:20:30 +0700 Subject: [PATCH 3/6] use named value (#2) --- conn.go | 46 +++++++++++++++++++--------- conn_go18.go | 86 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 92 insertions(+), 40 deletions(-) diff --git a/conn.go b/conn.go index a981bc0ab..296637d64 100644 --- a/conn.go +++ b/conn.go @@ -881,10 +881,14 @@ func (cn *conn) Close() (err error) { // Implement the "Queryer" interface func (cn *conn) Query(query string, args []driver.Value) (driver.Rows, error) { - return cn.query(query, args) + list := make([]driver.NamedValue, len(args)) + for i, v := range args { + list[i] = driver.NamedValue{Ordinal: i + 1, Value: v} + } + return cn.query(query, list) } -func (cn *conn) query(query string, args []driver.Value) (_ *rows, err error) { +func (cn *conn) query(query string, args []driver.NamedValue) (_ *rows, err error) { if err := cn.err.get(); err != nil { return nil, err } @@ -933,7 +937,12 @@ func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err } if cn.binaryParameters { - cn.sendBinaryModeQuery(query, args) + list := make([]driver.NamedValue, len(args)) + for i, v := range args { + list[i] = driver.NamedValue{Ordinal: i + 1, Value: v} + } + + cn.sendBinaryModeQuery(query, list) cn.readParseResponse() cn.readBindResponse() @@ -1391,10 +1400,14 @@ func (st *stmt) Close() (err error) { } func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) { - return st.query(v) + list := make([]driver.NamedValue, len(v)) + for i, v := range v { + list[i] = driver.NamedValue{Ordinal: i + 1, Value: v} + } + return st.query(list) } -func (st *stmt) query(v []driver.Value) (r *rows, err error) { +func (st *stmt) query(v []driver.NamedValue) (r *rows, err error) { if err := st.cn.err.get(); err != nil { return nil, err } @@ -1413,12 +1426,17 @@ func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { } defer st.cn.errRecover(&err) - st.exec(v) + list := make([]driver.NamedValue, len(v)) + for i, v := range v { + list[i] = driver.NamedValue{Ordinal: i + 1, Value: v} + } + + st.exec(list) res, _, err = st.cn.readExecuteResponse("simple query") return res, err } -func (st *stmt) exec(v []driver.Value) { +func (st *stmt) exec(v []driver.NamedValue) { if len(v) >= 65536 { errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(v)) } @@ -1437,10 +1455,10 @@ func (st *stmt) exec(v []driver.Value) { w.int16(0) w.int16(len(v)) for i, x := range v { - if x == nil { + if x.Value == nil { w.int32(-1) } else { - b := encode(&cn.parameterStatus, x, st.paramTyps[i]) + b := encode(&cn.parameterStatus, x.Value, st.paramTyps[i]) w.int32(len(b)) w.bytes(b) } @@ -1708,13 +1726,13 @@ func md5s(s string) string { return fmt.Sprintf("%x", h.Sum(nil)) } -func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.Value) { +func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.NamedValue) { // Do one pass over the parameters to see if we're going to send any of // them over in binary. If we are, create a paramFormats array at the // same time. var paramFormats []int for i, x := range args { - _, ok := x.([]byte) + _, ok := x.Value.([]byte) if ok { if paramFormats == nil { paramFormats = make([]int, len(args)) @@ -1733,17 +1751,17 @@ func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.Value) { b.int16(len(args)) for _, x := range args { - if x == nil { + if x.Value == nil { b.int32(-1) } else { - datum := binaryEncode(&cn.parameterStatus, x) + datum := binaryEncode(&cn.parameterStatus, x.Value) b.int32(len(datum)) b.bytes(datum) } } } -func (cn *conn) sendBinaryModeQuery(query string, args []driver.Value) { +func (cn *conn) sendBinaryModeQuery(query string, args []driver.NamedValue) { if len(args) >= 65536 { errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(args)) } diff --git a/conn_go18.go b/conn_go18.go index daa7f55fa..5e387cfff 100644 --- a/conn_go18.go +++ b/conn_go18.go @@ -6,6 +6,7 @@ import ( "database/sql/driver" "fmt" "io" + "strings" "time" ) @@ -15,12 +16,8 @@ const ( // Implement the "QueryerContext" interface func (cn *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { - list := make([]driver.Value, len(args)) - for i, nv := range args { - list[i] = nv.Value - } finish := cn.watchCancel(ctx) - r, err := cn.query(query, list) + r, err := cn.query(query, args) if err != nil { if finish != nil { finish() @@ -32,25 +29,64 @@ func (cn *conn) QueryContext(ctx context.Context, query string, args []driver.Na } // Implement the "ExecerContext" interface -func (cn *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { - list := make([]driver.Value, len(args)) - for i, nv := range args { - list[i] = nv.Value - } - +func (cn *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (res driver.Result, err error) { if finish := cn.watchCancel(ctx); finish != nil { defer finish() } - return cn.Exec(query, list) + if err := cn.err.get(); err != nil { + return nil, err + } + defer cn.errRecover(&err) + + // Check to see if we can use the "simpleExec" interface, which is + // *much* faster than going through prepare/exec + if len(args) == 0 { + // ignore commandTag, our caller doesn't care + r, _, err := cn.simpleExec(query) + return r, err + } + + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) + + cn.readParseResponse() + cn.readBindResponse() + cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + res, _, err = cn.readExecuteResponse("Execute") + return res, err + } + // Use the unnamed statement to defer planning until bind + // time, or else value-based selectivity estimates cannot be + // used. + st := cn.prepareTo(query, "") + r, err := st.ExecContext(ctx, args) + if err != nil { + panic(err) + } + return r, err } // Implement the "ConnPrepareContext" interface -func (cn *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { +func (cn *conn) PrepareContext(ctx context.Context, query string) (_ driver.Stmt, err error) { if finish := cn.watchCancel(ctx); finish != nil { defer finish() } - return cn.Prepare(query) + + if err := cn.err.get(); err != nil { + return nil, err + } + defer cn.errRecover(&err) + + if len(query) >= 4 && strings.EqualFold(query[:4], "COPY") { + s, err := cn.prepareCopyIn(query) + if err == nil { + cn.inCopy = true + } + return s, err + } + return cn.prepareTo(query, cn.gname()), nil } // Implement the "ConnBeginTx" interface @@ -182,12 +218,8 @@ func (cn *conn) cancel(ctx context.Context) error { // Implement the "StmtQueryContext" interface func (st *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { - list := make([]driver.Value, len(args)) - for i, nv := range args { - list[i] = nv.Value - } finish := st.watchCancel(ctx) - r, err := st.query(list) + r, err := st.query(args) if err != nil { if finish != nil { finish() @@ -199,17 +231,19 @@ func (st *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (dri } // Implement the "StmtExecContext" interface -func (st *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { - list := make([]driver.Value, len(args)) - for i, nv := range args { - list[i] = nv.Value - } - +func (st *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (res driver.Result, err error) { if finish := st.watchCancel(ctx); finish != nil { defer finish() } - return st.Exec(list) + if err := st.cn.err.get(); err != nil { + return nil, err + } + defer st.cn.errRecover(&err) + + st.exec(args) + res, _, err = st.cn.readExecuteResponse("simple query") + return res, err } // watchCancel is implemented on stmt in order to not mark the parent conn as bad From 402d43be9763867cbf6c6859d1a81be07339c01e Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Wed, 3 May 2023 19:09:34 -0400 Subject: [PATCH 4/6] Add support for NamedValueChecker interface (#1125) Added in 1.9: https://go.dev/doc/go1.9#minor_library_changes. --- conn_go19.go | 35 ++++++++++++++++++++ conn_go19_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 conn_go19.go create mode 100644 conn_go19_test.go diff --git a/conn_go19.go b/conn_go19.go new file mode 100644 index 000000000..003601565 --- /dev/null +++ b/conn_go19.go @@ -0,0 +1,35 @@ +//go:build go1.9 +// +build go1.9 + +package pq + +import ( + "database/sql/driver" + "reflect" +) + +var _ driver.NamedValueChecker = (*conn)(nil) + +func (c *conn) CheckNamedValue(nv *driver.NamedValue) error { + if _, ok := nv.Value.(driver.Valuer); ok { + // Ignore Valuer, for backward compatibility with pq.Array(). + return driver.ErrSkip + } + + // Ignoring []byte / []uint8. + if _, ok := nv.Value.([]uint8); ok { + return driver.ErrSkip + } + + v := reflect.ValueOf(nv.Value) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() == reflect.Slice { + var err error + nv.Value, err = Array(v.Interface()).Value() + return err + } + + return driver.ErrSkip +} diff --git a/conn_go19_test.go b/conn_go19_test.go new file mode 100644 index 000000000..43c982631 --- /dev/null +++ b/conn_go19_test.go @@ -0,0 +1,83 @@ +//go:build go1.9 +// +build go1.9 + +package pq + +import ( + "fmt" + "reflect" + "testing" +) + +func TestArrayArg(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + for _, tc := range []struct { + pgType string + in, out interface{} + }{ + { + pgType: "int[]", + in: []int{245, 231}, + out: []int64{245, 231}, + }, + { + pgType: "int[]", + in: &[]int{245, 231}, + out: []int64{245, 231}, + }, + { + pgType: "int[]", + in: []int64{245, 231}, + }, + { + pgType: "int[]", + in: &[]int64{245, 231}, + out: []int64{245, 231}, + }, + { + pgType: "varchar[]", + in: []string{"hello", "world"}, + }, + { + pgType: "varchar[]", + in: &[]string{"hello", "world"}, + out: []string{"hello", "world"}, + }, + } { + if tc.out == nil { + tc.out = tc.in + } + t.Run(fmt.Sprintf("%#v", tc.in), func(t *testing.T) { + r, err := db.Query(fmt.Sprintf("SELECT $1::%s", tc.pgType), tc.in) + if err != nil { + t.Fatal(err) + } + defer r.Close() + + if !r.Next() { + if r.Err() != nil { + t.Fatal(r.Err()) + } + t.Fatal("expected row") + } + + defer func() { + if r.Next() { + t.Fatal("unexpected row") + } + }() + + got := reflect.New(reflect.TypeOf(tc.out)) + if err := r.Scan(Array(got.Interface())); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(tc.out, got.Elem().Interface()) { + t.Errorf("got %v, want %v", got, tc.out) + } + }) + } + +} From 09b0644d65696867f772d2cc035a687c93eedf43 Mon Sep 17 00:00:00 2001 From: Thanatat Tamtan Date: Thu, 4 May 2023 11:00:08 +0700 Subject: [PATCH 5/6] remove NullTime (#3) --- encode.go | 23 ----------------------- encode_test.go | 20 -------------------- 2 files changed, 43 deletions(-) diff --git a/encode.go b/encode.go index bffe6096a..8f5e43231 100644 --- a/encode.go +++ b/encode.go @@ -2,7 +2,6 @@ package pq import ( "bytes" - "database/sql/driver" "encoding/binary" "encoding/hex" "errors" @@ -608,25 +607,3 @@ func encodeBytea(serverVersion int, v []byte) (result []byte) { return result } - -// NullTime represents a time.Time that may be null. NullTime implements the -// sql.Scanner interface so it can be used as a scan destination, similar to -// sql.NullString. -type NullTime struct { - Time time.Time - Valid bool // Valid is true if Time is not NULL -} - -// Scan implements the Scanner interface. -func (nt *NullTime) Scan(value interface{}) error { - nt.Time, nt.Valid = value.(time.Time) - return nil -} - -// Value implements the driver Valuer interface. -func (nt NullTime) Value() (driver.Value, error) { - if !nt.Valid { - return nil, nil - } - return nt.Time, nil -} diff --git a/encode_test.go b/encode_test.go index 69f9ebb10..8b8c7900b 100644 --- a/encode_test.go +++ b/encode_test.go @@ -11,26 +11,6 @@ import ( "github.com/lib/pq/oid" ) -func TestScanTimestamp(t *testing.T) { - var nt NullTime - tn := time.Now() - nt.Scan(tn) - if !nt.Valid { - t.Errorf("Expected Valid=false") - } - if nt.Time != tn { - t.Errorf("Time value mismatch") - } -} - -func TestScanNilTimestamp(t *testing.T) { - var nt NullTime - nt.Scan(nil) - if nt.Valid { - t.Errorf("Expected Valid=false") - } -} - var timeTests = []struct { str string timeval time.Time From c910a0574679b5588bf4eee3e0f673cdd5136583 Mon Sep 17 00:00:00 2001 From: Thanatat Tamtan Date: Sun, 17 May 2026 14:41:24 +0700 Subject: [PATCH 6/6] remove NullTime re-introduced by upstream merge Upstream lib/pq kept NullTime as a deprecated alias for sql.NullTime. This repository previously removed NullTime entirely in #3, so drop it again after the merge to preserve that decision. Co-Authored-By: Claude Opus 4.7 --- deprecated.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/deprecated.go b/deprecated.go index 861076777..f6fd2f793 100644 --- a/deprecated.go +++ b/deprecated.go @@ -2,7 +2,6 @@ package pq import ( "bytes" - "database/sql" "database/sql/driver" "github.com/lib/pq/pqerror" @@ -87,13 +86,6 @@ func (e *Error) Get(k byte) (v string) { // now works, and calling this manually is no longer required. func ParseURL(url string) (string, error) { return convertURL(url) } -// NullTime represents a [time.Time] that may be null. -// -// Deprecated: this is an alias for [sql.NullTime]. -// -//go:fix inline -type NullTime = sql.NullTime - // CopyIn creates a COPY FROM statement which can be prepared with Tx.Prepare(). // The target table should be visible in search_path. //