2
0

Add PgxScanner interface

Enables types to support database/sql at the same time as pgx.

fixes #232
This commit is contained in:
Jack Christensen
2017-02-02 20:20:31 -06:00
parent 63e482f6bc
commit a52a6bd555
6 changed files with 90 additions and 7 deletions
+1
View File
@@ -15,6 +15,7 @@
* Add json/jsonb binary support to allow use with CopyTo * Add json/jsonb binary support to allow use with CopyTo
* Add named error ErrAcquireTimeout (Alexander Staubo) * Add named error ErrAcquireTimeout (Alexander Staubo)
* Add logical replication decoding (Kris Wehner) * Add logical replication decoding (Kris Wehner)
* Add PgxScanner interface to allow types to simultaneously support database/sql and pgx (Jack Christensen)
## Compatibility ## Compatibility
+6 -5
View File
@@ -157,14 +157,15 @@ Custom Type Support
pgx includes support for the common data types like integers, floats, strings, pgx includes support for the common data types like integers, floats, strings,
dates, and times that have direct mappings between Go and SQL. Support can be dates, and times that have direct mappings between Go and SQL. Support can be
added for additional types like point, hstore, numeric, etc. that do not have added for additional types like point, hstore, numeric, etc. that do not have
direct mappings in Go by the types implementing Scanner and Encoder. direct mappings in Go by the types implementing ScannerPgx and Encoder.
Custom types can support text or binary formats. Binary format can provide a Custom types can support text or binary formats. Binary format can provide a
large performance increase. The natural place for deciding the format for a large performance increase. The natural place for deciding the format for a
value would be in Scanner as it is responsible for decoding the returned data. value would be in ScannerPgx as it is responsible for decoding the returned
However, that is impossible as the query has already been sent by the time the data. However, that is impossible as the query has already been sent by the time
Scanner is invoked. The solution to this is the global DefaultTypeFormats. If a the ScannerPgx is invoked. The solution to this is the global
custom type prefers binary format it should register it there. DefaultTypeFormats. If a custom type prefers binary format it should register it
there.
pgx.DefaultTypeFormats["point"] = pgx.BinaryFormatCode pgx.DefaultTypeFormats["point"] = pgx.BinaryFormatCode
+1 -1
View File
@@ -18,7 +18,7 @@ type NullPoint struct {
Valid bool // Valid is true if not NULL Valid bool // Valid is true if not NULL
} }
func (p *NullPoint) Scan(vr *pgx.ValueReader) error { func (p *NullPoint) ScanPgx(vr *pgx.ValueReader) error {
if vr.Type().DataTypeName != "point" { if vr.Type().DataTypeName != "point" {
return pgx.SerializationError(fmt.Sprintf("NullPoint.Scan cannot decode %s (OID %d)", vr.Type().DataTypeName, vr.Type().DataType)) return pgx.SerializationError(fmt.Sprintf("NullPoint.Scan cannot decode %s (OID %d)", vr.Type().DataTypeName, vr.Type().DataType))
} }
+5
View File
@@ -264,6 +264,11 @@ func (rows *Rows) Scan(dest ...interface{}) (err error) {
if err != nil { if err != nil {
rows.Fatal(scanArgError{col: i, err: err}) rows.Fatal(scanArgError{col: i, err: err})
} }
} else if s, ok := d.(PgxScanner); ok {
err = s.ScanPgx(vr)
if err != nil {
rows.Fatal(scanArgError{col: i, err: err})
}
} else if s, ok := d.(sql.Scanner); ok { } else if s, ok := d.(sql.Scanner); ok {
var val interface{} var val interface{}
if 0 <= vr.Len() { if 0 <= vr.Len() {
+62
View File
@@ -3,6 +3,7 @@ package pgx_test
import ( import (
"bytes" "bytes"
"database/sql" "database/sql"
"fmt"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -291,6 +292,67 @@ func TestConnQueryScanner(t *testing.T) {
ensureConnValid(t, conn) ensureConnValid(t, conn)
} }
type pgxNullInt64 struct {
Int64 int64
Valid bool // Valid is true if Int64 is not NULL
}
func (n *pgxNullInt64) ScanPgx(vr *pgx.ValueReader) error {
if vr.Type().DataType != pgx.Int8Oid {
return pgx.SerializationError(fmt.Sprintf("pgxNullInt64.Scan cannot decode OID %d", vr.Type().DataType))
}
if vr.Len() == -1 {
n.Int64, n.Valid = 0, false
return nil
}
n.Valid = true
err := pgx.Decode(vr, &n.Int64)
if err != nil {
return err
}
return vr.Err()
}
func TestConnQueryPgxScanner(t *testing.T) {
t.Parallel()
conn := mustConnect(t, *defaultConnConfig)
defer closeConn(t, conn)
rows, err := conn.Query("select null::int8, 1::int8")
if err != nil {
t.Fatalf("conn.Query failed: %v", err)
}
ok := rows.Next()
if !ok {
t.Fatal("rows.Next terminated early")
}
var n, m pgxNullInt64
err = rows.Scan(&n, &m)
if err != nil {
t.Fatalf("rows.Scan failed: %v", err)
}
rows.Close()
if n.Valid {
t.Error("Null should not be valid, but it was")
}
if !m.Valid {
t.Error("1 should be valid, but it wasn't")
}
if m.Int64 != 1 {
t.Errorf("m.Int64 should have been 1, but it was %v", m.Int64)
}
ensureConnValid(t, conn)
}
func TestConnQueryErrorWhileReturningRows(t *testing.T) { func TestConnQueryErrorWhileReturningRows(t *testing.T) {
t.Parallel() t.Parallel()
+15 -1
View File
@@ -127,7 +127,9 @@ func (e SerializationError) Error() string {
return string(e) return string(e)
} }
// Scanner is an interface used to decode values from the PostgreSQL server. // Deprecated: Scanner is an interface used to decode values from the PostgreSQL
// server. To allow types to support pgx and database/sql.Scan this interface
// has been deprecated in favor of PgxScanner.
type Scanner interface { type Scanner interface {
// Scan MUST check r.Type().DataType (to check by OID) or // Scan MUST check r.Type().DataType (to check by OID) or
// r.Type().DataTypeName (to check by name) to ensure that it is scanning an // r.Type().DataTypeName (to check by name) to ensure that it is scanning an
@@ -137,6 +139,18 @@ type Scanner interface {
Scan(r *ValueReader) error Scan(r *ValueReader) error
} }
// PgxScanner is an interface used to decode values from the PostgreSQL server.
// It is used exactly the same as the Scanner interface. It simply has renamed
// the method.
type PgxScanner interface {
// ScanPgx MUST check r.Type().DataType (to check by OID) or
// r.Type().DataTypeName (to check by name) to ensure that it is scanning an
// expected column type. It also MUST check r.Type().FormatCode before
// decoding. It should not assume that it was called on a data type or format
// that it understands.
ScanPgx(r *ValueReader) error
}
// Encoder is an interface used to encode values for transmission to the // Encoder is an interface used to encode values for transmission to the
// PostgreSQL server. // PostgreSQL server.
type Encoder interface { type Encoder interface {