From a52a6bd5558b1450ee69178739caf0489c23c3e6 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Thu, 2 Feb 2017 20:20:31 -0600 Subject: [PATCH] Add PgxScanner interface Enables types to support database/sql at the same time as pgx. fixes #232 --- CHANGELOG.md | 1 + doc.go | 11 ++++--- example_custom_type_test.go | 2 +- query.go | 5 +++ query_test.go | 62 +++++++++++++++++++++++++++++++++++++ values.go | 16 +++++++++- 6 files changed, 90 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a02c8e11..126baef4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Add json/jsonb binary support to allow use with CopyTo * Add named error ErrAcquireTimeout (Alexander Staubo) * Add logical replication decoding (Kris Wehner) +* Add PgxScanner interface to allow types to simultaneously support database/sql and pgx (Jack Christensen) ## Compatibility diff --git a/doc.go b/doc.go index 980c5a74..2b735bd5 100644 --- a/doc.go +++ b/doc.go @@ -157,14 +157,15 @@ Custom Type Support 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 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 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. -However, that is impossible as the query has already been sent by the time the -Scanner is invoked. The solution to this is the global DefaultTypeFormats. If a -custom type prefers binary format it should register it there. +value would be in ScannerPgx as it is responsible for decoding the returned +data. However, that is impossible as the query has already been sent by the time +the ScannerPgx is invoked. The solution to this is the global +DefaultTypeFormats. If a custom type prefers binary format it should register it +there. pgx.DefaultTypeFormats["point"] = pgx.BinaryFormatCode diff --git a/example_custom_type_test.go b/example_custom_type_test.go index c8d8e220..34cc3165 100644 --- a/example_custom_type_test.go +++ b/example_custom_type_test.go @@ -18,7 +18,7 @@ type NullPoint struct { 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" { return pgx.SerializationError(fmt.Sprintf("NullPoint.Scan cannot decode %s (OID %d)", vr.Type().DataTypeName, vr.Type().DataType)) } diff --git a/query.go b/query.go index 4e4b8e53..19b867e2 100644 --- a/query.go +++ b/query.go @@ -264,6 +264,11 @@ func (rows *Rows) Scan(dest ...interface{}) (err error) { if err != nil { 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 { var val interface{} if 0 <= vr.Len() { diff --git a/query_test.go b/query_test.go index 457bc1fb..f08887b5 100644 --- a/query_test.go +++ b/query_test.go @@ -3,6 +3,7 @@ package pgx_test import ( "bytes" "database/sql" + "fmt" "strings" "testing" "time" @@ -291,6 +292,67 @@ func TestConnQueryScanner(t *testing.T) { 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) { t.Parallel() diff --git a/values.go b/values.go index b4466b82..938462d9 100644 --- a/values.go +++ b/values.go @@ -127,7 +127,9 @@ func (e SerializationError) Error() string { 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 { // 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 @@ -137,6 +139,18 @@ type Scanner interface { 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 // PostgreSQL server. type Encoder interface {