From b52421341541e98ca5a8c08d8da6ed6c6033720b Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Mon, 1 Jan 2024 21:37:49 -0800 Subject: [PATCH 01/16] Upgrade to pgx v5 - For pggen row structs, implement pgx.RowScanner. - Remove the typeResolver, since we're punting type resolution to the pgx.Conn type map for now. #74 --- example/acceptance_test.go | 8 +- example/author/query.sql.go | 94 ++++-------- example/author/query.sql_test.go | 2 +- example/complex_params/query.sql.go | 6 +- example/composite/codegen_test.go | 2 +- example/composite/query.sql.go | 6 +- example/composite/query.sql_test.go | 2 +- example/custom_types/query.sql.go | 6 +- example/custom_types/query.sql_test.go | 4 +- example/device/query.sql.go | 6 +- example/device/query.sql_test.go | 2 +- example/domain/query.sql.go | 6 +- example/enums/query.sql.go | 6 +- example/enums/query.sql_test.go | 2 +- example/erp/order/customer.sql.go | 6 +- example/erp/order/customer.sql_test.go | 2 +- example/erp/order/price.sql.go | 2 +- example/function/query.sql.go | 6 +- example/go_pointer_types/query.sql.go | 6 +- .../inline_param_count/inline0/query.sql.go | 6 +- .../inline0/query.sql_test.go | 2 +- .../inline_param_count/inline1/query.sql.go | 6 +- .../inline1/query.sql_test.go | 2 +- .../inline_param_count/inline2/query.sql.go | 6 +- .../inline2/query.sql_test.go | 2 +- .../inline_param_count/inline3/query.sql.go | 6 +- .../inline3/query.sql_test.go | 2 +- example/ltree/codegen_test.go | 4 +- example/ltree/query.sql.go | 6 +- example/ltree/query.sql_test.go | 2 +- example/nested/query.sql.go | 10 +- example/pgcrypto/query.sql.go | 6 +- .../separate_out_dir/out/alpha_query.sql.0.go | 6 +- example/slices/query.sql.go | 6 +- example/syntax/query.sql.go | 6 +- example/void/query.sql.go | 6 +- generate.go | 2 +- go.mod | 7 +- go.sum | 21 ++- internal/codegen/golang/declarer_composite.go | 2 +- internal/codegen/golang/gotype/known_types.go | 142 +++++++++--------- internal/codegen/golang/gotype/types.go | 2 +- internal/codegen/golang/query.gotemplate | 31 +--- internal/codegen/golang/templater.go | 8 +- internal/codegen/golang/type_resolver_test.go | 6 +- internal/pg/column.go | 17 +-- internal/pg/column_test.go | 7 +- internal/pg/known_types.go | 4 +- internal/pg/query.sql.go | 128 +++++----------- internal/pg/type_cache.go | 19 ++- internal/pg/type_fetcher.go | 31 ++-- internal/pg/type_fetcher_test.go | 7 +- internal/pg/types.go | 89 ++++++----- internal/pgdocker/pgdocker.go | 2 +- internal/pginfer/pginfer.go | 14 +- internal/pgplan/pgplan.go | 2 +- internal/pgtest/pg_test_db.go | 2 +- 57 files changed, 346 insertions(+), 455 deletions(-) diff --git a/example/acceptance_test.go b/example/acceptance_test.go index c1e231a9..5a8cb469 100644 --- a/example/acceptance_test.go +++ b/example/acceptance_test.go @@ -7,7 +7,7 @@ import ( "context" "flag" "fmt" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/errs" "github.com/jschaf/pggen/internal/pgdocker" "math/rand" @@ -57,7 +57,7 @@ func TestExamples(t *testing.T) { "--go-type", "int8=int", "--go-type", "int4=int", "--go-type", "text=string", - "--go-type", "citext=github.com/jackc/pgtype.Text", + "--go-type", "citext=github.com/jackc/pgx/v5/pgtype.Text", }, }, { @@ -179,8 +179,8 @@ func TestExamples(t *testing.T) { args: []string{ "--schema-glob", "example/ltree/schema.sql", "--query-glob", "example/ltree/query.sql", - "--go-type", "ltree=github.com/jackc/pgtype.Text", - "--go-type", "_ltree=github.com/jackc/pgtype.TextArray", + "--go-type", "ltree=github.com/jackc/pgx/v5/pgtype.Text", + "--go-type", "_ltree=github.com/jackc/pgx/v5/pgtype.TextArray", }, }, { diff --git a/example/author/query.sql.go b/example/author/query.sql.go index e654ea13..2b892bec 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -5,9 +5,8 @@ package author import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" ) // Querier is a typesafe Go interface backed by SQL queries. @@ -48,8 +47,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn // underlying Postgres transport to use } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -61,36 +59,7 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true -} - -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt + return &DBQuerier{conn: conn} } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -102,12 +71,17 @@ type FindAuthorByIDRow struct { Suffix *string `json:"suffix"` } +func (r *FindAuthorByIDRow) scanRow(rows pgx.Rows) error { + return rows.Scan(&r.AuthorID, &r.FirstName, &r.LastName, &r.Suffix) +} + // FindAuthorByID implements Querier.FindAuthorByID. func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAuthorByIDRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") row := q.conn.QueryRow(ctx, findAuthorByIDSQL, authorID) var item FindAuthorByIDRow - if err := row.Scan(&item.AuthorID, &item.FirstName, &item.LastName, &item.Suffix); err != nil { + s := scanner[*FindAuthorByIDRow]{&item} + if err := row.Scan(s); err != nil { return item, fmt.Errorf("query FindAuthorByID: %w", err) } return item, nil @@ -122,6 +96,10 @@ type FindAuthorsRow struct { Suffix *string `json:"suffix"` } +func (r *FindAuthorsRow) scanRow(rows pgx.Rows) error { + return rows.Scan(&r.AuthorID, &r.FirstName, &r.LastName, &r.Suffix) +} + // FindAuthors implements Querier.FindAuthors. func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAuthorsRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthors") @@ -131,9 +109,11 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu } defer rows.Close() items := []FindAuthorsRow{} + s := scanner[*FindAuthorsRow]{} for rows.Next() { var item FindAuthorsRow - if err := rows.Scan(&item.AuthorID, &item.FirstName, &item.LastName, &item.Suffix); err != nil { + s.item = &item + if err := rows.Scan(s); err != nil { return nil, fmt.Errorf("scan FindAuthors row: %w", err) } items = append(items, item) @@ -151,6 +131,10 @@ type FindAuthorNamesRow struct { LastName *string `json:"last_name"` } +func (r *FindAuthorNamesRow) scanRow(rows pgx.Rows) error { + return rows.Scan(&r.FirstName, &r.LastName) +} + // FindAuthorNames implements Querier.FindAuthorNames. func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]FindAuthorNamesRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorNames") @@ -160,9 +144,11 @@ func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]Find } defer rows.Close() items := []FindAuthorNamesRow{} + s := scanner[*FindAuthorNamesRow]{} for rows.Next() { var item FindAuthorNamesRow - if err := rows.Scan(&item.FirstName, &item.LastName); err != nil { + s.item = &item + if err := rows.Scan(s); err != nil { return nil, fmt.Errorf("scan FindAuthorNames row: %w", err) } items = append(items, item) @@ -275,12 +261,17 @@ type InsertAuthorSuffixRow struct { Suffix *string `json:"suffix"` } +func (r *InsertAuthorSuffixRow) scanRow(rows pgx.Rows) error { + return rows.Scan(&r.AuthorID, &r.FirstName, &r.LastName, &r.Suffix) +} + // InsertAuthorSuffix implements Querier.InsertAuthorSuffix. func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorSuffixParams) (InsertAuthorSuffixRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthorSuffix") row := q.conn.QueryRow(ctx, insertAuthorSuffixSQL, params.FirstName, params.LastName, params.Suffix) var item InsertAuthorSuffixRow - if err := row.Scan(&item.AuthorID, &item.FirstName, &item.LastName, &item.Suffix); err != nil { + s := scanner[*InsertAuthorSuffixRow]{&item} + if err := row.Scan(s); err != nil { return item, fmt.Errorf("query InsertAuthorSuffix: %w", err) } return item, nil @@ -312,27 +303,8 @@ func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]st return item, nil } -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } +type rowScanner interface{ scanRow(rows pgx.Rows) error } -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} +type scanner[T rowScanner] struct{ item T } -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 +func (s scanner[T]) ScanRow(rows pgx.Rows) error { return s.item.scanRow(rows) } diff --git a/example/author/query.sql_test.go b/example/author/query.sql_test.go index 9b4f753b..55521e45 100644 --- a/example/author/query.sql_test.go +++ b/example/author/query.sql_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" "testing" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" ) diff --git a/example/complex_params/query.sql.go b/example/complex_params/query.sql.go index 4315bdc5..fc0589f8 100644 --- a/example/complex_params/query.sql.go +++ b/example/complex_params/query.sql.go @@ -5,9 +5,9 @@ package complex_params import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/composite/codegen_test.go b/example/composite/codegen_test.go index 68334b1f..3972f024 100644 --- a/example/composite/codegen_test.go +++ b/example/composite/codegen_test.go @@ -28,7 +28,7 @@ func TestGenerate_Go_Example_Composite(t *testing.T) { "int8": "int", "int4": "int", "text": "string", - "citext": "github.com/jackc/pgtype.Text", + "citext": "github.com/jackc/pgx/v5/pgtype.Text", }, }) if err != nil { diff --git a/example/composite/query.sql.go b/example/composite/query.sql.go index c4b9b0b5..a62844af 100644 --- a/example/composite/query.sql.go +++ b/example/composite/query.sql.go @@ -5,9 +5,9 @@ package composite import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/composite/query.sql_test.go b/example/composite/query.sql_test.go index 1a9d0d0f..f8e8e82e 100644 --- a/example/composite/query.sql_test.go +++ b/example/composite/query.sql_test.go @@ -2,7 +2,7 @@ package composite import ( "context" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/difftest" "github.com/jschaf/pggen/internal/pgtest" "github.com/jschaf/pggen/internal/ptrs" diff --git a/example/custom_types/query.sql.go b/example/custom_types/query.sql.go index 133c424c..326d2b2d 100644 --- a/example/custom_types/query.sql.go +++ b/example/custom_types/query.sql.go @@ -5,9 +5,9 @@ package custom_types import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/example/custom_types/mytype" ) diff --git a/example/custom_types/query.sql_test.go b/example/custom_types/query.sql_test.go index 1912fe94..e98d7cd2 100644 --- a/example/custom_types/query.sql_test.go +++ b/example/custom_types/query.sql_test.go @@ -2,7 +2,7 @@ package custom_types import ( "context" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/jschaf/pggen/internal/texts" "github.com/stretchr/testify/assert" @@ -38,7 +38,7 @@ func TestQuerier_CustomMyInt(t *testing.T) { AND pn.nspname = current_schema() LIMIT 1; `)) - oidVal := pgtype.OIDValue{} + oidVal := uint32Value{} err := row.Scan(&oidVal) require.NoError(t, err) t.Logf("my_int oid: %d", oidVal.Uint) diff --git a/example/device/query.sql.go b/example/device/query.sql.go index d2b6fcd8..cdad2365 100644 --- a/example/device/query.sql.go +++ b/example/device/query.sql.go @@ -5,9 +5,9 @@ package device import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/device/query.sql_test.go b/example/device/query.sql_test.go index 3319f94e..95171f60 100644 --- a/example/device/query.sql_test.go +++ b/example/device/query.sql_test.go @@ -2,7 +2,7 @@ package device import ( "context" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/example/domain/query.sql.go b/example/domain/query.sql.go index 01130f2c..3d1de221 100644 --- a/example/domain/query.sql.go +++ b/example/domain/query.sql.go @@ -5,9 +5,9 @@ package domain import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/enums/query.sql.go b/example/enums/query.sql.go index cbaea2ed..c72c9d9c 100644 --- a/example/enums/query.sql.go +++ b/example/enums/query.sql.go @@ -5,9 +5,9 @@ package enums import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/enums/query.sql_test.go b/example/enums/query.sql_test.go index e4d87bd3..e36f04a5 100644 --- a/example/enums/query.sql_test.go +++ b/example/enums/query.sql_test.go @@ -2,7 +2,7 @@ package enums import ( "context" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/example/erp/order/customer.sql.go b/example/erp/order/customer.sql.go index ca372b20..45aca9c8 100644 --- a/example/erp/order/customer.sql.go +++ b/example/erp/order/customer.sql.go @@ -5,9 +5,9 @@ package order import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/erp/order/customer.sql_test.go b/example/erp/order/customer.sql_test.go index f979954d..83498397 100644 --- a/example/erp/order/customer.sql_test.go +++ b/example/erp/order/customer.sql_test.go @@ -2,7 +2,7 @@ package order import ( "context" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/example/erp/order/price.sql.go b/example/erp/order/price.sql.go index 2395a2ca..c4c0b9ab 100644 --- a/example/erp/order/price.sql.go +++ b/example/erp/order/price.sql.go @@ -5,7 +5,7 @@ package order import ( "context" "fmt" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" ) const findOrdersByPriceSQL = `SELECT * FROM orders WHERE order_total > $1;` diff --git a/example/function/query.sql.go b/example/function/query.sql.go index ebfd5e7c..c7ba5bd0 100644 --- a/example/function/query.sql.go +++ b/example/function/query.sql.go @@ -5,9 +5,9 @@ package function import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/go_pointer_types/query.sql.go b/example/go_pointer_types/query.sql.go index fce3f65d..08276e52 100644 --- a/example/go_pointer_types/query.sql.go +++ b/example/go_pointer_types/query.sql.go @@ -5,9 +5,9 @@ package go_pointer_types import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/inline_param_count/inline0/query.sql.go b/example/inline_param_count/inline0/query.sql.go index 4b4bbace..c63d4b90 100644 --- a/example/inline_param_count/inline0/query.sql.go +++ b/example/inline_param_count/inline0/query.sql.go @@ -5,9 +5,9 @@ package inline0 import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/inline_param_count/inline0/query.sql_test.go b/example/inline_param_count/inline0/query.sql_test.go index 2babf0a2..a7b1bb82 100644 --- a/example/inline_param_count/inline0/query.sql_test.go +++ b/example/inline_param_count/inline0/query.sql_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" "testing" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" ) diff --git a/example/inline_param_count/inline1/query.sql.go b/example/inline_param_count/inline1/query.sql.go index 0583f0df..2b55216d 100644 --- a/example/inline_param_count/inline1/query.sql.go +++ b/example/inline_param_count/inline1/query.sql.go @@ -5,9 +5,9 @@ package inline1 import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/inline_param_count/inline1/query.sql_test.go b/example/inline_param_count/inline1/query.sql_test.go index c3bbfc9a..af989259 100644 --- a/example/inline_param_count/inline1/query.sql_test.go +++ b/example/inline_param_count/inline1/query.sql_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" "testing" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" ) diff --git a/example/inline_param_count/inline2/query.sql.go b/example/inline_param_count/inline2/query.sql.go index b5624ca2..113ecb35 100644 --- a/example/inline_param_count/inline2/query.sql.go +++ b/example/inline_param_count/inline2/query.sql.go @@ -5,9 +5,9 @@ package inline2 import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/inline_param_count/inline2/query.sql_test.go b/example/inline_param_count/inline2/query.sql_test.go index b119f7fb..a17347f1 100644 --- a/example/inline_param_count/inline2/query.sql_test.go +++ b/example/inline_param_count/inline2/query.sql_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" "testing" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" ) diff --git a/example/inline_param_count/inline3/query.sql.go b/example/inline_param_count/inline3/query.sql.go index 8940776a..88f0c874 100644 --- a/example/inline_param_count/inline3/query.sql.go +++ b/example/inline_param_count/inline3/query.sql.go @@ -5,9 +5,9 @@ package inline3 import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/inline_param_count/inline3/query.sql_test.go b/example/inline_param_count/inline3/query.sql_test.go index 527a983b..b4e9121f 100644 --- a/example/inline_param_count/inline3/query.sql_test.go +++ b/example/inline_param_count/inline3/query.sql_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/require" "testing" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" ) diff --git a/example/ltree/codegen_test.go b/example/ltree/codegen_test.go index f5f1fec1..2a05d1f9 100644 --- a/example/ltree/codegen_test.go +++ b/example/ltree/codegen_test.go @@ -23,8 +23,8 @@ func TestGenerate_Go_Example_ltree(t *testing.T) { Language: pggen.LangGo, InlineParamCount: 2, TypeOverrides: map[string]string{ - "ltree": "github.com/jackc/pgtype.Text", - "_ltree": "github.com/jackc/pgtype.TextArray", + "ltree": "github.com/jackc/pgx/v5/pgtype.Text", + "_ltree": "github.com/jackc/pgx/v5/pgtype.TextArray", }, }) if err != nil { diff --git a/example/ltree/query.sql.go b/example/ltree/query.sql.go index aee3db7f..537e2343 100644 --- a/example/ltree/query.sql.go +++ b/example/ltree/query.sql.go @@ -5,9 +5,9 @@ package ltree import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/ltree/query.sql_test.go b/example/ltree/query.sql_test.go index 83d0d8d9..ce591bbc 100644 --- a/example/ltree/query.sql_test.go +++ b/example/ltree/query.sql_test.go @@ -2,7 +2,7 @@ package ltree import ( "context" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/example/nested/query.sql.go b/example/nested/query.sql.go index 1ffb4074..f1bc80bd 100644 --- a/example/nested/query.sql.go +++ b/example/nested/query.sql.go @@ -5,9 +5,9 @@ package nested import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. @@ -57,7 +57,7 @@ type ProductImageType struct { // typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name + connInfo *pgtype.Codec // types by Postgres type name } func newTypeResolver() *typeResolver { @@ -148,6 +148,7 @@ func (tr *typeResolver) newDimensions() pgtype.ValueTranscoder { // newProductImageSetType creates a new pgtype.ValueTranscoder for the Postgres // composite type 'product_image_set_type'. func (tr *typeResolver) newProductImageSetType() pgtype.ValueTranscoder { + pgtype.CompositeCodec{} return tr.newCompositeValue( "product_image_set_type", compositeField{name: "name", typeName: "text", defaultVal: &pgtype.Text{}}, @@ -169,6 +170,7 @@ func (tr *typeResolver) newProductImageType() pgtype.ValueTranscoder { // newProductImageTypeArray creates a new pgtype.ValueTranscoder for the Postgres // '_product_image_type' array type. func (tr *typeResolver) newProductImageTypeArray() pgtype.ValueTranscoder { + pgtype.CompositeCodec{} return tr.newArrayValue("_product_image_type", "product_image_type", tr.newProductImageType) } diff --git a/example/pgcrypto/query.sql.go b/example/pgcrypto/query.sql.go index 90be999c..ff478512 100644 --- a/example/pgcrypto/query.sql.go +++ b/example/pgcrypto/query.sql.go @@ -5,9 +5,9 @@ package pgcrypto import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/separate_out_dir/out/alpha_query.sql.0.go b/example/separate_out_dir/out/alpha_query.sql.0.go index bfebba47..f99f15dc 100644 --- a/example/separate_out_dir/out/alpha_query.sql.0.go +++ b/example/separate_out_dir/out/alpha_query.sql.0.go @@ -5,9 +5,9 @@ package out import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/slices/query.sql.go b/example/slices/query.sql.go index 8ef9062c..ed0fad12 100644 --- a/example/slices/query.sql.go +++ b/example/slices/query.sql.go @@ -5,9 +5,9 @@ package slices import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" "time" ) diff --git a/example/syntax/query.sql.go b/example/syntax/query.sql.go index b221822c..85bf0661 100644 --- a/example/syntax/query.sql.go +++ b/example/syntax/query.sql.go @@ -5,9 +5,9 @@ package syntax import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/example/void/query.sql.go b/example/void/query.sql.go index 62fb382e..146b3cf1 100644 --- a/example/void/query.sql.go +++ b/example/void/query.sql.go @@ -5,9 +5,9 @@ package void import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. diff --git a/generate.go b/generate.go index 94aef72c..d7bf8699 100644 --- a/generate.go +++ b/generate.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/ast" "github.com/jschaf/pggen/internal/codegen" "github.com/jschaf/pggen/internal/codegen/golang" diff --git a/go.mod b/go.mod index a46a9a7f..db08d690 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/jackc/pgproto3/v2 v2.3.2 github.com/jackc/pgtype v1.14.0 github.com/jackc/pgx/v4 v4.18.1 + github.com/jackc/pgx/v5 v5.5.1 github.com/peterbourgon/ff/v3 v3.4.0 github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.8.4 @@ -26,8 +27,7 @@ require ( github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/kr/pretty v0.2.1 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/kr/text v0.2.0 // indirect github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect github.com/morikuni/aec v1.0.0 // indirect @@ -35,7 +35,8 @@ require ( github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.16.0 // indirect + github.com/rogpeppe/go-internal v1.6.1 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.15.0 // indirect diff --git a/go.sum b/go.sum index 32ca3a76..28462699 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,9 @@ github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwX github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= @@ -81,17 +82,22 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= +github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= +github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -123,6 +129,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -175,8 +183,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -258,8 +266,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/codegen/golang/declarer_composite.go b/internal/codegen/golang/declarer_composite.go index 3dc1988f..34625c04 100644 --- a/internal/codegen/golang/declarer_composite.go +++ b/internal/codegen/golang/declarer_composite.go @@ -263,7 +263,7 @@ func (c CompositeInitDeclarer) Declare(string) (string, error) { // of a composite type as a generic array: []interface{}. Necessary because we // can only set pgtype.CompositeType from a []interface{}. // -// Revisit after https://github.com/jackc/pgtype/pull/100 to see if we can +// Revisit after https://github.com/jackc/pgx/v5/pgtype/pull/100 to see if we can // simplify. type CompositeRawDeclarer struct { typ *gotype.CompositeType diff --git a/internal/codegen/golang/gotype/known_types.go b/internal/codegen/golang/gotype/known_types.go index 6d893f52..87ed8b4b 100644 --- a/internal/codegen/golang/gotype/known_types.go +++ b/internal/codegen/golang/gotype/known_types.go @@ -1,14 +1,14 @@ package gotype import ( - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pg" "github.com/jschaf/pggen/internal/pg/pgoid" ) // FindKnownTypePgx returns the native pgx type, like pgtype.Text, if known, for // a Postgres OID. If there is no known type, returns nil. -func FindKnownTypePgx(oid pgtype.OID) (Type, bool) { +func FindKnownTypePgx(oid uint32) (Type, bool) { typ, ok := knownTypesByOID[oid] return typ.pgNative, ok } @@ -16,7 +16,7 @@ func FindKnownTypePgx(oid pgtype.OID) (Type, bool) { // FindKnownTypeNullable returns the nullable type, like *string, if known, for // a Postgres OID. Falls back to the pgNative type. If there is no known type // for the OID, returns nil. -func FindKnownTypeNullable(oid pgtype.OID) (Type, bool) { +func FindKnownTypeNullable(oid uint32) (Type, bool) { typ, ok := knownTypesByOID[oid] if !ok { return nil, false @@ -30,7 +30,7 @@ func FindKnownTypeNullable(oid pgtype.OID) (Type, bool) { // FindKnownTypeNonNullable returns the non-nullable type like string, if known, // for a Postgres OID. Falls back to the nullable type and pgNative type. If // there is no known type for the OID, returns nil. -func FindKnownTypeNonNullable(oid pgtype.OID) (Type, bool) { +func FindKnownTypeNonNullable(oid uint32) (Type, bool) { typ, ok := knownTypesByOID[oid] if !ok { return nil, false @@ -91,73 +91,73 @@ var ( // pgtype types prefixed with "pg". var ( - PgBool = MustParseKnownType("github.com/jackc/pgtype.Bool", pg.Bool) - PgQChar = MustParseKnownType("github.com/jackc/pgtype.QChar", pg.QChar) - PgName = MustParseKnownType("github.com/jackc/pgtype.Name", pg.Name) - PgInt8 = MustParseKnownType("github.com/jackc/pgtype.Int8", pg.Int8) - PgInt2 = MustParseKnownType("github.com/jackc/pgtype.Int2", pg.Int2) - PgInt4 = MustParseKnownType("github.com/jackc/pgtype.Int4", pg.Int4) - PgText = MustParseKnownType("github.com/jackc/pgtype.Text", pg.Text) - PgBytea = MustParseKnownType("github.com/jackc/pgtype.Bytea", pg.Bytea) - PgOID = MustParseKnownType("github.com/jackc/pgtype.OID", pg.OID) - PgTID = MustParseKnownType("github.com/jackc/pgtype.TID", pg.TID) - PgXID = MustParseKnownType("github.com/jackc/pgtype.XID", pg.XID) - PgCID = MustParseKnownType("github.com/jackc/pgtype.CID", pg.CID) - PgJSON = MustParseKnownType("github.com/jackc/pgtype.JSON", pg.JSON) - PgPoint = MustParseKnownType("github.com/jackc/pgtype.Point", pg.Point) - PgLseg = MustParseKnownType("github.com/jackc/pgtype.Lseg", pg.Lseg) - PgPath = MustParseKnownType("github.com/jackc/pgtype.Path", pg.Path) - PgBox = MustParseKnownType("github.com/jackc/pgtype.Box", pg.Box) - PgPolygon = MustParseKnownType("github.com/jackc/pgtype.Polygon", pg.Polygon) - PgLine = MustParseKnownType("github.com/jackc/pgtype.Line", pg.Line) - PgCIDR = MustParseKnownType("github.com/jackc/pgtype.CIDR", pg.CIDR) - PgCIDRArray = MustParseKnownType("github.com/jackc/pgtype.CIDRArray", pg.CIDRArray) - PgFloat4 = MustParseKnownType("github.com/jackc/pgtype.Float4", pg.Float4) - PgFloat8 = MustParseKnownType("github.com/jackc/pgtype.Float8", pg.Float8) - PgUnknown = MustParseKnownType("github.com/jackc/pgtype.Unknown", pg.Unknown) - PgCircle = MustParseKnownType("github.com/jackc/pgtype.Circle", pg.Circle) - PgMacaddr = MustParseKnownType("github.com/jackc/pgtype.Macaddr", pg.Macaddr) - PgInet = MustParseKnownType("github.com/jackc/pgtype.Inet", pg.Inet) - PgBoolArray = MustParseKnownType("github.com/jackc/pgtype.BoolArray", pg.BoolArray) - PgByteaArray = MustParseKnownType("github.com/jackc/pgtype.ByteaArray", pg.ByteaArray) - PgInt2Array = MustParseKnownType("github.com/jackc/pgtype.Int2Array", pg.Int2Array) - PgInt4Array = MustParseKnownType("github.com/jackc/pgtype.Int4Array", pg.Int4Array) - PgTextArray = MustParseKnownType("github.com/jackc/pgtype.TextArray", pg.TextArray) - PgBPCharArray = MustParseKnownType("github.com/jackc/pgtype.BPCharArray", pg.BPCharArray) - PgVarcharArray = MustParseKnownType("github.com/jackc/pgtype.VarcharArray", pg.VarcharArray) - PgInt8Array = MustParseKnownType("github.com/jackc/pgtype.Int8Array", pg.Int8Array) - PgFloat4Array = MustParseKnownType("github.com/jackc/pgtype.Float4Array", pg.Float4Array) - PgFloat8Array = MustParseKnownType("github.com/jackc/pgtype.Float8Array", pg.Float8Array) - PgACLItem = MustParseKnownType("github.com/jackc/pgtype.ACLItem", pg.ACLItem) - PgACLItemArray = MustParseKnownType("github.com/jackc/pgtype.ACLItemArray", pg.ACLItemArray) - PgInetArray = MustParseKnownType("github.com/jackc/pgtype.InetArray", pg.InetArray) - PgMacaddrArray = MustParseKnownType("github.com/jackc/pgtype.MacaddrArray", pg.MacaddrArray) - PgBPChar = MustParseKnownType("github.com/jackc/pgtype.BPChar", pg.BPChar) - PgVarchar = MustParseKnownType("github.com/jackc/pgtype.Varchar", pg.Varchar) - PgDate = MustParseKnownType("github.com/jackc/pgtype.Date", pg.Date) - PgTime = MustParseKnownType("github.com/jackc/pgtype.Time", pg.Time) - PgTimestamp = MustParseKnownType("github.com/jackc/pgtype.Timestamp", pg.Timestamp) - PgTimestampArray = MustParseKnownType("github.com/jackc/pgtype.TimestampArray", pg.TimestampArray) - PgDateArray = MustParseKnownType("github.com/jackc/pgtype.DateArray", pg.DateArray) - PgTimestamptz = MustParseKnownType("github.com/jackc/pgtype.Timestamptz", pg.Timestamptz) - PgTimestamptzArray = MustParseKnownType("github.com/jackc/pgtype.TimestamptzArray", pg.TimestamptzArray) - PgInterval = MustParseKnownType("github.com/jackc/pgtype.Interval", pg.Interval) - PgNumericArray = MustParseKnownType("github.com/jackc/pgtype.NumericArray", pg.NumericArray) - PgBit = MustParseKnownType("github.com/jackc/pgtype.Bit", pg.Bit) - PgVarbit = MustParseKnownType("github.com/jackc/pgtype.Varbit", pg.Varbit) + PgBool = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Bool", pg.Bool) + PgQChar = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.QChar", pg.QChar) + PgName = MustParseKnownType("string", pg.Name) + PgInt8 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int8", pg.Int8) + PgInt2 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int2", pg.Int2) + PgInt4 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int4", pg.Int4) + PgText = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Text", pg.Text) + PgBytea = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Bytea", pg.Bytea) + PgOID = MustParseKnownType("uint32", pg.OID) + PgTID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.TID", pg.TID) + PgXID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.XID", pg.XID) + PgCID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.CID", pg.CID) + PgJSON = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.JSON", pg.JSON) + PgPoint = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Point", pg.Point) + PgLseg = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Lseg", pg.Lseg) + PgPath = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Path", pg.Path) + PgBox = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Box", pg.Box) + PgPolygon = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Polygon", pg.Polygon) + PgLine = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Line", pg.Line) + PgCIDR = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.CIDR", pg.CIDR) + PgCIDRArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.CIDRArray", pg.CIDRArray) + PgFloat4 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Float4", pg.Float4) + PgFloat8 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Float8", pg.Float8) + PgUnknown = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Unknown", pg.Unknown) + PgCircle = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Circle", pg.Circle) + PgMacaddr = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Macaddr", pg.Macaddr) + PgInet = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Inet", pg.Inet) + PgBoolArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.BoolArray", pg.BoolArray) + PgByteaArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.ByteaArray", pg.ByteaArray) + PgInt2Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int2Array", pg.Int2Array) + PgInt4Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int4Array", pg.Int4Array) + PgTextArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.TextArray", pg.TextArray) + PgBPCharArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.BPCharArray", pg.BPCharArray) + PgVarcharArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.VarcharArray", pg.VarcharArray) + PgInt8Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int8Array", pg.Int8Array) + PgFloat4Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Float4Array", pg.Float4Array) + PgFloat8Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Float8Array", pg.Float8Array) + PgACLItem = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.ACLItem", pg.ACLItem) + PgACLItemArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.ACLItemArray", pg.ACLItemArray) + PgInetArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.InetArray", pg.InetArray) + PgMacaddrArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.MacaddrArray", pg.MacaddrArray) + PgBPChar = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.BPChar", pg.BPChar) + PgVarchar = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Varchar", pg.Varchar) + PgDate = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Date", pg.Date) + PgTime = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Time", pg.Time) + PgTimestamp = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Timestamp", pg.Timestamp) + PgTimestampArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.TimestampArray", pg.TimestampArray) + PgDateArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.DateArray", pg.DateArray) + PgTimestamptz = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Timestamptz", pg.Timestamptz) + PgTimestamptzArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.TimestamptzArray", pg.TimestamptzArray) + PgInterval = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Interval", pg.Interval) + PgNumericArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.NumericArray", pg.NumericArray) + PgBit = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Bit", pg.Bit) + PgVarbit = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Varbit", pg.Varbit) PgVoid = &VoidType{} - PgNumeric = MustParseKnownType("github.com/jackc/pgtype.Numeric", pg.Numeric) - PgRecord = MustParseKnownType("github.com/jackc/pgtype.Record", pg.Record) - PgUUID = MustParseKnownType("github.com/jackc/pgtype.UUID", pg.UUID) - PgUUIDArray = MustParseKnownType("github.com/jackc/pgtype.UUIDArray", pg.UUIDArray) - PgJSONB = MustParseKnownType("github.com/jackc/pgtype.JSONB", pg.JSONB) - PgJSONBArray = MustParseKnownType("github.com/jackc/pgtype.JSONBArray", pg.JSONBArray) - PgInt4range = MustParseKnownType("github.com/jackc/pgtype.Int4range", pg.Int4range) - PgNumrange = MustParseKnownType("github.com/jackc/pgtype.Numrange", pg.Numrange) - PgTsrange = MustParseKnownType("github.com/jackc/pgtype.Tsrange", pg.Tsrange) - PgTstzrange = MustParseKnownType("github.com/jackc/pgtype.Tstzrange", pg.Tstzrange) - PgDaterange = MustParseKnownType("github.com/jackc/pgtype.Daterange", pg.Daterange) - PgInt8range = MustParseKnownType("github.com/jackc/pgtype.Int8range", pg.Int8range) + PgNumeric = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Numeric", pg.Numeric) + PgRecord = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Record", pg.Record) + PgUUID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.UUID", pg.UUID) + PgUUIDArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.UUIDArray", pg.UUIDArray) + PgJSONB = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.JSONB", pg.JSONB) + PgJSONBArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.JSONBArray", pg.JSONBArray) + PgInt4range = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int4range", pg.Int4range) + PgNumrange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Numrange", pg.Numrange) + PgTsrange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Tsrange", pg.Tsrange) + PgTstzrange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Tstzrange", pg.Tstzrange) + PgDaterange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Daterange", pg.Daterange) + PgInt8range = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int8range", pg.Int8range) ) // knownGoType is the native pgtype type, the nullable and non-nullable types @@ -177,7 +177,7 @@ var ( // "string" for a Postgres text type. type knownGoType struct{ pgNative, nullable, nonNullable Type } -var knownTypesByOID = map[pgtype.OID]knownGoType{ +var knownTypesByOID = map[uint32]knownGoType{ pgtype.BoolOID: {PgBool, Boolp, Bool}, pgtype.QCharOID: {PgQChar, nil, nil}, pgtype.NameOID: {PgName, nil, nil}, diff --git a/internal/codegen/golang/gotype/types.go b/internal/codegen/golang/gotype/types.go index 65e8fce6..199d1e86 100644 --- a/internal/codegen/golang/gotype/types.go +++ b/internal/codegen/golang/gotype/types.go @@ -240,7 +240,7 @@ func ParseOpaqueType(qualType string, pgType pg.Type) (Type, error) { } // MustParseKnownType creates a gotype.Type by parsing a fully qualified Go type -// that pgx supports natively like "github.com/jackc/pgtype.Int4Array", or most +// that pgx supports natively like "github.com/jackc/pgx/v5/pgtype.Int4Array", or most // builtin types like "string" and []*int16. func MustParseKnownType(qualType string, pgType pg.Type) Type { typ, err := ParseOpaqueType(qualType, pgType) diff --git a/internal/codegen/golang/query.gotemplate b/internal/codegen/golang/query.gotemplate index 720befbb..d6456671 100644 --- a/internal/codegen/golang/query.gotemplate +++ b/internal/codegen/golang/query.gotemplate @@ -28,7 +28,6 @@ var _ Querier = &DBQuerier{} type DBQuerier struct { conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -40,7 +39,7 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } {{- range .Declarers}}{{- "\n\n" -}}{{ .Declare $.PkgPath }}{{ end -}} @@ -93,33 +92,5 @@ func (q *DBQuerier) {{ $q.Name }}(ctx context.Context {{- $q.EmitParams }}) ({{ {{- end }} } {{- end -}} - -{{- if .IsLeader -}} -{{- "\n\n" -}} -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 -{{- end -}} {{- "\n" -}} {{- end -}} diff --git a/internal/codegen/golang/templater.go b/internal/codegen/golang/templater.go index 150035bf..de73461e 100644 --- a/internal/codegen/golang/templater.go +++ b/internal/codegen/golang/templater.go @@ -73,7 +73,7 @@ func (tm Templater) TemplateAll(files []codegen.QueryFile) ([]TemplatedFile, err pgconnIdx := -1 imports := file.Imports for i, pkg := range imports { - if pkg == "github.com/jackc/pgconn" { + if pkg == "github.com/jackc/pgx/v5/pgconn" { pgconnIdx = i break } @@ -113,10 +113,10 @@ func (tm Templater) templateFile(file codegen.QueryFile, isLeader bool) (Templat imports := NewImportSet() imports.AddPackage("context") imports.AddPackage("fmt") - imports.AddPackage("github.com/jackc/pgconn") + imports.AddPackage("github.com/jackc/pgx/v5/pgconn") if isLeader { - imports.AddPackage("github.com/jackc/pgtype") - imports.AddPackage("github.com/jackc/pgx/v4") + imports.AddPackage("github.com/jackc/pgx/v5/pgtype") + imports.AddPackage("github.com/jackc/pgx/v5") } pkgPath := "" diff --git a/internal/codegen/golang/type_resolver_test.go b/internal/codegen/golang/type_resolver_test.go index e31492e3..bd371049 100644 --- a/internal/codegen/golang/type_resolver_test.go +++ b/internal/codegen/golang/type_resolver_test.go @@ -2,7 +2,7 @@ package golang import ( "github.com/google/go-cmp/cmp" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/casing" "github.com/jschaf/pggen/internal/codegen/golang/gotype" "github.com/jschaf/pggen/internal/difftest" @@ -88,7 +88,7 @@ func TestTypeResolver_Resolve(t *testing.T) { pgType: pg.BaseType{Name: "point", ID: pgtype.PointOID}, nullable: false, want: &gotype.ImportType{ - PkgPath: "github.com/jackc/pgtype", + PkgPath: "github.com/jackc/pgx/v5/pgtype", Type: &gotype.OpaqueType{ PgType: pg.BaseType{Name: "point", ID: pgtype.PointOID}, Name: "Point", @@ -100,7 +100,7 @@ func TestTypeResolver_Resolve(t *testing.T) { pgType: pg.BaseType{Name: "point", ID: pgtype.PointOID}, nullable: true, want: &gotype.ImportType{ - PkgPath: "github.com/jackc/pgtype", + PkgPath: "github.com/jackc/pgx/v5/pgtype", Type: &gotype.OpaqueType{ PgType: pg.BaseType{Name: "point", ID: pgtype.PointOID}, Name: "Point", diff --git a/internal/pg/column.go b/internal/pg/column.go index e381ec71..9215205d 100644 --- a/internal/pg/column.go +++ b/internal/pg/column.go @@ -3,8 +3,7 @@ package pg import ( "context" "fmt" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/texts" "strconv" "strings" @@ -15,18 +14,18 @@ import ( // Column stores information about a column in a TableOID. // https://www.postgresql.org/docs/13/catalog-pg-attribute.html type Column struct { - Name string // pg_attribute.attname: column name - TableOID pgtype.OID // pg_attribute:attrelid: table the column belongs to - TableName string // pg_class.relname: name of table that owns the column - Number uint16 // pg_attribute.attnum: the number of column starting from 1 - Type Type // pg_attribute.atttypid: data type of the column - Null bool // pg_attribute.attnotnull: represents a not-null constraint + Name string // pg_attribute.attname: column name + TableOID uint32 // pg_attribute:attrelid: table the column belongs to + TableName string // pg_class.relname: name of table that owns the column + Number uint16 // pg_attribute.attnum: the number of column starting from 1 + Type Type // pg_attribute.atttypid: data type of the column + Null bool // pg_attribute.attnotnull: represents a not-null constraint } // ColumnKey is a composite key of a table OID and the number of the column // within the table. type ColumnKey struct { - TableOID pgtype.OID + TableOID uint32 Number uint16 // the number of column starting from 1 } diff --git a/internal/pg/column_test.go b/internal/pg/column_test.go index c437c89f..5465b77f 100644 --- a/internal/pg/column_test.go +++ b/internal/pg/column_test.go @@ -3,8 +3,7 @@ package pg import ( "context" "github.com/google/go-cmp/cmp" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/pgtest" "github.com/jschaf/pggen/internal/texts" "github.com/stretchr/testify/assert" @@ -74,7 +73,7 @@ func TestFetchColumns(t *testing.T) { } } -func findTableOID(t *testing.T, conn *pgx.Conn, table string) pgtype.OID { +func findTableOID(t *testing.T, conn *pgx.Conn, table string) uint32 { sql := texts.Dedent(` SELECT oid AS table_oid FROM pg_class @@ -85,7 +84,7 @@ func findTableOID(t *testing.T, conn *pgx.Conn, table string) pgtype.OID { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() row := conn.QueryRow(ctx, sql, table) - var oid pgtype.OID = 0 + var oid uint32 = 0 if err := row.Scan(&oid); err != nil && err != pgx.ErrNoRows { t.Fatal(err) } diff --git a/internal/pg/known_types.go b/internal/pg/known_types.go index 5540ad30..a78702c8 100644 --- a/internal/pg/known_types.go +++ b/internal/pg/known_types.go @@ -1,7 +1,7 @@ package pg import ( - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pg/pgoid" ) @@ -81,7 +81,7 @@ var ( ) // All known Postgres types by OID. -var defaultKnownTypes = map[pgtype.OID]Type{ +var defaultKnownTypes = map[uint32]Type{ pgtype.BoolOID: Bool, pgtype.ByteaOID: Bytea, pgtype.QCharOID: QChar, diff --git a/internal/pg/query.sql.go b/internal/pg/query.sql.go index 61f49aa9..af7b16a3 100644 --- a/internal/pg/query.sql.go +++ b/internal/pg/query.sql.go @@ -5,9 +5,8 @@ package pg import ( "context" "fmt" - "github.com/jackc/pgconn" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" ) // Querier is a typesafe Go interface backed by SQL queries. @@ -23,11 +22,11 @@ type Querier interface { // Recursively expands all given OIDs to all descendants through composite // types. - FindDescendantOIDs(ctx context.Context, oids []uint32) ([]pgtype.OID, error) + FindDescendantOIDs(ctx context.Context, oids []uint32) ([]uint32, error) - FindOIDByName(ctx context.Context, name string) (pgtype.OID, error) + FindOIDByName(ctx context.Context, name string) (uint32, error) - FindOIDName(ctx context.Context, oid pgtype.OID) (pgtype.Name, error) + FindOIDName(ctx context.Context, oid uint32) (string, error) FindOIDNames(ctx context.Context, oid []uint32) ([]FindOIDNamesRow, error) } @@ -35,8 +34,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn // underlying Postgres transport to use } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -48,36 +46,7 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true -} - -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt + return &DBQuerier{conn: conn} } const findEnumTypesSQL = `WITH enums AS ( @@ -125,13 +94,13 @@ WHERE typ.typisdefined AND typ.oid = ANY ($1::oid[]);` type FindEnumTypesRow struct { - OID pgtype.OID `json:"oid"` - TypeName string `json:"type_name"` - ChildOIDs []int `json:"child_oids"` - Orders []float32 `json:"orders"` - Labels []string `json:"labels"` - TypeKind pgtype.QChar `json:"type_kind"` - DefaultExpr string `json:"default_expr"` + OID uint32 `json:"oid"` + TypeName string `json:"type_name"` + ChildOIDs []int `json:"child_oids"` + Orders []float32 `json:"orders"` + Labels []string `json:"labels"` + TypeKind byte `json:"type_kind"` + DefaultExpr string `json:"default_expr"` } // FindEnumTypes implements Querier.FindEnumTypes. @@ -186,10 +155,10 @@ WHERE arr_typ.typisdefined AND arr_typ.oid = ANY ($1::oid[]);` type FindArrayTypesRow struct { - OID pgtype.OID `json:"oid"` - TypeName string `json:"type_name"` - ElemOID pgtype.OID `json:"elem_oid"` - TypeKind pgtype.QChar `json:"type_kind"` + OID uint32 `json:"oid"` + TypeName string `json:"type_name"` + ElemOID uint32 `json:"elem_oid"` + TypeKind byte `json:"type_kind"` } // FindArrayTypes implements Querier.FindArrayTypes. @@ -245,14 +214,14 @@ WHERE typ.oid = ANY ($1::oid[]) AND typ.typtype = 'c';` type FindCompositeTypesRow struct { - TableTypeName string `json:"table_type_name"` - TableTypeOID pgtype.OID `json:"table_type_oid"` - TableName pgtype.Name `json:"table_name"` - ColNames []string `json:"col_names"` - ColOIDs []int `json:"col_oids"` - ColOrders []int `json:"col_orders"` - ColNotNulls pgtype.BoolArray `json:"col_not_nulls"` - ColTypeNames []string `json:"col_type_names"` + TableTypeName string `json:"table_type_name"` + TableTypeOID uint32 `json:"table_type_oid"` + TableName string `json:"table_name"` + ColNames []string `json:"col_names"` + ColOIDs []int `json:"col_oids"` + ColOrders []int `json:"col_orders"` + ColNotNulls []bool `json:"col_not_nulls"` + ColTypeNames []string `json:"col_type_names"` } // FindCompositeTypes implements Querier.FindCompositeTypes. @@ -306,16 +275,16 @@ SELECT oid FROM oid_descs;` // FindDescendantOIDs implements Querier.FindDescendantOIDs. -func (q *DBQuerier) FindDescendantOIDs(ctx context.Context, oids []uint32) ([]pgtype.OID, error) { +func (q *DBQuerier) FindDescendantOIDs(ctx context.Context, oids []uint32) ([]uint32, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindDescendantOIDs") rows, err := q.conn.Query(ctx, findDescendantOIDsSQL, oids) if err != nil { return nil, fmt.Errorf("query FindDescendantOIDs: %w", err) } defer rows.Close() - items := []pgtype.OID{} + items := []uint32{} for rows.Next() { - var item pgtype.OID + var item uint32 if err := rows.Scan(&item); err != nil { return nil, fmt.Errorf("scan FindDescendantOIDs row: %w", err) } @@ -334,10 +303,10 @@ ORDER BY oid DESC LIMIT 1;` // FindOIDByName implements Querier.FindOIDByName. -func (q *DBQuerier) FindOIDByName(ctx context.Context, name string) (pgtype.OID, error) { +func (q *DBQuerier) FindOIDByName(ctx context.Context, name string) (uint32, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindOIDByName") row := q.conn.QueryRow(ctx, findOIDByNameSQL, name) - var item pgtype.OID + var item uint32 if err := row.Scan(&item); err != nil { return item, fmt.Errorf("query FindOIDByName: %w", err) } @@ -349,10 +318,10 @@ FROM pg_type WHERE oid = $1;` // FindOIDName implements Querier.FindOIDName. -func (q *DBQuerier) FindOIDName(ctx context.Context, oid pgtype.OID) (pgtype.Name, error) { +func (q *DBQuerier) FindOIDName(ctx context.Context, oid uint32) (string, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindOIDName") row := q.conn.QueryRow(ctx, findOIDNameSQL, oid) - var item pgtype.Name + var item string if err := row.Scan(&item); err != nil { return item, fmt.Errorf("query FindOIDName: %w", err) } @@ -364,9 +333,9 @@ FROM pg_type WHERE oid = ANY ($1::oid[]);` type FindOIDNamesRow struct { - OID pgtype.OID `json:"oid"` - Name pgtype.Name `json:"name"` - Kind pgtype.QChar `json:"kind"` + OID uint32 `json:"oid"` + Name string `json:"name"` + Kind byte `json:"kind"` } // FindOIDNames implements Querier.FindOIDNames. @@ -390,28 +359,3 @@ func (q *DBQuerier) FindOIDNames(ctx context.Context, oid []uint32) ([]FindOIDNa } return items, err } - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/internal/pg/type_cache.go b/internal/pg/type_cache.go index d21a060a..53101a37 100644 --- a/internal/pg/type_cache.go +++ b/internal/pg/type_cache.go @@ -1,18 +1,17 @@ package pg import ( - "github.com/jackc/pgtype" "sync" ) // typeCache caches a map from a Postgres pg_type.oid to a Type. type typeCache struct { - types map[pgtype.OID]Type + types map[uint32]Type mu *sync.Mutex } func newTypeCache() *typeCache { - m := make(map[pgtype.OID]Type, len(defaultKnownTypes)) + m := make(map[uint32]Type, len(defaultKnownTypes)) for oid, typ := range defaultKnownTypes { m[oid] = typ } @@ -23,16 +22,16 @@ func newTypeCache() *typeCache { } // getOIDs returns the cached OIDS (with the type) and uncached OIDs. -func (tc *typeCache) getOIDs(oids ...uint32) (map[pgtype.OID]Type, map[pgtype.OID]struct{}) { - cachedTypes := make(map[pgtype.OID]Type, len(oids)) - uncachedTypes := make(map[pgtype.OID]struct{}, len(oids)) +func (tc *typeCache) getOIDs(oids ...uint32) (map[uint32]Type, map[uint32]struct{}) { + cachedTypes := make(map[uint32]Type, len(oids)) + uncachedTypes := make(map[uint32]struct{}, len(oids)) tc.mu.Lock() defer tc.mu.Unlock() for _, oid := range oids { - if t, ok := tc.types[pgtype.OID(oid)]; ok { - cachedTypes[pgtype.OID(oid)] = t + if t, ok := tc.types[uint32(oid)]; ok { + cachedTypes[uint32(oid)] = t } else { - uncachedTypes[pgtype.OID(oid)] = struct{}{} + uncachedTypes[uint32(oid)] = struct{}{} } } return cachedTypes, uncachedTypes @@ -40,7 +39,7 @@ func (tc *typeCache) getOIDs(oids ...uint32) (map[pgtype.OID]Type, map[pgtype.OI func (tc *typeCache) getOID(oid uint32) (Type, bool) { tc.mu.Lock() - typ, ok := tc.types[pgtype.OID(oid)] + typ, ok := tc.types[uint32(oid)] tc.mu.Unlock() return typ, ok } diff --git a/internal/pg/type_fetcher.go b/internal/pg/type_fetcher.go index c0ad0d6c..8eae7034 100644 --- a/internal/pg/type_fetcher.go +++ b/internal/pg/type_fetcher.go @@ -3,8 +3,7 @@ package pg import ( "context" "fmt" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "time" ) @@ -24,7 +23,7 @@ func NewTypeFetcher(conn *pgx.Conn) *TypeFetcher { // FindTypesByOIDs returns a map of a type OID to the Type description. The // returned map contains every unique OID in oids (oids may contain duplicates) // unless there's an error. -func (tf *TypeFetcher) FindTypesByOIDs(oids ...uint32) (map[pgtype.OID]Type, error) { +func (tf *TypeFetcher) FindTypesByOIDs(oids ...uint32) (map[uint32]Type, error) { if types, uncached := tf.cache.getOIDs(oids...); len(uncached) == 0 { return types, nil } @@ -96,7 +95,7 @@ func (tf *TypeFetcher) FindTypesByOIDs(oids ...uint32) (map[pgtype.OID]Type, err return types, nil } -func (tf *TypeFetcher) findEnumTypes(ctx context.Context, uncached map[pgtype.OID]struct{}) ([]EnumType, error) { +func (tf *TypeFetcher) findEnumTypes(ctx context.Context, uncached map[uint32]struct{}) ([]EnumType, error) { oids := oidKeys(uncached) rows, err := tf.querier.FindEnumTypes(ctx, oids) if err != nil { @@ -104,9 +103,9 @@ func (tf *TypeFetcher) findEnumTypes(ctx context.Context, uncached map[pgtype.OI } types := make([]EnumType, len(rows)) for i, enum := range rows { - childOIDs := make([]pgtype.OID, len(enum.ChildOIDs)) + childOIDs := make([]uint32, len(enum.ChildOIDs)) for i, oidUint32 := range enum.ChildOIDs { - childOIDs[i] = pgtype.OID(oidUint32) + childOIDs[i] = uint32(oidUint32) } types[i] = EnumType{ ID: enum.OID, @@ -119,14 +118,14 @@ func (tf *TypeFetcher) findEnumTypes(ctx context.Context, uncached map[pgtype.OI return types, nil } -func (tf *TypeFetcher) findCompositeTypes(ctx context.Context, uncached map[pgtype.OID]struct{}) ([]CompositeType, error) { +func (tf *TypeFetcher) findCompositeTypes(ctx context.Context, uncached map[uint32]struct{}) ([]CompositeType, error) { oids := oidKeys(uncached) rows, err := tf.querier.FindCompositeTypes(ctx, oids) if err != nil { return nil, fmt.Errorf("find composite types: %w", err) } // Record all composite types to fake a topological sort by repeated iteration. - allComposites := make(map[pgtype.OID]struct{}, len(rows)) + allComposites := make(map[uint32]struct{}, len(rows)) for _, row := range rows { allComposites[row.TableTypeOID] = struct{}{} } @@ -148,13 +147,13 @@ func (tf *TypeFetcher) findCompositeTypes(ctx context.Context, uncached map[pgty // We might resolve this type in a future pass like findArrayTypes. At // the end, we'll attempt to to replace the placeholder with the // resolved type. - colTypes[i] = placeholderType{ID: pgtype.OID(colOID)} + colTypes[i] = placeholderType{ID: uint32(colOID)} colNames[i] = row.ColNames[i] } } typ := CompositeType{ ID: row.TableTypeOID, - Name: row.TableName.String, + Name: row.TableName, ColumnNames: colNames, ColumnTypes: colTypes, } @@ -164,7 +163,7 @@ func (tf *TypeFetcher) findCompositeTypes(ctx context.Context, uncached map[pgty return types, nil } -func (tf *TypeFetcher) findUnknownTypes(ctx context.Context, uncached map[pgtype.OID]struct{}) ([]UnknownType, error) { +func (tf *TypeFetcher) findUnknownTypes(ctx context.Context, uncached map[uint32]struct{}) ([]UnknownType, error) { oids := oidKeys(uncached) rows, err := tf.querier.FindOIDNames(ctx, oids) if err != nil { @@ -174,14 +173,14 @@ func (tf *TypeFetcher) findUnknownTypes(ctx context.Context, uncached map[pgtype for i, row := range rows { types[i] = UnknownType{ ID: row.OID, - Name: row.Name.String, - PgKind: TypeKind(row.Kind.Int), + Name: row.Name, + PgKind: TypeKind(row.Kind), } } return types, nil } -func (tf *TypeFetcher) findArrayTypes(ctx context.Context, uncached map[pgtype.OID]struct{}) ([]ArrayType, error) { +func (tf *TypeFetcher) findArrayTypes(ctx context.Context, uncached map[uint32]struct{}) ([]ArrayType, error) { oids := oidKeys(uncached) rows, err := tf.querier.FindArrayTypes(ctx, oids) if err != nil { @@ -204,7 +203,7 @@ func (tf *TypeFetcher) findArrayTypes(ctx context.Context, uncached map[pgtype.O // resolvePlaceholderTypes resolves all placeholder types or errors if we can't // resolve a placeholderType using all known types. -func (tf *TypeFetcher) resolvePlaceholderTypes(knownTypes map[pgtype.OID]Type) error { +func (tf *TypeFetcher) resolvePlaceholderTypes(knownTypes map[uint32]Type) error { // resolveType walks down type, replacing placeholderType with a known type. var resolveType func(typ Type) (Type, error) resolveType = func(typ Type) (Type, error) { @@ -246,7 +245,7 @@ func (tf *TypeFetcher) resolvePlaceholderTypes(knownTypes map[pgtype.OID]Type) e return nil } -func oidKeys(os map[pgtype.OID]struct{}) []uint32 { +func oidKeys(os map[uint32]struct{}) []uint32 { oids := make([]uint32, 0, len(os)) for oid := range os { oids = append(oids, uint32(oid)) diff --git a/internal/pg/type_fetcher_test.go b/internal/pg/type_fetcher_test.go index 08e7456f..43fc0077 100644 --- a/internal/pg/type_fetcher_test.go +++ b/internal/pg/type_fetcher_test.go @@ -4,7 +4,6 @@ import ( "context" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/jackc/pgtype" "github.com/jschaf/pggen/internal/pg/pgoid" "github.com/jschaf/pggen/internal/pgtest" "github.com/jschaf/pggen/internal/texts" @@ -248,7 +247,7 @@ func TestNewTypeFetcher(t *testing.T) { } // Get the OID by name if fetchOID was a string, or just return the OID. -func findOIDVal(t *testing.T, fetchOID interface{}, querier *DBQuerier) pgtype.OID { +func findOIDVal(t *testing.T, fetchOID interface{}, querier *DBQuerier) uint32 { switch rawOID := fetchOID.(type) { case string: oid, err := querier.FindOIDByName(context.Background(), rawOID) @@ -256,10 +255,10 @@ func findOIDVal(t *testing.T, fetchOID interface{}, querier *DBQuerier) pgtype.O t.Fatalf("find oid by name %s: %s", rawOID, err) } return oid - case pgtype.OID: + case uint32: return rawOID case int: - return pgtype.OID(rawOID) + return uint32(rawOID) default: t.Fatalf("unhandled oid test value type %T: %v", rawOID, rawOID) return 0 diff --git a/internal/pg/types.go b/internal/pg/types.go index b5b07af4..b662df8a 100644 --- a/internal/pg/types.go +++ b/internal/pg/types.go @@ -1,15 +1,14 @@ package pg import ( - "github.com/jackc/pgtype" "github.com/jschaf/pggen/internal/pg/pgoid" "strconv" ) // Type is a Postgres type. type Type interface { - OID() pgtype.OID // pg_type.oid: row identifier - String() string // pg_type.typname: data type name + OID() uint32 // pg_type.oid: row identifier + String() string // pg_type.typname: data type name Kind() TypeKind } @@ -49,8 +48,8 @@ type ( // BaseType is a fundamental Postgres type like text and bool. // https://www.postgresql.org/docs/13/catalog-pg-type.html BaseType struct { - ID pgtype.OID // pg_type.oid: row identifier - Name string // pg_type.typname: data type name + ID uint32 // pg_type.oid: row identifier + Name string // pg_type.typname: data type name } // VoidType is an empty type. A void type doesn't appear in output, but it's @@ -60,7 +59,7 @@ type ( // ArrayType is an array type where pg_type.typelem != 0 and the name begins // with an underscore. ArrayType struct { - ID pgtype.OID // pg_type.oid: row identifier + ID uint32 // pg_type.oid: row identifier // The name of the type, like _int4. Array types in Postgres typically // begin with an underscore. From pg_type.typname. Name string @@ -69,7 +68,7 @@ type ( } EnumType struct { - ID pgtype.OID // pg_type.oid: row identifier + ID uint32 // pg_type.oid: row identifier // The name of the enum, like 'device_type' in: // CREATE TYPE device_type AS ENUM ('foo'); // From pg_type.typname. @@ -82,26 +81,26 @@ type ( // values is that they be correctly ordered and unique within each enum // type. Orders []float32 - ChildOIDs []pgtype.OID + ChildOIDs []uint32 } // DomainType is a user-create domain type. DomainType struct { - ID pgtype.OID // pg_type.oid: row identifier - Name string // pg_type.typname: data type name - IsNotNull bool // pg_type.typnotnull: domains only, not null constraint for domains - HasDefault bool // pg_type.typdefault: domains only, if there's a default value - BaseType BaseType // pg_type.typbasetype: domains only, the base type - Dimensions int // pg_type.typndims: domains on array type only, 0 otherwise, number of array dimensions + ID uint32 // pg_type.oid: row identifier + Name string // pg_type.typname: data type name + IsNotNull bool // pg_type.typnotnull: domains only, not null constraint for domains + HasDefault bool // pg_type.typdefault: domains only, if there's a default value + BaseType BaseType // pg_type.typbasetype: domains only, the base type + Dimensions int // pg_type.typndims: domains on array type only, 0 otherwise, number of array dimensions } // CompositeType is a type containing multiple columns and is represented as // a class. https://www.postgresql.org/docs/13/catalog-pg-class.html CompositeType struct { - ID pgtype.OID // pg_class.oid: row identifier - Name string // pg_class.relname: name of the composite type - ColumnNames []string // pg_attribute.attname: names of the column, in order - ColumnTypes []Type // pg_attribute JOIN pg_type: information about columns of the composite type + ID uint32 // pg_class.oid: row identifier + Name string // pg_class.relname: name of the composite type + ColumnNames []string // pg_attribute.attname: names of the column, in order + ColumnTypes []Type // pg_attribute JOIN pg_type: information about columns of the composite type } // UnknownType is a Postgres type that's not a well-known type in @@ -109,8 +108,8 @@ type ( // generator might be able to resolve this type from a user-provided mapping // like --go-type my_int=int. UnknownType struct { - ID pgtype.OID // pg_type.oid: row identifier - Name string // pg_type.typname: data type name + ID uint32 // pg_type.oid: row identifier + Name string // pg_type.typname: data type name PgKind TypeKind } @@ -120,38 +119,38 @@ type ( // requires two passes for cases like when a composite type has a child type // that's an array. placeholderType struct { - ID pgtype.OID // pg_type.oid: row identifier + ID uint32 // pg_type.oid: row identifier } ) -func (b BaseType) OID() pgtype.OID { return b.ID } -func (b BaseType) String() string { return b.Name } -func (b BaseType) Kind() TypeKind { return KindBaseType } +func (b BaseType) OID() uint32 { return b.ID } +func (b BaseType) String() string { return b.Name } +func (b BaseType) Kind() TypeKind { return KindBaseType } -func (b VoidType) OID() pgtype.OID { return pgoid.Void } -func (b VoidType) String() string { return "void" } -func (b VoidType) Kind() TypeKind { return KindPseudoType } +func (b VoidType) OID() uint32 { return pgoid.Void } +func (b VoidType) String() string { return "void" } +func (b VoidType) Kind() TypeKind { return KindPseudoType } -func (b ArrayType) OID() pgtype.OID { return b.ID } -func (b ArrayType) String() string { return b.Name } -func (b ArrayType) Kind() TypeKind { return KindBaseType } +func (b ArrayType) OID() uint32 { return b.ID } +func (b ArrayType) String() string { return b.Name } +func (b ArrayType) Kind() TypeKind { return KindBaseType } -func (e EnumType) OID() pgtype.OID { return e.ID } -func (e EnumType) String() string { return e.Name } -func (e EnumType) Kind() TypeKind { return KindEnumType } +func (e EnumType) OID() uint32 { return e.ID } +func (e EnumType) String() string { return e.Name } +func (e EnumType) Kind() TypeKind { return KindEnumType } -func (e DomainType) OID() pgtype.OID { return e.ID } -func (e DomainType) String() string { return e.Name } -func (e DomainType) Kind() TypeKind { return KindDomainType } +func (e DomainType) OID() uint32 { return e.ID } +func (e DomainType) String() string { return e.Name } +func (e DomainType) Kind() TypeKind { return KindDomainType } -func (e CompositeType) OID() pgtype.OID { return e.ID } -func (e CompositeType) String() string { return e.Name } -func (e CompositeType) Kind() TypeKind { return KindCompositeType } +func (e CompositeType) OID() uint32 { return e.ID } +func (e CompositeType) String() string { return e.Name } +func (e CompositeType) Kind() TypeKind { return KindCompositeType } -func (e UnknownType) OID() pgtype.OID { return e.ID } -func (e UnknownType) String() string { return e.Name } -func (e UnknownType) Kind() TypeKind { return e.PgKind } +func (e UnknownType) OID() uint32 { return e.ID } +func (e UnknownType) String() string { return e.Name } +func (e UnknownType) Kind() TypeKind { return e.PgKind } -func (p placeholderType) OID() pgtype.OID { return p.ID } -func (p placeholderType) String() string { return "placeholder-" + strconv.Itoa(int(p.ID)) } -func (p placeholderType) Kind() TypeKind { return kindPlaceholderType } +func (p placeholderType) OID() uint32 { return p.ID } +func (p placeholderType) String() string { return "placeholder-" + strconv.Itoa(int(p.ID)) } +func (p placeholderType) Kind() TypeKind { return kindPlaceholderType } diff --git a/internal/pgdocker/pgdocker.go b/internal/pgdocker/pgdocker.go index 1f4a1ce6..0d629932 100644 --- a/internal/pgdocker/pgdocker.go +++ b/internal/pgdocker/pgdocker.go @@ -12,7 +12,7 @@ import ( "github.com/docker/docker/api/types/container" dockerClient "github.com/docker/docker/client" "github.com/docker/go-connections/nat" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/errs" "github.com/jschaf/pggen/internal/ports" "io" diff --git a/internal/pginfer/pginfer.go b/internal/pginfer/pginfer.go index 82de7f24..bcb46d70 100644 --- a/internal/pginfer/pginfer.go +++ b/internal/pginfer/pginfer.go @@ -3,13 +3,11 @@ package pginfer import ( "context" "fmt" - "github.com/jackc/pgconn" + "github.com/jackc/pgx/v5/pgconn" "strings" "time" - "github.com/jackc/pgproto3/v2" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/ast" "github.com/jschaf/pggen/internal/pg" ) @@ -157,7 +155,7 @@ func (inf *Inferrer) prepareTypes(query *ast.SourceQuery) (_a []InputParam, _ [] return nil, nil, fmt.Errorf("fetch oid types: %w", err) } for i, oid := range stmtDesc.ParamOIDs { - inputType, ok := types[pgtype.OID(oid)] + inputType, ok := types[uint32(oid)] if !ok { return nil, nil, fmt.Errorf("no postgres type name found for parameter %s with oid %d", query.ParamNames[i], oid) } @@ -187,7 +185,7 @@ func (inf *Inferrer) prepareTypes(query *ast.SourceQuery) (_a []InputParam, _ [] // Create output columns var outputColumns []OutputColumn for i, desc := range stmtDesc.Fields { - pgType, ok := outputTypes[pgtype.OID(desc.DataTypeOID)] + pgType, ok := outputTypes[uint32(desc.DataTypeOID)] if !ok { return nil, nil, fmt.Errorf("no postgrestype name found for column %s with oid %d", string(desc.Name), desc.DataTypeOID) } @@ -202,7 +200,7 @@ func (inf *Inferrer) prepareTypes(query *ast.SourceQuery) (_a []InputParam, _ [] // inferOutputNullability infers which of the output columns produced by the // query and described by descs can be null. -func (inf *Inferrer) inferOutputNullability(query *ast.SourceQuery, descs []pgproto3.FieldDescription) ([]bool, error) { +func (inf *Inferrer) inferOutputNullability(query *ast.SourceQuery, descs []pgconn.FieldDescription) ([]bool, error) { if len(descs) == 0 { return nil, nil } @@ -215,7 +213,7 @@ func (inf *Inferrer) inferOutputNullability(query *ast.SourceQuery, descs []pgpr for i, desc := range descs { if desc.TableOID > 0 { columnKeys[i] = pg.ColumnKey{ - TableOID: pgtype.OID(desc.TableOID), + TableOID: uint32(desc.TableOID), Number: desc.TableAttributeNumber, } } diff --git a/internal/pgplan/pgplan.go b/internal/pgplan/pgplan.go index 18294505..d7e69db3 100644 --- a/internal/pgplan/pgplan.go +++ b/internal/pgplan/pgplan.go @@ -3,7 +3,7 @@ package pgplan import ( "context" "fmt" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "time" ) diff --git a/internal/pgtest/pg_test_db.go b/internal/pgtest/pg_test_db.go index e0905fd0..62c1a986 100644 --- a/internal/pgtest/pg_test_db.go +++ b/internal/pgtest/pg_test_db.go @@ -2,7 +2,7 @@ package pgtest import ( "context" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "math/rand" "os" "strconv" From 4bb40e2b95c380ce9d386688af28a408521121d8 Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Fri, 5 Jan 2024 15:12:55 -0800 Subject: [PATCH 02/16] experiment with codecs --- example/author/query.sql.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/example/author/query.sql.go b/example/author/query.sql.go index 2b892bec..46224828 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgtype" ) // Querier is a typesafe Go interface backed by SQL queries. @@ -80,6 +81,7 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") row := q.conn.QueryRow(ctx, findAuthorByIDSQL, authorID) var item FindAuthorByIDRow + s := scanner[*FindAuthorByIDRow]{&item} if err := row.Scan(s); err != nil { return item, fmt.Errorf("query FindAuthorByID: %w", err) @@ -108,13 +110,32 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu return nil, fmt.Errorf("query FindAuthors: %w", err) } defer rows.Close() + + fds := rows.FieldDescriptions() + + int4Codec := pgtype.Int4Codec{} + textCodec := &pgtype.TextCodec{} + + scan0 := int4Codec.PlanScan(nil, fds[0].DataTypeOID, fds[0].Format, (*int32)(nil)) + scan1 := textCodec.PlanScan(nil, fds[1].DataTypeOID, fds[1].Format, (*string)(nil)) + scan2 := textCodec.PlanScan(nil, fds[2].DataTypeOID, fds[2].Format, (*string)(nil)) + scan3 := textCodec.PlanScan(nil, fds[2].DataTypeOID, fds[3].Format, (*string)(nil)) + items := []FindAuthorsRow{} - s := scanner[*FindAuthorsRow]{} for rows.Next() { + vals := rows.RawValues() var item FindAuthorsRow - s.item = &item - if err := rows.Scan(s); err != nil { - return nil, fmt.Errorf("scan FindAuthors row: %w", err) + if err := scan0.Scan(vals[0], &item.AuthorID); err != nil { + return nil, fmt.Errorf("scan FindAuthors row author_id column: %w", err) + } + if err := scan1.Scan(vals[1], &item.FirstName); err != nil { + return nil, fmt.Errorf("scan FindAuthors row first_name column: %w", err) + } + if err := scan2.Scan(vals[2], &item.LastName); err != nil { + return nil, fmt.Errorf("scan FindAuthors row last_name column: %w", err) + } + if err := scan3.Scan(vals[3], item.Suffix); err != nil { + return nil, fmt.Errorf("scan FindAuthors row suffix column: %w", err) } items = append(items, item) } From 500b5c6be650536cc5b4cb45a13c45002d6d04ca Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Fri, 5 Jan 2024 17:45:35 -0800 Subject: [PATCH 03/16] More experiments with codecs --- example/author/query.sql.go | 86 ++++++++++++----- example/author/query.sql_test.go | 19 ++++ go.mod | 7 -- go.sum | 159 ------------------------------- 4 files changed, 81 insertions(+), 190 deletions(-) diff --git a/example/author/query.sql.go b/example/author/query.sql.go index 46224828..283cfa9a 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -102,6 +102,54 @@ func (r *FindAuthorsRow) scanRow(rows pgx.Rows) error { return rows.Scan(&r.AuthorID, &r.FirstName, &r.LastName, &r.Suffix) } +type scanCacheKey struct { + oid uint32 + format int16 + typeName string +} + +type scanPlan[T any] interface { + Scan([]byte, T) error +} + +var scanCache = map[string]scanPlan[any]{} + +func planScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription) scanPlan[*T] { + var target *T + key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("%T", target)} + // TODO: synchronize + if plan, ok := scanCache[key.typeName]; ok { + return plan.(scanPlan[*T]) + } + plan := codec.PlanScan(nil, fd.DataTypeOID, fd.Format, target) + scanCache[key.typeName] = plan + return plan.(scanPlan[*T]) +} + +type ptrScanner[T any] struct { + basePlan scanPlan[T] +} + +func (s ptrScanner[T]) Scan(src []byte, dst **T) error { + if src == nil { + return nil + } + *dst = new(T) + return s.basePlan.Scan(src, *dst) +} + +func planPtrScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription) scanPlan[**T] { + var target *T + key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("*%T", target)} + if scan, ok := scanCache[key.typeName]; ok { + return scan.(scanPlan[**T]) + } + basePlan := planScan[T](codec, fd) + var ptrPlan scanPlan[**T] = ptrScanner[T]{basePlan: basePlan.(scanPlan[T])} + scanCache[key.typeName] = ptrPlan.(scanPlan[any]) + return ptrPlan +} + // FindAuthors implements Querier.FindAuthors. func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAuthorsRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthors") @@ -112,30 +160,26 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu defer rows.Close() fds := rows.FieldDescriptions() - - int4Codec := pgtype.Int4Codec{} - textCodec := &pgtype.TextCodec{} - - scan0 := int4Codec.PlanScan(nil, fds[0].DataTypeOID, fds[0].Format, (*int32)(nil)) - scan1 := textCodec.PlanScan(nil, fds[1].DataTypeOID, fds[1].Format, (*string)(nil)) - scan2 := textCodec.PlanScan(nil, fds[2].DataTypeOID, fds[2].Format, (*string)(nil)) - scan3 := textCodec.PlanScan(nil, fds[2].DataTypeOID, fds[3].Format, (*string)(nil)) + var plan0 = planScan[int32](pgtype.Int4Codec{}, fds[0]) + var plan1 = planScan[string](pgtype.TextCodec{}, fds[1]) + var plan2 = plan1 + var plan3 = planPtrScan[string](pgtype.TextCodec{}, fds[3]) items := []FindAuthorsRow{} for rows.Next() { vals := rows.RawValues() var item FindAuthorsRow - if err := scan0.Scan(vals[0], &item.AuthorID); err != nil { - return nil, fmt.Errorf("scan FindAuthors row author_id column: %w", err) + if err := plan0.Scan(vals[0], &item.AuthorID); err != nil { + return nil, fmt.Errorf("scan FindAuthors.author_id column: %w", err) } - if err := scan1.Scan(vals[1], &item.FirstName); err != nil { - return nil, fmt.Errorf("scan FindAuthors row first_name column: %w", err) + if err := plan1.Scan(vals[1], &item.FirstName); err != nil { + return nil, fmt.Errorf("scan FindAuthors.first_name column: %w", err) } - if err := scan2.Scan(vals[2], &item.LastName); err != nil { - return nil, fmt.Errorf("scan FindAuthors row last_name column: %w", err) + if err := plan2.Scan(vals[2], &item.LastName); err != nil { + return nil, fmt.Errorf("scan FindAuthors.last_name column: %w", err) } - if err := scan3.Scan(vals[3], item.Suffix); err != nil { - return nil, fmt.Errorf("scan FindAuthors row suffix column: %w", err) + if err := plan3.Scan(vals[3], &item.Suffix); err != nil { + return nil, fmt.Errorf("scan FindAuthors.suffix column: %w", err) } items = append(items, item) } @@ -298,7 +342,7 @@ func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorS return item, nil } -const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS names FROM author WHERE author_id = $1;` +const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS NAMES FROM author WHERE author_id = $1;` // StringAggFirstName implements Querier.StringAggFirstName. func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*string, error) { @@ -311,7 +355,7 @@ func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*st return item, nil } -const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS names FROM author WHERE author_id = $1;` +const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS NAMES FROM author WHERE author_id = $1;` // ArrayAggFirstName implements Querier.ArrayAggFirstName. func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]string, error) { @@ -323,9 +367,3 @@ func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]st } return item, nil } - -type rowScanner interface{ scanRow(rows pgx.Rows) error } - -type scanner[T rowScanner] struct{ item T } - -func (s scanner[T]) ScanRow(rows pgx.Rows) error { return s.item.scanRow(rows) } diff --git a/example/author/query.sql_test.go b/example/author/query.sql_test.go index 55521e45..fc1199e7 100644 --- a/example/author/query.sql_test.go +++ b/example/author/query.sql_test.go @@ -63,6 +63,25 @@ func TestNewQuerier_FindAuthors(t *testing.T) { assert.Equal(t, want, authors) }) + t.Run("FindAuthors - 1 row suffix - bill", func(t *testing.T) { + insRow, err := q.InsertAuthorSuffix(context.Background(), InsertAuthorSuffixParams{ + FirstName: "bill", + LastName: "clinton", + Suffix: "jr", + }) + authors, err := q.FindAuthors(context.Background(), "bill") + require.NoError(t, err) + want := []FindAuthorsRow{ + { + AuthorID: insRow.AuthorID, + FirstName: "bill", + LastName: "clinton", + Suffix: ptrs.String("jr"), + }, + } + assert.Equal(t, want, authors) + }) + t.Run("FindAuthors - 2 rows - george", func(t *testing.T) { authors, err := q.FindAuthors(context.Background(), "george") require.NoError(t, err) diff --git a/go.mod b/go.mod index db08d690..af5672d9 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,8 @@ require ( github.com/docker/docker v24.0.7+incompatible github.com/docker/go-connections v0.4.0 github.com/google/go-cmp v0.6.0 - github.com/jackc/pgconn v1.14.1 - github.com/jackc/pgproto3/v2 v2.3.2 - github.com/jackc/pgtype v1.14.0 - github.com/jackc/pgx/v4 v4.18.1 github.com/jackc/pgx/v5 v5.5.1 github.com/peterbourgon/ff/v3 v3.4.0 - github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.8.4 golang.org/x/mod v0.11.0 ) @@ -24,8 +19,6 @@ require ( github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/go.sum b/go.sum index 28462699..5bd0bf93 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,9 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,95 +17,29 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= -github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4= -github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= -github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= -github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -128,139 +55,56 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -270,12 +114,9 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= From 8e58ee07478620e863ba20506fb978877d24a060 Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Wed, 24 Jan 2024 22:16:37 -0800 Subject: [PATCH 04/16] Simplify codec --- example/author/query.sql.go | 152 ++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 67 deletions(-) diff --git a/example/author/query.sql.go b/example/author/query.sql.go index 283cfa9a..f84070f3 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -8,6 +8,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" + "sync" ) // Querier is a typesafe Go interface backed by SQL queries. @@ -79,13 +80,14 @@ func (r *FindAuthorByIDRow) scanRow(rows pgx.Rows) error { // FindAuthorByID implements Querier.FindAuthorByID. func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAuthorByIDRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") - row := q.conn.QueryRow(ctx, findAuthorByIDSQL, authorID) + //row := q.conn.QueryRow(ctx, findAuthorByIDSQL, authorID) var item FindAuthorByIDRow - s := scanner[*FindAuthorByIDRow]{&item} - if err := row.Scan(s); err != nil { - return item, fmt.Errorf("query FindAuthorByID: %w", err) - } + //s := scanner[*FindAuthorByIDRow]{&item} + //if err := row.Scan(s); err != nil { + // return item, fmt.Errorf("query FindAuthorByID: %w", err) + //} + //return item, nil return item, nil } @@ -102,54 +104,6 @@ func (r *FindAuthorsRow) scanRow(rows pgx.Rows) error { return rows.Scan(&r.AuthorID, &r.FirstName, &r.LastName, &r.Suffix) } -type scanCacheKey struct { - oid uint32 - format int16 - typeName string -} - -type scanPlan[T any] interface { - Scan([]byte, T) error -} - -var scanCache = map[string]scanPlan[any]{} - -func planScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription) scanPlan[*T] { - var target *T - key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("%T", target)} - // TODO: synchronize - if plan, ok := scanCache[key.typeName]; ok { - return plan.(scanPlan[*T]) - } - plan := codec.PlanScan(nil, fd.DataTypeOID, fd.Format, target) - scanCache[key.typeName] = plan - return plan.(scanPlan[*T]) -} - -type ptrScanner[T any] struct { - basePlan scanPlan[T] -} - -func (s ptrScanner[T]) Scan(src []byte, dst **T) error { - if src == nil { - return nil - } - *dst = new(T) - return s.basePlan.Scan(src, *dst) -} - -func planPtrScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription) scanPlan[**T] { - var target *T - key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("*%T", target)} - if scan, ok := scanCache[key.typeName]; ok { - return scan.(scanPlan[**T]) - } - basePlan := planScan[T](codec, fd) - var ptrPlan scanPlan[**T] = ptrScanner[T]{basePlan: basePlan.(scanPlan[T])} - scanCache[key.typeName] = ptrPlan.(scanPlan[any]) - return ptrPlan -} - // FindAuthors implements Querier.FindAuthors. func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAuthorsRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthors") @@ -160,10 +114,10 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu defer rows.Close() fds := rows.FieldDescriptions() - var plan0 = planScan[int32](pgtype.Int4Codec{}, fds[0]) - var plan1 = planScan[string](pgtype.TextCodec{}, fds[1]) - var plan2 = plan1 - var plan3 = planPtrScan[string](pgtype.TextCodec{}, fds[3]) + plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) + plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) + plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) + plan3 := planPtrScan(pgtype.TextCodec{}, fds[3], (*string)(nil)) items := []FindAuthorsRow{} for rows.Next() { @@ -208,14 +162,21 @@ func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]Find return nil, fmt.Errorf("query FindAuthorNames: %w", err) } defer rows.Close() + + //fds := rows.FieldDescriptions() + //plan0 := planPtrScan[string](pgtype.TextCodec{}, fds[0]) + //plan1 := planPtrScan[string](pgtype.TextCodec{}, fds[1]) + items := []FindAuthorNamesRow{} - s := scanner[*FindAuthorNamesRow]{} for rows.Next() { + //vals := rows.RawValues() var item FindAuthorNamesRow - s.item = &item - if err := rows.Scan(s); err != nil { - return nil, fmt.Errorf("scan FindAuthorNames row: %w", err) - } + //if err := plan0.Scan(vals[0], &item.FirstName); err != nil { + // return nil, fmt.Errorf("scan FindAuthors.first_name column: %w", err) + //} + //if err := plan1.Scan(vals[1], &item.FirstName); err != nil { + // return nil, fmt.Errorf("scan FindAuthors.last_name column: %w", err) + //} items = append(items, item) } if err := rows.Err(); err != nil { @@ -333,13 +294,15 @@ func (r *InsertAuthorSuffixRow) scanRow(rows pgx.Rows) error { // InsertAuthorSuffix implements Querier.InsertAuthorSuffix. func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorSuffixParams) (InsertAuthorSuffixRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthorSuffix") - row := q.conn.QueryRow(ctx, insertAuthorSuffixSQL, params.FirstName, params.LastName, params.Suffix) + //row := q.conn.QueryRow(ctx, insertAuthorSuffixSQL, params.FirstName, params.LastName, params.Suffix) var item InsertAuthorSuffixRow - s := scanner[*InsertAuthorSuffixRow]{&item} - if err := row.Scan(s); err != nil { - return item, fmt.Errorf("query InsertAuthorSuffix: %w", err) - } return item, nil + // + //s := scanner[*InsertAuthorSuffixRow]{&item} + //if err := row.Scan(s); err != nil { + // return item, fmt.Errorf("query InsertAuthorSuffix: %w", err) + //} + //return item, nil } const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS NAMES FROM author WHERE author_id = $1;` @@ -367,3 +330,58 @@ func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]st } return item, nil } + +type scanCacheKey struct { + oid uint32 + format int16 + typeName string +} + +var ( + plans = make(map[scanCacheKey]pgtype.ScanPlan, 16) + plansMu sync.RWMutex +) + +func planScan(codec pgtype.Codec, fd pgconn.FieldDescription, target any) pgtype.ScanPlan { + key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("%T", target)} + plansMu.RLock() + plan := plans[key] + plansMu.RUnlock() + if plan != nil { + return plan + } + plan = codec.PlanScan(nil, fd.DataTypeOID, fd.Format, target) + plansMu.Lock() + plans[key] = plan + plansMu.Unlock() + return plan +} + +type ptrScanner[T any] struct { + basePlan pgtype.ScanPlan +} + +func (s ptrScanner[T]) Scan(src []byte, dst any) error { + if src == nil { + return nil + } + d := dst.(**T) + *d = new(T) + return s.basePlan.Scan(src, **d) +} + +func planPtrScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription, target *T) pgtype.ScanPlan { + key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("*%T", target)} + plansMu.RLock() + plan := plans[key] + plansMu.RUnlock() + if plan != nil { + return plan + } + basePlan := planScan(codec, fd, target) + ptrPlan := ptrScanner[T]{basePlan} + plansMu.Lock() + plans[key] = plan + plansMu.Unlock() + return ptrPlan +} From 1b29c21c3998c76af787ed3cd089593aaaaa4f68 Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Wed, 24 Jan 2024 23:21:16 -0800 Subject: [PATCH 05/16] fix most author example tests --- example/author/query.sql.go | 217 ++++++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 85 deletions(-) diff --git a/example/author/query.sql.go b/example/author/query.sql.go index f84070f3..6dd22144 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -73,22 +73,37 @@ type FindAuthorByIDRow struct { Suffix *string `json:"suffix"` } -func (r *FindAuthorByIDRow) scanRow(rows pgx.Rows) error { - return rows.Scan(&r.AuthorID, &r.FirstName, &r.LastName, &r.Suffix) -} - // FindAuthorByID implements Querier.FindAuthorByID. func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAuthorByIDRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") - //row := q.conn.QueryRow(ctx, findAuthorByIDSQL, authorID) - var item FindAuthorByIDRow - - //s := scanner[*FindAuthorByIDRow]{&item} - //if err := row.Scan(s); err != nil { - // return item, fmt.Errorf("query FindAuthorByID: %w", err) - //} - //return item, nil - return item, nil + rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) + if err != nil { + return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) + } + + fds := rows.FieldDescriptions() + plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) + plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) + plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) + plan3 := planPtrScan(pgtype.TextCodec{}, fds[3], (*string)(nil)) + + return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (FindAuthorByIDRow, error) { + vals := row.RawValues() + var item FindAuthorByIDRow + if err := plan0.Scan(vals[0], &item.AuthorID); err != nil { + return item, fmt.Errorf("scan FindAuthors.author_id column: %w", err) + } + if err := plan1.Scan(vals[1], &item.FirstName); err != nil { + return item, fmt.Errorf("scan FindAuthors.first_name column: %w", err) + } + if err := plan2.Scan(vals[2], &item.LastName); err != nil { + return item, fmt.Errorf("scan FindAuthors.last_name column: %w", err) + } + if err := plan3.Scan(vals[3], &item.Suffix); err != nil { + return item, fmt.Errorf("scan FindAuthors.suffix column: %w", err) + } + return item, nil + }) } const findAuthorsSQL = `SELECT * FROM author WHERE first_name = $1;` @@ -100,10 +115,6 @@ type FindAuthorsRow struct { Suffix *string `json:"suffix"` } -func (r *FindAuthorsRow) scanRow(rows pgx.Rows) error { - return rows.Scan(&r.AuthorID, &r.FirstName, &r.LastName, &r.Suffix) -} - // FindAuthors implements Querier.FindAuthors. func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAuthorsRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthors") @@ -111,7 +122,6 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu if err != nil { return nil, fmt.Errorf("query FindAuthors: %w", err) } - defer rows.Close() fds := rows.FieldDescriptions() plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) @@ -119,28 +129,23 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) plan3 := planPtrScan(pgtype.TextCodec{}, fds[3], (*string)(nil)) - items := []FindAuthorsRow{} - for rows.Next() { - vals := rows.RawValues() + return pgx.CollectRows(rows, func(row pgx.CollectableRow) (FindAuthorsRow, error) { + vals := row.RawValues() var item FindAuthorsRow if err := plan0.Scan(vals[0], &item.AuthorID); err != nil { - return nil, fmt.Errorf("scan FindAuthors.author_id column: %w", err) + return item, fmt.Errorf("scan FindAuthors.author_id column: %w", err) } if err := plan1.Scan(vals[1], &item.FirstName); err != nil { - return nil, fmt.Errorf("scan FindAuthors.first_name column: %w", err) + return item, fmt.Errorf("scan FindAuthors.first_name column: %w", err) } if err := plan2.Scan(vals[2], &item.LastName); err != nil { - return nil, fmt.Errorf("scan FindAuthors.last_name column: %w", err) + return item, fmt.Errorf("scan FindAuthors.last_name column: %w", err) } if err := plan3.Scan(vals[3], &item.Suffix); err != nil { - return nil, fmt.Errorf("scan FindAuthors.suffix column: %w", err) + return item, fmt.Errorf("scan FindAuthors.suffix column: %w", err) } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindAuthors rows: %w", err) - } - return items, err + return item, nil + }) } const findAuthorNamesSQL = `SELECT first_name, last_name FROM author ORDER BY author_id = $1;` @@ -161,28 +166,22 @@ func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]Find if err != nil { return nil, fmt.Errorf("query FindAuthorNames: %w", err) } - defer rows.Close() - //fds := rows.FieldDescriptions() - //plan0 := planPtrScan[string](pgtype.TextCodec{}, fds[0]) - //plan1 := planPtrScan[string](pgtype.TextCodec{}, fds[1]) + fds := rows.FieldDescriptions() + plan0 := planScan(pgtype.TextCodec{}, fds[0], (*string)(nil)) + plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) - items := []FindAuthorNamesRow{} - for rows.Next() { - //vals := rows.RawValues() + return pgx.CollectRows(rows, func(row pgx.CollectableRow) (FindAuthorNamesRow, error) { + vals := row.RawValues() var item FindAuthorNamesRow - //if err := plan0.Scan(vals[0], &item.FirstName); err != nil { - // return nil, fmt.Errorf("scan FindAuthors.first_name column: %w", err) - //} - //if err := plan1.Scan(vals[1], &item.FirstName); err != nil { - // return nil, fmt.Errorf("scan FindAuthors.last_name column: %w", err) - //} - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindAuthorNames rows: %w", err) - } - return items, err + if err := plan0.Scan(vals[0], &item.FirstName); err != nil { + return item, fmt.Errorf("scan FindAuthors.first_name column: %w", err) + } + if err := plan1.Scan(vals[1], &item.LastName); err != nil { + return item, fmt.Errorf("scan FindAuthors.last_name column: %w", err) + } + return item, nil + }) } const findFirstNamesSQL = `SELECT first_name FROM author ORDER BY author_id = $1;` @@ -194,19 +193,18 @@ func (q *DBQuerier) FindFirstNames(ctx context.Context, authorID int32) ([]*stri if err != nil { return nil, fmt.Errorf("query FindFirstNames: %w", err) } - defer rows.Close() - items := []*string{} - for rows.Next() { + + fds := rows.FieldDescriptions() + plan0 := planScan(pgtype.TextCodec{}, fds[0], (*string)(nil)) + + return pgx.CollectRows(rows, func(row pgx.CollectableRow) (*string, error) { + vals := row.RawValues() var item string - if err := rows.Scan(&item); err != nil { + if err := plan0.Scan(vals[0], &item); err != nil { return nil, fmt.Errorf("scan FindFirstNames row: %w", err) } - items = append(items, &item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindFirstNames rows: %w", err) - } - return items, err + return &item, nil + }) } const deleteAuthorsSQL = `DELETE FROM author WHERE first_name = 'joe';` @@ -262,12 +260,22 @@ RETURNING author_id;` // InsertAuthor implements Querier.InsertAuthor. func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName string) (int32, error) { ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthor") - row := q.conn.QueryRow(ctx, insertAuthorSQL, firstName, lastName) - var item int32 - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query InsertAuthor: %w", err) + rows, err := q.conn.Query(ctx, insertAuthorSQL, firstName, lastName) + if err != nil { + return 0, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + fds := rows.FieldDescriptions() + plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) + + return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (int32, error) { + vals := row.RawValues() + var item int32 + if err := plan0.Scan(vals[0], &item); err != nil { + return item, fmt.Errorf("scan InsertAuthor: %w", err) + } + return item, nil + }) } const insertAuthorSuffixSQL = `INSERT INTO author (first_name, last_name, suffix) @@ -294,15 +302,34 @@ func (r *InsertAuthorSuffixRow) scanRow(rows pgx.Rows) error { // InsertAuthorSuffix implements Querier.InsertAuthorSuffix. func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorSuffixParams) (InsertAuthorSuffixRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthorSuffix") - //row := q.conn.QueryRow(ctx, insertAuthorSuffixSQL, params.FirstName, params.LastName, params.Suffix) - var item InsertAuthorSuffixRow - return item, nil - // - //s := scanner[*InsertAuthorSuffixRow]{&item} - //if err := row.Scan(s); err != nil { - // return item, fmt.Errorf("query InsertAuthorSuffix: %w", err) - //} - //return item, nil + rows, err := q.conn.Query(ctx, insertAuthorSuffixSQL, params.FirstName, params.LastName, params.Suffix) + if err != nil { + return InsertAuthorSuffixRow{}, fmt.Errorf("query InsertAuthorSuffix: %w", err) + } + + fds := rows.FieldDescriptions() + plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) + plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) + plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) + plan3 := planPtrScan(pgtype.TextCodec{}, fds[3], (*string)(nil)) + + return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (InsertAuthorSuffixRow, error) { + vals := row.RawValues() + var item InsertAuthorSuffixRow + if err := plan0.Scan(vals[0], &item.AuthorID); err != nil { + return item, fmt.Errorf("scan InsertAuthorSuffixRow.author_id column: %w", err) + } + if err := plan1.Scan(vals[1], &item.FirstName); err != nil { + return item, fmt.Errorf("scan InsertAuthorSuffixRow.first_name column: %w", err) + } + if err := plan2.Scan(vals[2], &item.LastName); err != nil { + return item, fmt.Errorf("scan InsertAuthorSuffixRow.last_name column: %w", err) + } + if err := plan3.Scan(vals[3], &item.Suffix); err != nil { + return item, fmt.Errorf("scan InsertAuthorSuffixRow.suffix column: %w", err) + } + return item, nil + }) } const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS NAMES FROM author WHERE author_id = $1;` @@ -310,12 +337,22 @@ const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS NAMES FROM // StringAggFirstName implements Querier.StringAggFirstName. func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*string, error) { ctx = context.WithValue(ctx, "pggen_query_name", "StringAggFirstName") - row := q.conn.QueryRow(ctx, stringAggFirstNameSQL, authorID) - var item *string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query StringAggFirstName: %w", err) + rows, err := q.conn.Query(ctx, stringAggFirstNameSQL, authorID) + if err != nil { + return nil, fmt.Errorf("query StringAggFirstName: %w", err) } - return item, nil + + fds := rows.FieldDescriptions() + plan0 := planPtrScan(pgtype.TextCodec{}, fds[0], (*string)(nil)) + + return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (*string, error) { + vals := row.RawValues() + var item *string + if err := plan0.Scan(vals[0], &item); err != nil { + return item, fmt.Errorf("scan StringAggFirstName column: %w", err) + } + return item, nil + }) } const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS NAMES FROM author WHERE author_id = $1;` @@ -323,12 +360,22 @@ const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS NAMES FROM author // ArrayAggFirstName implements Querier.ArrayAggFirstName. func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]string, error) { ctx = context.WithValue(ctx, "pggen_query_name", "ArrayAggFirstName") - row := q.conn.QueryRow(ctx, arrayAggFirstNameSQL, authorID) - item := []string{} - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query ArrayAggFirstName: %w", err) + rows, err := q.conn.Query(ctx, arrayAggFirstNameSQL, authorID) + if err != nil { + return nil, fmt.Errorf("query ArrayAggFirstName: %w", err) } - return item, nil + + fds := rows.FieldDescriptions() + plan0 := planScan(pgtype.TextCodec{}, fds[0], (*string)(nil)) + + return pgx.CollectRows(rows, func(row pgx.CollectableRow) (string, error) { + vals := row.RawValues() + var item string + if err := plan0.Scan(vals[0], &item); err != nil { + return item, fmt.Errorf("scan StringAggFirstName column: %w", err) + } + return item, nil + }) } type scanCacheKey struct { @@ -367,7 +414,7 @@ func (s ptrScanner[T]) Scan(src []byte, dst any) error { } d := dst.(**T) *d = new(T) - return s.basePlan.Scan(src, **d) + return s.basePlan.Scan(src, *d) } func planPtrScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription, target *T) pgtype.ScanPlan { From 2b41dd18069be924125027b2c63c7362243ffe4a Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Thu, 25 Jan 2024 17:27:34 -0800 Subject: [PATCH 06/16] move codecs into template --- internal/codegen/golang/query.gotemplate | 112 +++++++++++++++++----- internal/codegen/golang/templated_file.go | 59 +++++++++++- 2 files changed, 141 insertions(+), 30 deletions(-) diff --git a/internal/codegen/golang/query.gotemplate b/internal/codegen/golang/query.gotemplate index d6456671..852c1b84 100644 --- a/internal/codegen/golang/query.gotemplate +++ b/internal/codegen/golang/query.gotemplate @@ -42,7 +42,6 @@ func NewQuerier(conn genericConn) *DBQuerier { return &DBQuerier{conn: conn} } -{{- range .Declarers}}{{- "\n\n" -}}{{ .Declare $.PkgPath }}{{ end -}} {{- end -}} {{- range $i, $q := .Queries -}} @@ -55,34 +54,41 @@ const {{ $q.SQLVarName }} = {{ $q.EmitPreparedSQL }} func (q *DBQuerier) {{ $q.Name }}(ctx context.Context {{- $q.EmitParams }}) ({{ $q.EmitResultType }}, error) { ctx = context.WithValue(ctx, "pggen_query_name", "{{ $q.Name }}") {{- if eq $q.ResultKind ":one" }} - row := q.conn.QueryRow(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) - {{ $q.EmitResultTypeInit "item" }} - {{- $q.EmitResultDecoders }} - if err := row.Scan({{ $q.EmitRowScanArgs }}); err != nil { - return {{ $q.EmitResultExpr "item" }}, fmt.Errorf("query {{ $q.Name }}: %w", err) - } - {{- $q.EmitResultAssigns "item" }} - return {{ $q.EmitResultExpr "item" }}, nil -{{- else if eq $q.ResultKind ":many" }} - rows, err := q.conn.Query(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) + row, err := q.conn.Query(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) if err != nil { return nil, fmt.Errorf("query {{ $q.Name }}: %w", err) } - defer rows.Close() - {{ $q.EmitResultTypeInit "items" }} - {{- $q.EmitResultDecoders }} - for rows.Next() { - var item {{ $q.EmitResultElem }} - if err := rows.Scan({{- $q.EmitRowScanArgs -}}); err != nil { - return nil, fmt.Errorf("scan {{ $q.Name }} row: %w", err) - } - {{- $q.EmitResultAssigns "nil" }} - items = append(items, {{ $q.EmitResultExpr "item" }}) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close {{ $q.Name }} rows: %w", err) + + fds := rows.FieldDescriptions() + {{- range $i, $col := $q.Outputs -}} + {{ $q.EmitPlanScan $i $col}} + {{- end -}} + + return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) ({{ $q.EmitSingularResultType }}, error) { + vals := row.RawValues() + {{ $q.EmitResultTypeInit "item" }} + {{- range $i, $col := $q.Outputs -}} + {{- $q.EmitScanColumn $i $col -}} + {{- end -}} + }) +{{- else if eq $q.ResultKind ":many" }} + row, err := q.conn.Query(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) + if err != nil { + return nil, fmt.Errorf("query {{ $q.Name }}: %w", err) } - return items, err + + fds := rows.FieldDescriptions() + {{- range $i, $col := $q.Outputs -}} + {{ $q.EmitPlanScan $i $col}} + {{- end -}} + + return pgx.CollectRows(rows, func(row pgx.CollectableRow) ({{ $q.EmitSingularResultType }}, error) { + vals := row.RawValues() + {{ $q.EmitResultTypeInit "item" }} + {{- range $i, $col := $q.Outputs -}} + {{- $q.EmitScanColumn $i $col -}} + {{- end -}} + }) {{- else if eq $q.ResultKind ":exec" }} cmdTag, err := q.conn.Exec(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) if err != nil { @@ -93,4 +99,60 @@ func (q *DBQuerier) {{ $q.Name }}(ctx context.Context {{- $q.EmitParams }}) ({{ } {{- end -}} {{- "\n" -}} +{{ if .IsLeader }} +type scanCacheKey struct { + oid uint32 + format int16 + typeName string +} + +var ( + plans = make(map[scanCacheKey]pgtype.ScanPlan, 16) + plansMu sync.RWMutex +) + +func planScan(codec pgtype.Codec, fd pgconn.FieldDescription, target any) pgtype.ScanPlan { + key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("%T", target)} + plansMu.RLock() + plan := plans[key] + plansMu.RUnlock() + if plan != nil { + return plan + } + plan = codec.PlanScan(nil, fd.DataTypeOID, fd.Format, target) + plansMu.Lock() + plans[key] = plan + plansMu.Unlock() + return plan +} + +type ptrScanner[T any] struct { + basePlan pgtype.ScanPlan +} + +func (s ptrScanner[T]) Scan(src []byte, dst any) error { + if src == nil { + return nil + } + d := dst.(**T) + *d = new(T) + return s.basePlan.Scan(src, *d) +} + +func planPtrScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription, target *T) pgtype.ScanPlan { + key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("*%T", target)} + plansMu.RLock() + plan := plans[key] + plansMu.RUnlock() + if plan != nil { + return plan + } + basePlan := planScan(codec, fd, target) + ptrPlan := ptrScanner[T]{basePlan} + plansMu.Lock() + plans[key] = plan + plansMu.Unlock() + return ptrPlan +} +{{- end -}} {{- end -}} diff --git a/internal/codegen/golang/templated_file.go b/internal/codegen/golang/templated_file.go index d2a54309..715f7737 100644 --- a/internal/codegen/golang/templated_file.go +++ b/internal/codegen/golang/templated_file.go @@ -200,6 +200,31 @@ func (tq TemplatedQuery) isInlineParams() bool { return len(tq.Inputs) <= tq.InlineParamCount } +// EmitPlanScan emits the variable that hold the pgtype.ScanPlan for a query +// output. +func (tq TemplatedQuery) EmitPlanScan(idx int, out TemplatedColumn) (string, error) { + switch tq.ResultKind { + case ast.ResultKindExec: + return "", fmt.Errorf("cannot EmitPlanScanArgs for :exec query %s", tq.Name) + case ast.ResultKindMany, ast.ResultKindOne: + break // okay + default: + return "", fmt.Errorf("unhandled EmitPlanScanArgs type: %s", tq.ResultKind) + } + return fmt.Sprintf("planScan%d = pgtype.TODOCodec{}, fs[%d], (*%s)(nil))", idx, idx, out.Type.BaseName()), nil +} + +// EmitScanColumn emits scan call for a single TemplatedColumn. +func (tq TemplatedQuery) EmitScanColumn(idx int, out TemplatedColumn) (string, error) { + sb := &strings.Builder{} + _, _ = fmt.Fprintf(sb, "if err := plan%d.Scan(vals[%d], &item); err != nil\n", idx, idx) + sb.WriteString("\t\t\t") + _, _ = fmt.Fprintf(sb, `return item, fmt.Errorf("scan %s.%s: %%w", err)`, tq.Name, out.PgName) + sb.WriteString("\n") + sb.WriteString("\t\t}\n") + return sb.String(), nil +} + // EmitRowScanArgs emits the args to scan a single row from a pgx.Row or // pgx.Rows. func (tq TemplatedQuery) EmitRowScanArgs() (string, error) { @@ -256,9 +281,10 @@ func (tq TemplatedQuery) EmitRowScanArgs() (string, error) { return sb.String(), nil } -// EmitResultType returns the string representing the overall query result type, -// meaning the return result. -func (tq TemplatedQuery) EmitResultType() (string, error) { +// EmitSingularResultType returns the string representing a single element +// of the overall query result type, like FindAuthorsRow when the overall return +// type is []FindAuthorsRow. +func (tq TemplatedQuery) EmitSingularResultType() (string, error) { outs := removeVoidColumns(tq.Outputs) switch tq.ResultKind { case ast.ResultKindExec: @@ -268,9 +294,9 @@ func (tq TemplatedQuery) EmitResultType() (string, error) { case 0: return "pgconn.CommandTag", nil case 1: - return "[]" + outs[0].QualType, nil + return outs[0].QualType, nil default: - return "[]" + tq.Name + "Row", nil + return tq.Name + "Row", nil } case ast.ResultKindOne: switch len(outs) { @@ -281,6 +307,29 @@ func (tq TemplatedQuery) EmitResultType() (string, error) { default: return tq.Name + "Row", nil } + default: + return "", fmt.Errorf("unhandled EmitSingularResultType kind: %s", tq.ResultKind) + } +} + +// EmitResultType returns the string representing the overall query result type, +// meaning the return result. +func (tq TemplatedQuery) EmitResultType() (string, error) { + rt, err := tq.EmitSingularResultType() + if err != nil { + return "", fmt.Errorf("unhandled EmitResultType: %w", err) + } + switch tq.ResultKind { + case ast.ResultKindExec: + return rt, nil + case ast.ResultKindMany: + outs := removeVoidColumns(tq.Outputs) + if len(outs) == 0 { + return rt, nil + } + return "[]" + rt, nil + case ast.ResultKindOne: + return rt, nil default: return "", fmt.Errorf("unhandled EmitResultType kind: %s", tq.ResultKind) } From 7ca7c671954af85d8e511ec15c7d2417be335e15 Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Fri, 26 Jan 2024 10:13:40 -0800 Subject: [PATCH 07/16] update template for code --- example/author/query.sql.go | 124 ++++++------ internal/codegen/golang/query.gotemplate | 54 ++---- internal/codegen/golang/templated_file.go | 221 ++++++---------------- internal/codegen/golang/templater.go | 25 ++- internal/pg/type_cache.go | 19 +- 5 files changed, 173 insertions(+), 270 deletions(-) diff --git a/example/author/query.sql.go b/example/author/query.sql.go index 6dd22144..5af26d39 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -49,7 +49,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -80,27 +80,26 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut if err != nil { return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) + plan0 := planScan(pgtype.TextCodec{}, fds[0], (*int32)(nil)) plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) - plan3 := planPtrScan(pgtype.TextCodec{}, fds[3], (*string)(nil)) + plan3 := planScan(pgtype.TextCodec{}, fds[3], (**string)(nil)) return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (FindAuthorByIDRow, error) { vals := row.RawValues() var item FindAuthorByIDRow - if err := plan0.Scan(vals[0], &item.AuthorID); err != nil { - return item, fmt.Errorf("scan FindAuthors.author_id column: %w", err) + if err := plan0.Scan(vals[0], &item); err != nil { + return item, fmt.Errorf("scan FindAuthorByID.author_id: %w", err) } - if err := plan1.Scan(vals[1], &item.FirstName); err != nil { - return item, fmt.Errorf("scan FindAuthors.first_name column: %w", err) + if err := plan1.Scan(vals[1], &item); err != nil { + return item, fmt.Errorf("scan FindAuthorByID.first_name: %w", err) } - if err := plan2.Scan(vals[2], &item.LastName); err != nil { - return item, fmt.Errorf("scan FindAuthors.last_name column: %w", err) + if err := plan2.Scan(vals[2], &item); err != nil { + return item, fmt.Errorf("scan FindAuthorByID.last_name: %w", err) } - if err := plan3.Scan(vals[3], &item.Suffix); err != nil { - return item, fmt.Errorf("scan FindAuthors.suffix column: %w", err) + if err := plan3.Scan(vals[3], &item); err != nil { + return item, fmt.Errorf("scan FindAuthorByID.suffix: %w", err) } return item, nil }) @@ -122,27 +121,26 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu if err != nil { return nil, fmt.Errorf("query FindAuthors: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) + plan0 := planScan(pgtype.TextCodec{}, fds[0], (*int32)(nil)) plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) - plan3 := planPtrScan(pgtype.TextCodec{}, fds[3], (*string)(nil)) + plan3 := planScan(pgtype.TextCodec{}, fds[3], (**string)(nil)) return pgx.CollectRows(rows, func(row pgx.CollectableRow) (FindAuthorsRow, error) { vals := row.RawValues() var item FindAuthorsRow - if err := plan0.Scan(vals[0], &item.AuthorID); err != nil { - return item, fmt.Errorf("scan FindAuthors.author_id column: %w", err) + if err := plan0.Scan(vals[0], &item); err != nil { + return item, fmt.Errorf("scan FindAuthors.author_id: %w", err) } - if err := plan1.Scan(vals[1], &item.FirstName); err != nil { - return item, fmt.Errorf("scan FindAuthors.first_name column: %w", err) + if err := plan1.Scan(vals[1], &item); err != nil { + return item, fmt.Errorf("scan FindAuthors.first_name: %w", err) } - if err := plan2.Scan(vals[2], &item.LastName); err != nil { - return item, fmt.Errorf("scan FindAuthors.last_name column: %w", err) + if err := plan2.Scan(vals[2], &item); err != nil { + return item, fmt.Errorf("scan FindAuthors.last_name: %w", err) } - if err := plan3.Scan(vals[3], &item.Suffix); err != nil { - return item, fmt.Errorf("scan FindAuthors.suffix column: %w", err) + if err := plan3.Scan(vals[3], &item); err != nil { + return item, fmt.Errorf("scan FindAuthors.suffix: %w", err) } return item, nil }) @@ -155,10 +153,6 @@ type FindAuthorNamesRow struct { LastName *string `json:"last_name"` } -func (r *FindAuthorNamesRow) scanRow(rows pgx.Rows) error { - return rows.Scan(&r.FirstName, &r.LastName) -} - // FindAuthorNames implements Querier.FindAuthorNames. func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]FindAuthorNamesRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorNames") @@ -166,19 +160,18 @@ func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]Find if err != nil { return nil, fmt.Errorf("query FindAuthorNames: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (*string)(nil)) - plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) + plan0 := planScan(pgtype.TextCodec{}, fds[0], (**string)(nil)) + plan1 := planScan(pgtype.TextCodec{}, fds[1], (**string)(nil)) return pgx.CollectRows(rows, func(row pgx.CollectableRow) (FindAuthorNamesRow, error) { vals := row.RawValues() var item FindAuthorNamesRow - if err := plan0.Scan(vals[0], &item.FirstName); err != nil { - return item, fmt.Errorf("scan FindAuthors.first_name column: %w", err) + if err := plan0.Scan(vals[0], &item); err != nil { + return item, fmt.Errorf("scan FindAuthorNames.first_name: %w", err) } - if err := plan1.Scan(vals[1], &item.LastName); err != nil { - return item, fmt.Errorf("scan FindAuthors.last_name column: %w", err) + if err := plan1.Scan(vals[1], &item); err != nil { + return item, fmt.Errorf("scan FindAuthorNames.last_name: %w", err) } return item, nil }) @@ -193,17 +186,16 @@ func (q *DBQuerier) FindFirstNames(ctx context.Context, authorID int32) ([]*stri if err != nil { return nil, fmt.Errorf("query FindFirstNames: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (*string)(nil)) + plan0 := planScan(pgtype.TextCodec{}, fds[0], (**string)(nil)) return pgx.CollectRows(rows, func(row pgx.CollectableRow) (*string, error) { vals := row.RawValues() - var item string + var item *string if err := plan0.Scan(vals[0], &item); err != nil { - return nil, fmt.Errorf("scan FindFirstNames row: %w", err) + return item, fmt.Errorf("scan FindFirstNames.first_name: %w", err) } - return &item, nil + return item, nil }) } @@ -214,7 +206,7 @@ func (q *DBQuerier) DeleteAuthors(ctx context.Context) (pgconn.CommandTag, error ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthors") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsSQL) if err != nil { - return cmdTag, fmt.Errorf("exec query DeleteAuthors: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthors: %w", err) } return cmdTag, err } @@ -226,7 +218,7 @@ func (q *DBQuerier) DeleteAuthorsByFirstName(ctx context.Context, firstName stri ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthorsByFirstName") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsByFirstNameSQL, firstName) if err != nil { - return cmdTag, fmt.Errorf("exec query DeleteAuthorsByFirstName: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFirstName: %w", err) } return cmdTag, err } @@ -248,7 +240,7 @@ func (q *DBQuerier) DeleteAuthorsByFullName(ctx context.Context, params DeleteAu ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthorsByFullName") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsByFullNameSQL, params.FirstName, params.LastName, params.Suffix) if err != nil { - return cmdTag, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) } return cmdTag, err } @@ -264,15 +256,14 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName if err != nil { return 0, fmt.Errorf("query InsertAuthor: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) + plan0 := planScan(pgtype.TextCodec{}, fds[0], (*int32)(nil)) return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (int32, error) { vals := row.RawValues() var item int32 if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan InsertAuthor: %w", err) + return item, fmt.Errorf("scan InsertAuthor.author_id: %w", err) } return item, nil }) @@ -295,10 +286,6 @@ type InsertAuthorSuffixRow struct { Suffix *string `json:"suffix"` } -func (r *InsertAuthorSuffixRow) scanRow(rows pgx.Rows) error { - return rows.Scan(&r.AuthorID, &r.FirstName, &r.LastName, &r.Suffix) -} - // InsertAuthorSuffix implements Querier.InsertAuthorSuffix. func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorSuffixParams) (InsertAuthorSuffixRow, error) { ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthorSuffix") @@ -306,33 +293,32 @@ func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorS if err != nil { return InsertAuthorSuffixRow{}, fmt.Errorf("query InsertAuthorSuffix: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.Int4Codec{}, fds[0], (*int32)(nil)) + plan0 := planScan(pgtype.TextCodec{}, fds[0], (*int32)(nil)) plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) - plan3 := planPtrScan(pgtype.TextCodec{}, fds[3], (*string)(nil)) + plan3 := planScan(pgtype.TextCodec{}, fds[3], (**string)(nil)) return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (InsertAuthorSuffixRow, error) { vals := row.RawValues() var item InsertAuthorSuffixRow - if err := plan0.Scan(vals[0], &item.AuthorID); err != nil { - return item, fmt.Errorf("scan InsertAuthorSuffixRow.author_id column: %w", err) + if err := plan0.Scan(vals[0], &item); err != nil { + return item, fmt.Errorf("scan InsertAuthorSuffix.author_id: %w", err) } - if err := plan1.Scan(vals[1], &item.FirstName); err != nil { - return item, fmt.Errorf("scan InsertAuthorSuffixRow.first_name column: %w", err) + if err := plan1.Scan(vals[1], &item); err != nil { + return item, fmt.Errorf("scan InsertAuthorSuffix.first_name: %w", err) } - if err := plan2.Scan(vals[2], &item.LastName); err != nil { - return item, fmt.Errorf("scan InsertAuthorSuffixRow.last_name column: %w", err) + if err := plan2.Scan(vals[2], &item); err != nil { + return item, fmt.Errorf("scan InsertAuthorSuffix.last_name: %w", err) } - if err := plan3.Scan(vals[3], &item.Suffix); err != nil { - return item, fmt.Errorf("scan InsertAuthorSuffixRow.suffix column: %w", err) + if err := plan3.Scan(vals[3], &item); err != nil { + return item, fmt.Errorf("scan InsertAuthorSuffix.suffix: %w", err) } return item, nil }) } -const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS NAMES FROM author WHERE author_id = $1;` +const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS names FROM author WHERE author_id = $1;` // StringAggFirstName implements Querier.StringAggFirstName. func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*string, error) { @@ -341,21 +327,20 @@ func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*st if err != nil { return nil, fmt.Errorf("query StringAggFirstName: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planPtrScan(pgtype.TextCodec{}, fds[0], (*string)(nil)) + plan0 := planScan(pgtype.TextCodec{}, fds[0], (**string)(nil)) return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (*string, error) { vals := row.RawValues() var item *string if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan StringAggFirstName column: %w", err) + return item, fmt.Errorf("scan StringAggFirstName.names: %w", err) } return item, nil }) } -const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS NAMES FROM author WHERE author_id = $1;` +const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS names FROM author WHERE author_id = $1;` // ArrayAggFirstName implements Querier.ArrayAggFirstName. func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]string, error) { @@ -364,15 +349,14 @@ func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]st if err != nil { return nil, fmt.Errorf("query ArrayAggFirstName: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (*string)(nil)) + plan0 := planScan(pgtype.TextCodec{}, fds[0], (*[]string)(nil)) - return pgx.CollectRows(rows, func(row pgx.CollectableRow) (string, error) { + return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) ([]string, error) { vals := row.RawValues() - var item string + var item []string if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan StringAggFirstName column: %w", err) + return item, fmt.Errorf("scan ArrayAggFirstName.names: %w", err) } return item, nil }) diff --git a/internal/codegen/golang/query.gotemplate b/internal/codegen/golang/query.gotemplate index 852c1b84..d6c823f0 100644 --- a/internal/codegen/golang/query.gotemplate +++ b/internal/codegen/golang/query.gotemplate @@ -27,7 +27,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -53,48 +53,30 @@ const {{ $q.SQLVarName }} = {{ $q.EmitPreparedSQL }} // {{ $q.Name }} implements Querier.{{ $q.Name }}. func (q *DBQuerier) {{ $q.Name }}(ctx context.Context {{- $q.EmitParams }}) ({{ $q.EmitResultType }}, error) { ctx = context.WithValue(ctx, "pggen_query_name", "{{ $q.Name }}") -{{- if eq $q.ResultKind ":one" }} - row, err := q.conn.Query(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) +{{- if eq $q.ResultKind ":exec" }} + cmdTag, err := q.conn.Exec(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) if err != nil { - return nil, fmt.Errorf("query {{ $q.Name }}: %w", err) + return {{ $q.EmitZeroResult }}, fmt.Errorf("exec query {{ $q.Name }}: %w", err) + } + return cmdTag, err +{{- else }} + rows, err := q.conn.Query(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) + if err != nil { + return {{ $q.EmitZeroResult }}, fmt.Errorf("query {{ $q.Name }}: %w", err) } - fds := rows.FieldDescriptions() - {{- range $i, $col := $q.Outputs -}} + {{- range $i, $col := $q.Outputs }} {{ $q.EmitPlanScan $i $col}} - {{- end -}} + {{- end }} - return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) ({{ $q.EmitSingularResultType }}, error) { + return {{ $q.EmitCollectionFunc }}(rows, func(row pgx.CollectableRow) ({{ $q.EmitSingularResultType }}, error) { vals := row.RawValues() - {{ $q.EmitResultTypeInit "item" }} - {{- range $i, $col := $q.Outputs -}} - {{- $q.EmitScanColumn $i $col -}} - {{- end -}} - }) -{{- else if eq $q.ResultKind ":many" }} - row, err := q.conn.Query(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) - if err != nil { - return nil, fmt.Errorf("query {{ $q.Name }}: %w", err) - } - - fds := rows.FieldDescriptions() - {{- range $i, $col := $q.Outputs -}} - {{ $q.EmitPlanScan $i $col}} - {{- end -}} - - return pgx.CollectRows(rows, func(row pgx.CollectableRow) ({{ $q.EmitSingularResultType }}, error) { - vals := row.RawValues() - {{ $q.EmitResultTypeInit "item" }} - {{- range $i, $col := $q.Outputs -}} - {{- $q.EmitScanColumn $i $col -}} - {{- end -}} + var item {{ $q.EmitSingularResultType }} + {{- range $i, $col := $q.Outputs }} + {{ $q.EmitScanColumn $i $col }} + {{- end }} + return item, nil }) -{{- else if eq $q.ResultKind ":exec" }} - cmdTag, err := q.conn.Exec(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) - if err != nil { - return cmdTag, fmt.Errorf("exec query {{ $q.Name }}: %w", err) - } - return cmdTag, err {{- end }} } {{- end -}} diff --git a/internal/codegen/golang/templated_file.go b/internal/codegen/golang/templated_file.go index 715f7737..6b593806 100644 --- a/internal/codegen/golang/templated_file.go +++ b/internal/codegen/golang/templated_file.go @@ -40,7 +40,8 @@ type TemplatedQuery struct { Doc string // doc from the source query file, formatted for Go PreparedSQL string // SQL query, ready to run with PREPARE statement Inputs []TemplatedParam // input parameters to the query - Outputs []TemplatedColumn // output columns of the query + Outputs []TemplatedColumn // non-void output columns of the query + ScanCols []TemplatedColumn // all columns of the query, including void columns InlineParamCount int // inclusive count of params that will be inlined } @@ -211,17 +212,17 @@ func (tq TemplatedQuery) EmitPlanScan(idx int, out TemplatedColumn) (string, err default: return "", fmt.Errorf("unhandled EmitPlanScanArgs type: %s", tq.ResultKind) } - return fmt.Sprintf("planScan%d = pgtype.TODOCodec{}, fs[%d], (*%s)(nil))", idx, idx, out.Type.BaseName()), nil + return fmt.Sprintf("plan%d := planScan(pgtype.TextCodec{}, fds[%d], (*%s)(nil))", idx, idx, out.Type.BaseName()), nil } // EmitScanColumn emits scan call for a single TemplatedColumn. func (tq TemplatedQuery) EmitScanColumn(idx int, out TemplatedColumn) (string, error) { sb := &strings.Builder{} - _, _ = fmt.Fprintf(sb, "if err := plan%d.Scan(vals[%d], &item); err != nil\n", idx, idx) + _, _ = fmt.Fprintf(sb, "if err := plan%d.Scan(vals[%d], &item); err != nil {\n", idx, idx) sb.WriteString("\t\t\t") _, _ = fmt.Fprintf(sb, `return item, fmt.Errorf("scan %s.%s: %%w", err)`, tq.Name, out.PgName) sb.WriteString("\n") - sb.WriteString("\t\t}\n") + sb.WriteString("\t\t}") return sb.String(), nil } @@ -237,7 +238,7 @@ func (tq TemplatedQuery) EmitRowScanArgs() (string, error) { return "", fmt.Errorf("unhandled EmitRowScanArgs type: %s", tq.ResultKind) } - hasOnlyOneNonVoid := len(removeVoidColumns(tq.Outputs)) == 1 + hasOnlyOneOutput := len(tq.Outputs) == 1 sb := strings.Builder{} sb.Grow(15 * len(tq.Outputs)) for i, out := range tq.Outputs { @@ -248,7 +249,7 @@ func (tq TemplatedQuery) EmitRowScanArgs() (string, error) { sb.WriteString(out.LowerName) sb.WriteString("Array") default: - if hasOnlyOneNonVoid { + if hasOnlyOneOutput { sb.WriteString("&item") } else { sb.WriteString("&item.") @@ -261,7 +262,7 @@ func (tq TemplatedQuery) EmitRowScanArgs() (string, error) { sb.WriteString("Row") case *gotype.EnumType, *gotype.OpaqueType: - if hasOnlyOneNonVoid { + if hasOnlyOneOutput { sb.WriteString("&item") } else { sb.WriteString("&item.") @@ -281,55 +282,45 @@ func (tq TemplatedQuery) EmitRowScanArgs() (string, error) { return sb.String(), nil } -// EmitSingularResultType returns the string representing a single element -// of the overall query result type, like FindAuthorsRow when the overall return -// type is []FindAuthorsRow. -func (tq TemplatedQuery) EmitSingularResultType() (string, error) { - outs := removeVoidColumns(tq.Outputs) +func (tq TemplatedQuery) EmitCollectionFunc() (string, error) { switch tq.ResultKind { case ast.ResultKindExec: - return "pgconn.CommandTag", nil + return "", fmt.Errorf("cannot EmitCollectionFunc for :exec query %s", tq.Name) case ast.ResultKindMany: - switch len(outs) { - case 0: - return "pgconn.CommandTag", nil - case 1: - return outs[0].QualType, nil - default: - return tq.Name + "Row", nil - } + return "pgx.CollectRows", nil case ast.ResultKindOne: - switch len(outs) { - case 0: - return "pgconn.CommandTag", nil - case 1: - return outs[0].QualType, nil - default: - return tq.Name + "Row", nil - } + return "pgx.CollectExactlyOneRow", nil default: - return "", fmt.Errorf("unhandled EmitSingularResultType kind: %s", tq.ResultKind) + return "", fmt.Errorf("unhandled EmitCollectionFunc type: %s", tq.ResultKind) + } +} + +// EmitSingularResultType returns the string representing a single element +// of the overall query result type, like FindAuthorsRow when the overall return +// type is []FindAuthorsRow. +func (tq TemplatedQuery) EmitSingularResultType() string { + if tq.ResultKind == ast.ResultKindExec { + return "pgconn.CommandTag" + } + if len(tq.Outputs) == 0 { + return "pgconn.CommandTag" + } + if len(tq.Outputs) == 1 { + return tq.Outputs[0].QualType } + return tq.Name + "Row" } // EmitResultType returns the string representing the overall query result type, // meaning the return result. func (tq TemplatedQuery) EmitResultType() (string, error) { - rt, err := tq.EmitSingularResultType() - if err != nil { - return "", fmt.Errorf("unhandled EmitResultType: %w", err) - } switch tq.ResultKind { case ast.ResultKindExec: - return rt, nil + return "pgconn.CommandTag", nil case ast.ResultKindMany: - outs := removeVoidColumns(tq.Outputs) - if len(outs) == 0 { - return rt, nil - } - return "[]" + rt, nil + return "[]" + tq.EmitSingularResultType(), nil case ast.ResultKindOne: - return rt, nil + return tq.EmitSingularResultType(), nil default: return "", fmt.Errorf("unhandled EmitResultType kind: %s", tq.ResultKind) } @@ -371,108 +362,37 @@ func (tq TemplatedQuery) EmitResultTypeInit(name string) (string, error) { } } -// EmitResultDecoders declares all initialization required for output types. -func (tq TemplatedQuery) EmitResultDecoders() (string, error) { - sb := &strings.Builder{} - const indent = "\n\t" // 1 level indent inside querier method - for _, out := range tq.Outputs { - switch typ := gotype.UnwrapNestedType(out.Type).(type) { - case *gotype.CompositeType: - sb.WriteString(indent) - sb.WriteString(out.LowerName) - sb.WriteString("Row := q.types.") - sb.WriteString(NameCompositeTranscoderFunc(typ)) - sb.WriteString("()") - case *gotype.ArrayType: - switch gotype.UnwrapNestedType(typ.Elem).(type) { - case *gotype.EnumType, *gotype.CompositeType: - // For all other array elems, a normal array works. - sb.WriteString(indent) - sb.WriteString(out.LowerName) - sb.WriteString("Array := q.types.") - sb.WriteString(NameArrayTranscoderFunc(typ)) - sb.WriteString("()") - } - default: - continue +// EmitZeroResult returns the string representing the zero value of a result. +func (tq TemplatedQuery) EmitZeroResult() (string, error) { + switch tq.ResultKind { + case ast.ResultKindExec: + return "pgconn.CommandTag{}", nil + case ast.ResultKindMany: + return "nil", nil // empty slice + case ast.ResultKindOne: + if len(tq.Outputs) > 1 { + return tq.Name + "Row{}", nil } - } - return sb.String(), nil -} - -// EmitResultAssigns writes all the assign statements after scanning the result -// from pgx. -// -// Copies pgtype.CompositeFields representing a Postgres composite type into the -// output struct. -// -// Copies pgtype.EnumArray fields into Go enum array types. -func (tq TemplatedQuery) EmitResultAssigns(zeroVal string) (string, error) { - sb := &strings.Builder{} - indent := "\n\t" - if tq.ResultKind == ast.ResultKindMany { - indent += "\t" // a :many query processes items in a for loop - } - for _, out := range tq.Outputs { - switch typ := gotype.UnwrapNestedType(out.Type).(type) { - case *gotype.CompositeType: - sb.WriteString(indent) - sb.WriteString("if err := ") - sb.WriteString(out.LowerName) - sb.WriteString("Row.AssignTo(&item") - if len(removeVoidColumns(tq.Outputs)) > 1 { - sb.WriteRune('.') - sb.WriteString(out.UpperName) - } - sb.WriteString("); err != nil {") - sb.WriteString(indent) - sb.WriteString("\treturn ") - sb.WriteString(zeroVal) - sb.WriteString(", fmt.Errorf(\"assign ") - sb.WriteString(tq.Name) - sb.WriteString(" row: %w\", err)") - sb.WriteString(indent) - sb.WriteString("}") - case *gotype.ArrayType: - switch gotype.UnwrapNestedType(typ.Elem).(type) { - case *gotype.CompositeType, *gotype.EnumType: - sb.WriteString(indent) - sb.WriteString("if err := ") - sb.WriteString(out.LowerName) - sb.WriteString("Array.AssignTo(&item") - if len(removeVoidColumns(tq.Outputs)) > 1 { - sb.WriteRune('.') - sb.WriteString(out.UpperName) - } - sb.WriteString("); err != nil {") - sb.WriteString(indent) - sb.WriteString("\treturn ") - sb.WriteString(zeroVal) - sb.WriteString(", fmt.Errorf(\"assign ") - sb.WriteString(tq.Name) - sb.WriteString(" row: %w\", err)") - sb.WriteString(indent) - sb.WriteString("}") - } + typ := tq.Outputs[0].Type.BaseName() + switch { + case strings.HasPrefix(typ, "[]"): + return "nil", nil // empty slice + case strings.HasPrefix(typ, "*"): + return "nil", nil // nil pointer } + switch typ { + case "int", "int32", "int64", "float32", "float64": + return "0", nil + case "string": + return `""`, nil + case "bool": + return "false", nil + default: + return typ + "{}", nil // won't work for type Foo int + } + default: + return "", fmt.Errorf("unhandled EmitZeroResult kind: %s", tq.ResultKind) } - return sb.String(), nil -} - -// EmitResultElem returns the string representing a single item in the overall -// query result type. For :one and :exec queries, this is the same as -// EmitResultType. For :many queries, this is the element type of the slice -// result type. -func (tq TemplatedQuery) EmitResultElem() (string, error) { - result, err := tq.EmitResultType() - if err != nil { - return "", fmt.Errorf("unhandled EmitResultElem type: %w", err) - } - // Unwrap arrays because we build the array with append. - arr := strings.TrimPrefix(result, "[]") - // Unwrap pointers because we add "&" to return the correct types. - ptr := strings.TrimPrefix(arr, "*") - return ptr, nil } // EmitResultExpr returns the string representation of a single item to return @@ -531,16 +451,15 @@ func (tq TemplatedQuery) EmitRowStruct() string { case ast.ResultKindExec: return "" case ast.ResultKindOne, ast.ResultKindMany: - outs := removeVoidColumns(tq.Outputs) - if len(outs) <= 1 { + if len(tq.Outputs) == 1 { return "" // if there's only 1 output column, return it directly } sb := &strings.Builder{} sb.WriteString("\n\ntype ") sb.WriteString(tq.Name) sb.WriteString("Row struct {\n") - maxNameLen, maxTypeLen := getLongestOutput(outs) - for _, out := range outs { + maxNameLen, maxTypeLen := getLongestOutput(tq.Outputs) + for _, out := range tq.Outputs { // Name sb.WriteString("\t") sb.WriteString(out.UpperName) @@ -560,17 +479,3 @@ func (tq TemplatedQuery) EmitRowStruct() string { panic("unhandled result type: " + tq.ResultKind) } } - -// removeVoidColumns makes a copy of cols with all VoidType columns removed. -// Useful because return types shouldn't contain the void type, but we need -// to use a nil placeholder for void types when scanning a pgx.Row. -func removeVoidColumns(cols []TemplatedColumn) []TemplatedColumn { - outs := make([]TemplatedColumn, 0, len(cols)) - for _, col := range cols { - if _, ok := col.Type.(*gotype.VoidType); ok { - continue - } - outs = append(outs, col) - } - return outs -} diff --git a/internal/codegen/golang/templater.go b/internal/codegen/golang/templater.go index de73461e..4d56e40c 100644 --- a/internal/codegen/golang/templater.go +++ b/internal/codegen/golang/templater.go @@ -2,6 +2,7 @@ package golang import ( "fmt" + "github.com/jschaf/pggen/internal/ast" "github.com/jschaf/pggen/internal/casing" "github.com/jschaf/pggen/internal/codegen" "github.com/jschaf/pggen/internal/codegen/golang/gotype" @@ -183,14 +184,20 @@ func (tm Templater) templateFile(file codegen.QueryFile, isLeader bool) (Templat declarers.AddAll(ds...) } + nonVoidCols := removeVoidColumns(outputs) + resultKind := query.ResultKind + if len(nonVoidCols) == 0 { + resultKind = ast.ResultKindExec + } queries = append(queries, TemplatedQuery{ Name: tm.caser.ToUpperGoIdent(query.Name), SQLVarName: tm.caser.ToLowerGoIdent(query.Name) + "SQL", - ResultKind: query.ResultKind, + ResultKind: resultKind, Doc: docs.String(), PreparedSQL: query.PreparedSQL, Inputs: inputs, - Outputs: outputs, + Outputs: nonVoidCols, + ScanCols: outputs, InlineParamCount: tm.inlineParamCount, }) } @@ -232,3 +239,17 @@ func (tm Templater) chooseLowerName(pgName string, fallback string, idx int, num } return fallback + suffix } + +// removeVoidColumns makes a copy of cols with all VoidType columns removed. +// Useful because return types shouldn't contain the void type, but we need +// to use a nil placeholder for void types when scanning a pgx.Row. +func removeVoidColumns(cols []TemplatedColumn) []TemplatedColumn { + outs := make([]TemplatedColumn, 0, len(cols)) + for _, col := range cols { + if _, ok := col.Type.(*gotype.VoidType); ok { + continue + } + outs = append(outs, col) + } + return outs +} diff --git a/internal/pg/type_cache.go b/internal/pg/type_cache.go index 53101a37..dc18549e 100644 --- a/internal/pg/type_cache.go +++ b/internal/pg/type_cache.go @@ -1,6 +1,7 @@ package pg import ( + "github.com/jackc/pgx/v5/pgtype" "sync" ) @@ -21,6 +22,16 @@ func newTypeCache() *typeCache { } } +func lookup() { + !!!fixme + m := pgtype.NewMap() // !!! + m.TypeForValue() // looks up pg type for go type + m.FormatCodeForOID() + m.TypeForOID() + m.TypeForName() + +} + // getOIDs returns the cached OIDS (with the type) and uncached OIDs. func (tc *typeCache) getOIDs(oids ...uint32) (map[uint32]Type, map[uint32]struct{}) { cachedTypes := make(map[uint32]Type, len(oids)) @@ -28,10 +39,10 @@ func (tc *typeCache) getOIDs(oids ...uint32) (map[uint32]Type, map[uint32]struct tc.mu.Lock() defer tc.mu.Unlock() for _, oid := range oids { - if t, ok := tc.types[uint32(oid)]; ok { - cachedTypes[uint32(oid)] = t + if t, ok := tc.types[oid]; ok { + cachedTypes[oid] = t } else { - uncachedTypes[uint32(oid)] = struct{}{} + uncachedTypes[oid] = struct{}{} } } return cachedTypes, uncachedTypes @@ -39,7 +50,7 @@ func (tc *typeCache) getOIDs(oids ...uint32) (map[uint32]Type, map[uint32]struct func (tc *typeCache) getOID(oid uint32) (Type, bool) { tc.mu.Lock() - typ, ok := tc.types[uint32(oid)] + typ, ok := tc.types[oid] tc.mu.Unlock() return typ, ok } From 6d5a5fff2bdbd6867544dd0aa1de7d63fde65364 Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 12:55:57 +0000 Subject: [PATCH 08/16] Run tests through dockerized Postgres wrapper --- Makefile | 2 +- internal/pgdocker/cmd/pgdocker/main.go | 59 ++++++++++++++++++++++++++ internal/pgtest/pg_test_db.go | 26 ++++++++++-- 3 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 internal/pgdocker/cmd/pgdocker/main.go diff --git a/Makefile b/Makefile index 823c3480..6de01999 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ psql: .PHONY: test test: - go test ./... + go run ./internal/pgdocker/cmd/pgdocker -- go test ./... .PHONY: acceptance-test acceptance-test: diff --git a/internal/pgdocker/cmd/pgdocker/main.go b/internal/pgdocker/cmd/pgdocker/main.go new file mode 100644 index 00000000..765acecd --- /dev/null +++ b/internal/pgdocker/cmd/pgdocker/main.go @@ -0,0 +1,59 @@ +// pgdocker starts a Postgres Docker container and exposes it to a wrapped +// command through the PGURL environment variable. +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "os/exec" + "time" + + "github.com/jschaf/pggen/internal/errs" + "github.com/jschaf/pggen/internal/pgdocker" +) + +func main() { + if err := run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + fmt.Fprintln(os.Stderr, err) + os.Exit(exitErr.ExitCode()) + } + log.Fatal(err) + } +} + +func run() (mErr error) { + if len(os.Args) < 3 || os.Args[1] != "--" { + return fmt.Errorf("usage: %s -- [args...]", os.Args[0]) + } + + ctx := context.Background() + docker, err := pgdocker.Start(ctx, nil) + if err != nil { + return fmt.Errorf("start postgres: %w", err) + } + defer errs.Capture(&mErr, func() error { + stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + return docker.Stop(stopCtx) + }, "stop postgres") + + connStr, err := docker.ConnString() + if err != nil { + return fmt.Errorf("postgres connection string: %w", err) + } + + wrapped := exec.Command(os.Args[2], os.Args[3:]...) //nolint:gosec // Runs the command explicitly supplied after --. + wrapped.Stdin = os.Stdin + wrapped.Stdout = os.Stdout + wrapped.Stderr = os.Stderr + wrapped.Env = append(os.Environ(), "PGURL="+connStr) + if err := wrapped.Run(); err != nil { + return fmt.Errorf("%v: %w", wrapped.Args, err) + } + return nil +} diff --git a/internal/pgtest/pg_test_db.go b/internal/pgtest/pg_test_db.go index 1ee8388c..42e4172c 100644 --- a/internal/pgtest/pg_test_db.go +++ b/internal/pgtest/pg_test_db.go @@ -3,6 +3,7 @@ package pgtest import ( "context" "math/rand/v2" + "net/url" "os" "strconv" "strings" @@ -22,7 +23,7 @@ type Option func(config *pgx.ConnConfig) func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Conn, CleanupFunc) { t.Helper() // Create a new schema. - connStr := "user=postgres password=hunter2 host=localhost port=5555 dbname=pggen" + connStr := postgresConnString() ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() conn, err := pgx.Connect(ctx, connStr) @@ -36,10 +37,10 @@ func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Con t.Logf("created schema: %s", schema) // Load SQL files into new schema. - connStr += " search_path=" + schema - connCfg, err := pgx.ParseConfig(connStr) + schemaConnStr := postgresSchemaConnString(connStr, schema) + connCfg, err := pgx.ParseConfig(schemaConnStr) if err != nil { - t.Fatalf("parse config: %q: %s", connStr, err) + t.Fatalf("parse config: %q: %s", schemaConnStr, err) } for _, opt := range opts { opt(connCfg) @@ -69,6 +70,23 @@ func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Con return schemaConn, cleanup } +func postgresConnString() string { + if connStr := os.Getenv("PGURL"); connStr != "" { + return connStr + } + return "user=postgres password=hunter2 host=localhost port=5555 dbname=pggen" +} + +func postgresSchemaConnString(connStr string, schema string) string { + if u, err := url.Parse(connStr); err == nil && (u.Scheme == "postgres" || u.Scheme == "postgresql") { + q := u.Query() + q.Set("search_path", schema) + u.RawQuery = q.Encode() + return u.String() + } + return connStr + " search_path=" + schema +} + // NewPostgresSchema opens a connection with search_path set to a randomly // named, new schema and loads all sqlFiles. func NewPostgresSchema(t *testing.T, sqlFiles []string, opts ...Option) (*pgx.Conn, CleanupFunc) { From 79e1b92769af8f70e18ca21b546189711a62456c Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 14:40:57 +0000 Subject: [PATCH 09/16] Finish pgx v5 generated query support --- example/acceptance_test.go | 2 +- example/author/query.sql.go | 285 ++++----------- example/author/query.sql_test.go | 1 + example/complex_params/query.sql.go | 324 +++++------------- example/composite/query.sql.go | 310 ++++------------- example/composite/query.sql_test.go | 2 +- example/custom_types/query.sql.go | 129 +++---- example/custom_types/query.sql_test.go | 12 +- example/device/query.sql.go | 269 ++++----------- example/device/query.sql_test.go | 24 +- example/domain/query.sql.go | 96 ++---- example/enums/query.sql.go | 277 ++++----------- example/enums/query.sql_test.go | 7 +- example/erp/order/customer.sql.go | 193 ++++------- example/erp/order/customer.sql_test.go | 4 +- example/erp/order/price.sql.go | 47 +-- example/function/query.sql.go | 198 +++-------- example/go_pointer_types/query.sql.go | 175 ++++------ .../inline_param_count/inline0/query.sql.go | 138 ++++---- .../inline_param_count/inline1/query.sql.go | 138 ++++---- .../inline_param_count/inline2/query.sql.go | 138 ++++---- .../inline_param_count/inline3/query.sql.go | 138 ++++---- example/ltree/codegen_test.go | 2 +- example/ltree/query.sql.go | 140 +++----- example/ltree/query.sql_test.go | 41 +-- example/nested/query.sql.go | 225 +++--------- example/pgcrypto/query.sql.go | 104 +++--- .../separate_out_dir/out/alpha_query.sql.0.go | 180 +++------- .../separate_out_dir/out/alpha_query.sql.1.go | 13 +- .../separate_out_dir/out/bravo_query.sql.go | 13 +- example/slices/query.sql.go | 203 +++-------- example/syntax/query.sql.go | 196 +++++------ example/void/query.sql.go | 147 ++++---- internal/codegen/golang/declarer.go | 125 +++---- internal/codegen/golang/declarer_array.go | 143 +------- internal/codegen/golang/declarer_composite.go | 203 +---------- internal/codegen/golang/declarer_enum.go | 36 +- internal/codegen/golang/gotype/known_types.go | 73 ++-- internal/codegen/golang/import_set.go | 21 +- internal/codegen/golang/query.gotemplate | 76 +--- internal/codegen/golang/templated_file.go | 93 +++-- internal/codegen/golang/templater.go | 4 +- .../testdata/declarer_composite.input.golden | 132 ++----- .../testdata/declarer_composite.output.golden | 118 ++----- .../declarer_composite_array.input.golden | 150 ++------ .../declarer_composite_array.output.golden | 124 ++----- .../declarer_composite_enum.input.golden | 136 ++------ .../declarer_composite_enum.output.golden | 125 ++----- .../declarer_composite_nested.input.golden | 149 ++------ .../declarer_composite_nested.output.golden | 129 ++----- .../declarer_enum_escaping.input.golden | 53 +-- .../declarer_enum_escaping.output.golden | 53 +-- .../declarer_enum_simple.input.golden | 53 +-- .../declarer_enum_simple.output.golden | 53 +-- internal/pg/query.sql.go | 193 +++++------ internal/pg/type_fetcher.go | 8 +- internal/pg/type_fetcher_test.go | 2 +- internal/pginfer/pginfer.go | 10 +- internal/pgtest/pg_test_db.go | 80 +++++ 59 files changed, 2100 insertions(+), 4413 deletions(-) diff --git a/example/acceptance_test.go b/example/acceptance_test.go index 5a8cb469..8fac5cf5 100644 --- a/example/acceptance_test.go +++ b/example/acceptance_test.go @@ -180,7 +180,7 @@ func TestExamples(t *testing.T) { "--schema-glob", "example/ltree/schema.sql", "--query-glob", "example/ltree/query.sql", "--go-type", "ltree=github.com/jackc/pgx/v5/pgtype.Text", - "--go-type", "_ltree=github.com/jackc/pgx/v5/pgtype.TextArray", + "--go-type", "_ltree=[]string", }, }, { diff --git a/example/author/query.sql.go b/example/author/query.sql.go index 5af26d39..f950e443 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -7,10 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" - "sync" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { // FindAuthorById finds one (or zero) authors by ID. @@ -64,146 +64,116 @@ func NewQuerier(conn genericConn) *DBQuerier { return &DBQuerier{conn: conn} } +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil +} + +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} +} + const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` type FindAuthorByIDRow struct { - AuthorID int32 `json:"author_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Suffix *string `json:"suffix"` + AuthorID int32 `json:"author_id" db:"author_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` } // FindAuthorByID implements Querier.FindAuthorByID. func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAuthorByIDRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") + ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) if err != nil { return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (*int32)(nil)) - plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) - plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) - plan3 := planScan(pgtype.TextCodec{}, fds[3], (**string)(nil)) - - return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (FindAuthorByIDRow, error) { - vals := row.RawValues() - var item FindAuthorByIDRow - if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan FindAuthorByID.author_id: %w", err) - } - if err := plan1.Scan(vals[1], &item); err != nil { - return item, fmt.Errorf("scan FindAuthorByID.first_name: %w", err) - } - if err := plan2.Scan(vals[2], &item); err != nil { - return item, fmt.Errorf("scan FindAuthorByID.last_name: %w", err) - } - if err := plan3.Scan(vals[3], &item); err != nil { - return item, fmt.Errorf("scan FindAuthorByID.suffix: %w", err) - } - return item, nil - }) + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const findAuthorsSQL = `SELECT * FROM author WHERE first_name = $1;` type FindAuthorsRow struct { - AuthorID int32 `json:"author_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Suffix *string `json:"suffix"` + AuthorID int32 `json:"author_id" db:"author_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` } // FindAuthors implements Querier.FindAuthors. func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAuthorsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthors") + ctx = context.WithValue(ctx, QueryName{}, "FindAuthors") rows, err := q.conn.Query(ctx, findAuthorsSQL, firstName) if err != nil { return nil, fmt.Errorf("query FindAuthors: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (*int32)(nil)) - plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) - plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) - plan3 := planScan(pgtype.TextCodec{}, fds[3], (**string)(nil)) - - return pgx.CollectRows(rows, func(row pgx.CollectableRow) (FindAuthorsRow, error) { - vals := row.RawValues() - var item FindAuthorsRow - if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan FindAuthors.author_id: %w", err) - } - if err := plan1.Scan(vals[1], &item); err != nil { - return item, fmt.Errorf("scan FindAuthors.first_name: %w", err) - } - if err := plan2.Scan(vals[2], &item); err != nil { - return item, fmt.Errorf("scan FindAuthors.last_name: %w", err) - } - if err := plan3.Scan(vals[3], &item); err != nil { - return item, fmt.Errorf("scan FindAuthors.suffix: %w", err) - } - return item, nil - }) + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorsRow]) } const findAuthorNamesSQL = `SELECT first_name, last_name FROM author ORDER BY author_id = $1;` type FindAuthorNamesRow struct { - FirstName *string `json:"first_name"` - LastName *string `json:"last_name"` + FirstName *string `json:"first_name" db:"first_name"` + LastName *string `json:"last_name" db:"last_name"` } // FindAuthorNames implements Querier.FindAuthorNames. func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]FindAuthorNamesRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorNames") + ctx = context.WithValue(ctx, QueryName{}, "FindAuthorNames") rows, err := q.conn.Query(ctx, findAuthorNamesSQL, authorID) if err != nil { return nil, fmt.Errorf("query FindAuthorNames: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (**string)(nil)) - plan1 := planScan(pgtype.TextCodec{}, fds[1], (**string)(nil)) - - return pgx.CollectRows(rows, func(row pgx.CollectableRow) (FindAuthorNamesRow, error) { - vals := row.RawValues() - var item FindAuthorNamesRow - if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan FindAuthorNames.first_name: %w", err) - } - if err := plan1.Scan(vals[1], &item); err != nil { - return item, fmt.Errorf("scan FindAuthorNames.last_name: %w", err) - } - return item, nil - }) + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorNamesRow]) } const findFirstNamesSQL = `SELECT first_name FROM author ORDER BY author_id = $1;` // FindFirstNames implements Querier.FindFirstNames. func (q *DBQuerier) FindFirstNames(ctx context.Context, authorID int32) ([]*string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindFirstNames") + ctx = context.WithValue(ctx, QueryName{}, "FindFirstNames") rows, err := q.conn.Query(ctx, findFirstNamesSQL, authorID) if err != nil { return nil, fmt.Errorf("query FindFirstNames: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (**string)(nil)) - - return pgx.CollectRows(rows, func(row pgx.CollectableRow) (*string, error) { - vals := row.RawValues() - var item *string - if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan FindFirstNames.first_name: %w", err) - } - return item, nil - }) + + return pgx.CollectRows(rows, pgx.RowTo[*string]) } const deleteAuthorsSQL = `DELETE FROM author WHERE first_name = 'joe';` // DeleteAuthors implements Querier.DeleteAuthors. func (q *DBQuerier) DeleteAuthors(ctx context.Context) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthors") + ctx = context.WithValue(ctx, QueryName{}, "DeleteAuthors") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsSQL) if err != nil { return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthors: %w", err) @@ -215,7 +185,7 @@ const deleteAuthorsByFirstNameSQL = `DELETE FROM author WHERE first_name = $1;` // DeleteAuthorsByFirstName implements Querier.DeleteAuthorsByFirstName. func (q *DBQuerier) DeleteAuthorsByFirstName(ctx context.Context, firstName string) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthorsByFirstName") + ctx = context.WithValue(ctx, QueryName{}, "DeleteAuthorsByFirstName") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsByFirstNameSQL, firstName) if err != nil { return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFirstName: %w", err) @@ -237,7 +207,7 @@ type DeleteAuthorsByFullNameParams struct { // DeleteAuthorsByFullName implements Querier.DeleteAuthorsByFullName. func (q *DBQuerier) DeleteAuthorsByFullName(ctx context.Context, params DeleteAuthorsByFullNameParams) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthorsByFullName") + ctx = context.WithValue(ctx, QueryName{}, "DeleteAuthorsByFullName") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsByFullNameSQL, params.FirstName, params.LastName, params.Suffix) if err != nil { return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) @@ -251,22 +221,13 @@ RETURNING author_id;` // InsertAuthor implements Querier.InsertAuthor. func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName string) (int32, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthor") + ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") rows, err := q.conn.Query(ctx, insertAuthorSQL, firstName, lastName) if err != nil { return 0, fmt.Errorf("query InsertAuthor: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (*int32)(nil)) - - return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (int32, error) { - vals := row.RawValues() - var item int32 - if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan InsertAuthor.author_id: %w", err) - } - return item, nil - }) + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) } const insertAuthorSuffixSQL = `INSERT INTO author (first_name, last_name, suffix) @@ -280,139 +241,45 @@ type InsertAuthorSuffixParams struct { } type InsertAuthorSuffixRow struct { - AuthorID int32 `json:"author_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Suffix *string `json:"suffix"` + AuthorID int32 `json:"author_id" db:"author_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` } // InsertAuthorSuffix implements Querier.InsertAuthorSuffix. func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorSuffixParams) (InsertAuthorSuffixRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthorSuffix") + ctx = context.WithValue(ctx, QueryName{}, "InsertAuthorSuffix") rows, err := q.conn.Query(ctx, insertAuthorSuffixSQL, params.FirstName, params.LastName, params.Suffix) if err != nil { return InsertAuthorSuffixRow{}, fmt.Errorf("query InsertAuthorSuffix: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (*int32)(nil)) - plan1 := planScan(pgtype.TextCodec{}, fds[1], (*string)(nil)) - plan2 := planScan(pgtype.TextCodec{}, fds[2], (*string)(nil)) - plan3 := planScan(pgtype.TextCodec{}, fds[3], (**string)(nil)) - - return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (InsertAuthorSuffixRow, error) { - vals := row.RawValues() - var item InsertAuthorSuffixRow - if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan InsertAuthorSuffix.author_id: %w", err) - } - if err := plan1.Scan(vals[1], &item); err != nil { - return item, fmt.Errorf("scan InsertAuthorSuffix.first_name: %w", err) - } - if err := plan2.Scan(vals[2], &item); err != nil { - return item, fmt.Errorf("scan InsertAuthorSuffix.last_name: %w", err) - } - if err := plan3.Scan(vals[3], &item); err != nil { - return item, fmt.Errorf("scan InsertAuthorSuffix.suffix: %w", err) - } - return item, nil - }) + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertAuthorSuffixRow]) } const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS names FROM author WHERE author_id = $1;` // StringAggFirstName implements Querier.StringAggFirstName. func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "StringAggFirstName") + ctx = context.WithValue(ctx, QueryName{}, "StringAggFirstName") rows, err := q.conn.Query(ctx, stringAggFirstNameSQL, authorID) if err != nil { return nil, fmt.Errorf("query StringAggFirstName: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (**string)(nil)) - - return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (*string, error) { - vals := row.RawValues() - var item *string - if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan StringAggFirstName.names: %w", err) - } - return item, nil - }) + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*string]) } const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS names FROM author WHERE author_id = $1;` // ArrayAggFirstName implements Querier.ArrayAggFirstName. func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "ArrayAggFirstName") + ctx = context.WithValue(ctx, QueryName{}, "ArrayAggFirstName") rows, err := q.conn.Query(ctx, arrayAggFirstNameSQL, authorID) if err != nil { return nil, fmt.Errorf("query ArrayAggFirstName: %w", err) } - fds := rows.FieldDescriptions() - plan0 := planScan(pgtype.TextCodec{}, fds[0], (*[]string)(nil)) - - return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) ([]string, error) { - vals := row.RawValues() - var item []string - if err := plan0.Scan(vals[0], &item); err != nil { - return item, fmt.Errorf("scan ArrayAggFirstName.names: %w", err) - } - return item, nil - }) -} - -type scanCacheKey struct { - oid uint32 - format int16 - typeName string -} - -var ( - plans = make(map[scanCacheKey]pgtype.ScanPlan, 16) - plansMu sync.RWMutex -) - -func planScan(codec pgtype.Codec, fd pgconn.FieldDescription, target any) pgtype.ScanPlan { - key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("%T", target)} - plansMu.RLock() - plan := plans[key] - plansMu.RUnlock() - if plan != nil { - return plan - } - plan = codec.PlanScan(nil, fd.DataTypeOID, fd.Format, target) - plansMu.Lock() - plans[key] = plan - plansMu.Unlock() - return plan -} - -type ptrScanner[T any] struct { - basePlan pgtype.ScanPlan -} - -func (s ptrScanner[T]) Scan(src []byte, dst any) error { - if src == nil { - return nil - } - d := dst.(**T) - *d = new(T) - return s.basePlan.Scan(src, *d) -} -func planPtrScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription, target *T) pgtype.ScanPlan { - key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("*%T", target)} - plansMu.RLock() - plan := plans[key] - plansMu.RUnlock() - if plan != nil { - return plan - } - basePlan := planScan(codec, fd, target) - ptrPlan := ptrScanner[T]{basePlan} - plansMu.Lock() - plans[key] = plan - plansMu.Unlock() - return ptrPlan + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]string]) } diff --git a/example/author/query.sql_test.go b/example/author/query.sql_test.go index 8565f9ff..2e15de91 100644 --- a/example/author/query.sql_test.go +++ b/example/author/query.sql_test.go @@ -69,6 +69,7 @@ func TestNewQuerier_FindAuthors(t *testing.T) { LastName: "clinton", Suffix: "jr", }) + require.NoError(t, err) authors, err := q.FindAuthors(t.Context(), "bill") require.NoError(t, err) want := []FindAuthorsRow{ diff --git a/example/complex_params/query.sql.go b/example/complex_params/query.sql.go index fc0589f8..6f4c9e84 100644 --- a/example/complex_params/query.sql.go +++ b/example/complex_params/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { ParamArrayInt(ctx context.Context, ints []int) ([]int, error) @@ -26,8 +27,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -39,313 +39,149 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } // Dimensions represents the Postgres composite type "dimensions". type Dimensions struct { - Width int `json:"width"` - Height int `json:"height"` + Width int `json:"width" db:"width"` + Height int `json:"height" db:"height"` } // ProductImageSetType represents the Postgres composite type "product_image_set_type". type ProductImageSetType struct { - Name string `json:"name"` - OrigImage ProductImageType `json:"orig_image"` - Images []ProductImageType `json:"images"` + Name string `json:"name" db:"name"` + OrigImage ProductImageType `json:"orig_image" db:"orig_image"` + Images []ProductImageType `json:"images" db:"images"` } // ProductImageType represents the Postgres composite type "product_image_type". type ProductImageType struct { - Source string `json:"source"` - Dimensions Dimensions `json:"dimensions"` + Source string `json:"source" db:"source"` + Dimensions Dimensions `json:"dimensions" db:"dimensions"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} +var typesToRegister = []string{} -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true -} -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use -} +var _ = addTypeToRegister("\"dimensions\"") -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newDimensions creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'dimensions'. -func (tr *typeResolver) newDimensions() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "dimensions", - compositeField{name: "width", typeName: "int4", defaultVal: &pgtype.Int4{}}, - compositeField{name: "height", typeName: "int4", defaultVal: &pgtype.Int4{}}, - ) -} -// newDimensionsInit creates an initialized pgtype.ValueTranscoder for the -// Postgres composite type 'dimensions' to encode query parameters. -func (tr *typeResolver) newDimensionsInit(v Dimensions) pgtype.ValueTranscoder { - return tr.setValue(tr.newDimensions(), tr.newDimensionsRaw(v)) -} -// newDimensionsRaw returns all composite fields for the Postgres composite -// type 'dimensions' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newDimensionsRaw(v Dimensions) []interface{} { - return []interface{}{ - v.Width, - v.Height, - } -} +var _ = addTypeToRegister("\"product_image_set_type\"") -// newProductImageSetType creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'product_image_set_type'. -func (tr *typeResolver) newProductImageSetType() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "product_image_set_type", - compositeField{name: "name", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "orig_image", typeName: "product_image_type", defaultVal: tr.newProductImageType()}, - compositeField{name: "images", typeName: "_product_image_type", defaultVal: tr.newProductImageTypeArray()}, - ) -} -// newProductImageSetTypeInit creates an initialized pgtype.ValueTranscoder for the -// Postgres composite type 'product_image_set_type' to encode query parameters. -func (tr *typeResolver) newProductImageSetTypeInit(v ProductImageSetType) pgtype.ValueTranscoder { - return tr.setValue(tr.newProductImageSetType(), tr.newProductImageSetTypeRaw(v)) -} -// newProductImageSetTypeRaw returns all composite fields for the Postgres composite -// type 'product_image_set_type' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newProductImageSetTypeRaw(v ProductImageSetType) []interface{} { - return []interface{}{ - v.Name, - tr.newProductImageTypeRaw(v.OrigImage), - tr.newProductImageTypeArrayRaw(v.Images), - } -} -// newProductImageType creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'product_image_type'. -func (tr *typeResolver) newProductImageType() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "product_image_type", - compositeField{name: "source", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "dimensions", typeName: "dimensions", defaultVal: tr.newDimensions()}, - ) -} -// newProductImageTypeInit creates an initialized pgtype.ValueTranscoder for the -// Postgres composite type 'product_image_type' to encode query parameters. -func (tr *typeResolver) newProductImageTypeInit(v ProductImageType) pgtype.ValueTranscoder { - return tr.setValue(tr.newProductImageType(), tr.newProductImageTypeRaw(v)) -} +var _ = addTypeToRegister("\"product_image_type\"") + + + + + +var _ = addTypeToRegister("\"_product_image_type\"") -// newProductImageTypeRaw returns all composite fields for the Postgres composite -// type 'product_image_type' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newProductImageTypeRaw(v ProductImageType) []interface{} { - return []interface{}{ - v.Source, - tr.newDimensionsRaw(v.Dimensions), - } -} -// newProductImageTypeArray creates a new pgtype.ValueTranscoder for the Postgres -// '_product_image_type' array type. -func (tr *typeResolver) newProductImageTypeArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_product_image_type", "product_image_type", tr.newProductImageType) -} -// newProductImageTypeArrayInit creates an initialized pgtype.ValueTranscoder for the -// Postgres array type '_product_image_type' to encode query parameters. -func (tr *typeResolver) newProductImageTypeArrayInit(ps []ProductImageType) pgtype.ValueTranscoder { - dec := tr.newProductImageTypeArray() - if err := dec.Set(tr.newProductImageTypeArrayRaw(ps)); err != nil { - panic("encode []ProductImageType: " + err.Error()) // should always succeed - } - return textPreferrer{ValueTranscoder: dec, typeName: "_product_image_type"} -} -// newProductImageTypeArrayRaw returns all elements for the Postgres array type '_product_image_type' -// as a slice of interface{} for use with the pgtype.Value Set method. -func (tr *typeResolver) newProductImageTypeArrayRaw(vs []ProductImageType) []interface{} { - elems := make([]interface{}, len(vs)) - for i, v := range vs { - elems[i] = tr.newProductImageTypeRaw(v) - } - return elems -} const paramArrayIntSQL = `SELECT $1::bigint[];` // ParamArrayInt implements Querier.ParamArrayInt. func (q *DBQuerier) ParamArrayInt(ctx context.Context, ints []int) ([]int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "ParamArrayInt") - row := q.conn.QueryRow(ctx, paramArrayIntSQL, ints) - item := []int{} - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query ParamArrayInt: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "ParamArrayInt") + rows, err := q.conn.Query(ctx, paramArrayIntSQL, ints) + if err != nil { + return nil, fmt.Errorf("query ParamArrayInt: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]int]) } const paramNested1SQL = `SELECT $1::dimensions;` // ParamNested1 implements Querier.ParamNested1. func (q *DBQuerier) ParamNested1(ctx context.Context, dimensions Dimensions) (Dimensions, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "ParamNested1") - row := q.conn.QueryRow(ctx, paramNested1SQL, q.types.newDimensionsInit(dimensions)) - var item Dimensions - dimensionsRow := q.types.newDimensions() - if err := row.Scan(dimensionsRow); err != nil { - return item, fmt.Errorf("query ParamNested1: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "ParamNested1") + rows, err := q.conn.Query(ctx, paramNested1SQL, dimensions) + if err != nil { + return Dimensions{}, fmt.Errorf("query ParamNested1: %w", err) } - if err := dimensionsRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ParamNested1 row: %w", err) - } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Dimensions]) } const paramNested2SQL = `SELECT $1::product_image_type;` // ParamNested2 implements Querier.ParamNested2. func (q *DBQuerier) ParamNested2(ctx context.Context, image ProductImageType) (ProductImageType, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "ParamNested2") - row := q.conn.QueryRow(ctx, paramNested2SQL, q.types.newProductImageTypeInit(image)) - var item ProductImageType - productImageTypeRow := q.types.newProductImageType() - if err := row.Scan(productImageTypeRow); err != nil { - return item, fmt.Errorf("query ParamNested2: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "ParamNested2") + rows, err := q.conn.Query(ctx, paramNested2SQL, image) + if err != nil { + return ProductImageType{}, fmt.Errorf("query ParamNested2: %w", err) } - if err := productImageTypeRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ParamNested2 row: %w", err) - } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[ProductImageType]) } const paramNested2ArraySQL = `SELECT $1::product_image_type[];` // ParamNested2Array implements Querier.ParamNested2Array. func (q *DBQuerier) ParamNested2Array(ctx context.Context, images []ProductImageType) ([]ProductImageType, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "ParamNested2Array") - row := q.conn.QueryRow(ctx, paramNested2ArraySQL, q.types.newProductImageTypeArrayInit(images)) - item := []ProductImageType{} - productImageTypeArray := q.types.newProductImageTypeArray() - if err := row.Scan(productImageTypeArray); err != nil { - return item, fmt.Errorf("query ParamNested2Array: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "ParamNested2Array") + rows, err := q.conn.Query(ctx, paramNested2ArraySQL, images) + if err != nil { + return nil, fmt.Errorf("query ParamNested2Array: %w", err) } - if err := productImageTypeArray.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ParamNested2Array row: %w", err) - } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]ProductImageType]) } const paramNested3SQL = `SELECT $1::product_image_set_type;` // ParamNested3 implements Querier.ParamNested3. func (q *DBQuerier) ParamNested3(ctx context.Context, imageSet ProductImageSetType) (ProductImageSetType, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "ParamNested3") - row := q.conn.QueryRow(ctx, paramNested3SQL, q.types.newProductImageSetTypeInit(imageSet)) - var item ProductImageSetType - productImageSetTypeRow := q.types.newProductImageSetType() - if err := row.Scan(productImageSetTypeRow); err != nil { - return item, fmt.Errorf("query ParamNested3: %w", err) - } - if err := productImageSetTypeRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ParamNested3 row: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "ParamNested3") + rows, err := q.conn.Query(ctx, paramNested3SQL, imageSet) + if err != nil { + return ProductImageSetType{}, fmt.Errorf("query ParamNested3: %w", err) } - return item, nil -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[ProductImageSetType]) } - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/composite/query.sql.go b/example/composite/query.sql.go index a62844af..87632eb1 100644 --- a/example/composite/query.sql.go +++ b/example/composite/query.sql.go @@ -10,6 +10,8 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { SearchScreenshots(ctx context.Context, params SearchScreenshotsParams) ([]SearchScreenshotsRow, error) @@ -26,8 +28,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -39,175 +40,79 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } // Arrays represents the Postgres composite type "arrays". type Arrays struct { - Texts []string `json:"texts"` - Int8s []*int `json:"int8s"` - Bools []bool `json:"bools"` - Floats []*float64 `json:"floats"` + Texts []string `json:"texts" db:"texts"` + Int8s []*int `json:"int8s" db:"int8s"` + Bools []bool `json:"bools" db:"bools"` + Floats []*float64 `json:"floats" db:"floats"` } // Blocks represents the Postgres composite type "blocks". type Blocks struct { - ID int `json:"id"` - ScreenshotID int `json:"screenshot_id"` - Body string `json:"body"` + ID int `json:"id" db:"id"` + ScreenshotID int `json:"screenshot_id" db:"screenshot_id"` + Body string `json:"body" db:"body"` } // UserEmail represents the Postgres composite type "user_email". type UserEmail struct { - ID string `json:"id"` - Email pgtype.Text `json:"email"` + ID string `json:"id" db:"id"` + Email pgtype.Text `json:"email" db:"email"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} +var typesToRegister = []string{} -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true -} -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use -} +var _ = addTypeToRegister("\"arrays\"") -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newArrays creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'arrays'. -func (tr *typeResolver) newArrays() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "arrays", - compositeField{name: "texts", typeName: "_text", defaultVal: &pgtype.TextArray{}}, - compositeField{name: "int8s", typeName: "_int8", defaultVal: &pgtype.Int8Array{}}, - compositeField{name: "bools", typeName: "_bool", defaultVal: &pgtype.BoolArray{}}, - compositeField{name: "floats", typeName: "_float8", defaultVal: &pgtype.Float8Array{}}, - ) -} -// newArraysInit creates an initialized pgtype.ValueTranscoder for the -// Postgres composite type 'arrays' to encode query parameters. -func (tr *typeResolver) newArraysInit(v Arrays) pgtype.ValueTranscoder { - return tr.setValue(tr.newArrays(), tr.newArraysRaw(v)) -} -// newArraysRaw returns all composite fields for the Postgres composite -// type 'arrays' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newArraysRaw(v Arrays) []interface{} { - return []interface{}{ - v.Texts, - v.Int8s, - v.Bools, - v.Floats, - } -} +var _ = addTypeToRegister("\"blocks\"") -// newBlocks creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'blocks'. -func (tr *typeResolver) newBlocks() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "blocks", - compositeField{name: "id", typeName: "int4", defaultVal: &pgtype.Int4{}}, - compositeField{name: "screenshot_id", typeName: "int8", defaultVal: &pgtype.Int8{}}, - compositeField{name: "body", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"user_email\"") -// newUserEmail creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'user_email'. -func (tr *typeResolver) newUserEmail() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "user_email", - compositeField{name: "id", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "email", typeName: "citext", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"_blocks\"") -// newBlocksArray creates a new pgtype.ValueTranscoder for the Postgres -// '_blocks' array type. -func (tr *typeResolver) newBlocksArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_blocks", "blocks", tr.newBlocks) -} -// newboolArrayRaw returns all elements for the Postgres array type '_bool' -// as a slice of interface{} for use with the pgtype.Value Set method. -func (tr *typeResolver) newboolArrayRaw(vs []bool) []interface{} { - elems := make([]interface{}, len(vs)) - for i, v := range vs { - elems[i] = v - } - return elems -} const searchScreenshotsSQL = `SELECT ss.id, @@ -226,34 +131,19 @@ type SearchScreenshotsParams struct { } type SearchScreenshotsRow struct { - ID int `json:"id"` - Blocks []Blocks `json:"blocks"` + ID int `json:"id" db:"id"` + Blocks []Blocks `json:"blocks" db:"blocks"` } // SearchScreenshots implements Querier.SearchScreenshots. func (q *DBQuerier) SearchScreenshots(ctx context.Context, params SearchScreenshotsParams) ([]SearchScreenshotsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "SearchScreenshots") + ctx = context.WithValue(ctx, QueryName{}, "SearchScreenshots") rows, err := q.conn.Query(ctx, searchScreenshotsSQL, params.Body, params.Limit, params.Offset) if err != nil { return nil, fmt.Errorf("query SearchScreenshots: %w", err) } - defer rows.Close() - items := []SearchScreenshotsRow{} - blocksArray := q.types.newBlocksArray() - for rows.Next() { - var item SearchScreenshotsRow - if err := rows.Scan(&item.ID, blocksArray); err != nil { - return nil, fmt.Errorf("scan SearchScreenshots row: %w", err) - } - if err := blocksArray.AssignTo(&item.Blocks); err != nil { - return nil, fmt.Errorf("assign SearchScreenshots row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close SearchScreenshots rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[SearchScreenshotsRow]) } const searchScreenshotsOneColSQL = `SELECT @@ -273,28 +163,13 @@ type SearchScreenshotsOneColParams struct { // SearchScreenshotsOneCol implements Querier.SearchScreenshotsOneCol. func (q *DBQuerier) SearchScreenshotsOneCol(ctx context.Context, params SearchScreenshotsOneColParams) ([][]Blocks, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "SearchScreenshotsOneCol") + ctx = context.WithValue(ctx, QueryName{}, "SearchScreenshotsOneCol") rows, err := q.conn.Query(ctx, searchScreenshotsOneColSQL, params.Body, params.Limit, params.Offset) if err != nil { return nil, fmt.Errorf("query SearchScreenshotsOneCol: %w", err) } - defer rows.Close() - items := [][]Blocks{} - blocksArray := q.types.newBlocksArray() - for rows.Next() { - var item []Blocks - if err := rows.Scan(blocksArray); err != nil { - return nil, fmt.Errorf("scan SearchScreenshotsOneCol row: %w", err) - } - if err := blocksArray.AssignTo(&item); err != nil { - return nil, fmt.Errorf("assign SearchScreenshotsOneCol row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close SearchScreenshotsOneCol rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowTo[[]Blocks]) } const insertScreenshotBlocksSQL = `WITH screens AS ( @@ -307,77 +182,44 @@ VALUES ($1, $2) RETURNING id, screenshot_id, body;` type InsertScreenshotBlocksRow struct { - ID int `json:"id"` - ScreenshotID int `json:"screenshot_id"` - Body string `json:"body"` + ID int `json:"id" db:"id"` + ScreenshotID int `json:"screenshot_id" db:"screenshot_id"` + Body string `json:"body" db:"body"` } // InsertScreenshotBlocks implements Querier.InsertScreenshotBlocks. func (q *DBQuerier) InsertScreenshotBlocks(ctx context.Context, screenshotID int, body string) (InsertScreenshotBlocksRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertScreenshotBlocks") - row := q.conn.QueryRow(ctx, insertScreenshotBlocksSQL, screenshotID, body) - var item InsertScreenshotBlocksRow - if err := row.Scan(&item.ID, &item.ScreenshotID, &item.Body); err != nil { - return item, fmt.Errorf("query InsertScreenshotBlocks: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "InsertScreenshotBlocks") + rows, err := q.conn.Query(ctx, insertScreenshotBlocksSQL, screenshotID, body) + if err != nil { + return InsertScreenshotBlocksRow{}, fmt.Errorf("query InsertScreenshotBlocks: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertScreenshotBlocksRow]) } const arraysInputSQL = `SELECT $1::arrays;` // ArraysInput implements Querier.ArraysInput. func (q *DBQuerier) ArraysInput(ctx context.Context, arrays Arrays) (Arrays, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "ArraysInput") - row := q.conn.QueryRow(ctx, arraysInputSQL, q.types.newArraysInit(arrays)) - var item Arrays - arraysRow := q.types.newArrays() - if err := row.Scan(arraysRow); err != nil { - return item, fmt.Errorf("query ArraysInput: %w", err) - } - if err := arraysRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ArraysInput row: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "ArraysInput") + rows, err := q.conn.Query(ctx, arraysInputSQL, arrays) + if err != nil { + return Arrays{}, fmt.Errorf("query ArraysInput: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Arrays]) } const userEmailsSQL = `SELECT ('foo', 'bar@example.com')::user_email;` // UserEmails implements Querier.UserEmails. func (q *DBQuerier) UserEmails(ctx context.Context) (UserEmail, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "UserEmails") - row := q.conn.QueryRow(ctx, userEmailsSQL) - var item UserEmail - rowRow := q.types.newUserEmail() - if err := row.Scan(rowRow); err != nil { - return item, fmt.Errorf("query UserEmails: %w", err) - } - if err := rowRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign UserEmails row: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "UserEmails") + rows, err := q.conn.Query(ctx, userEmailsSQL) + if err != nil { + return UserEmail{}, fmt.Errorf("query UserEmails: %w", err) } - return item, nil -} -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[UserEmail]) } - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/composite/query.sql_test.go b/example/composite/query.sql_test.go index a1d657c4..1724785a 100644 --- a/example/composite/query.sql_test.go +++ b/example/composite/query.sql_test.go @@ -87,7 +87,7 @@ func TestNewQuerier_UserEmails(t *testing.T) { require.NoError(t, err) want := UserEmail{ ID: "foo", - Email: pgtype.Text{String: "bar@example.com", Status: pgtype.Present}, + Email: pgtype.Text{String: "bar@example.com", Valid: true}, } difftest.AssertSame(t, want, got) } diff --git a/example/custom_types/query.sql.go b/example/custom_types/query.sql.go index 326d2b2d..6b4dbc74 100644 --- a/example/custom_types/query.sql.go +++ b/example/custom_types/query.sql.go @@ -7,10 +7,11 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/example/custom_types/mytype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { CustomTypes(ctx context.Context) (CustomTypesRow, error) @@ -23,8 +24,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -36,114 +36,83 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name + return &DBQuerier{conn: conn} } -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const customTypesSQL = `SELECT 'some_text', 1::bigint;` type CustomTypesRow struct { - Column mytype.String `json:"?column?"` - Int8 CustomInt `json:"int8"` + Column mytype.String `json:"?column?" db:"?column?"` + Int8 CustomInt `json:"int8" db:"int8"` } // CustomTypes implements Querier.CustomTypes. func (q *DBQuerier) CustomTypes(ctx context.Context) (CustomTypesRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CustomTypes") - row := q.conn.QueryRow(ctx, customTypesSQL) - var item CustomTypesRow - if err := row.Scan(&item.Column, &item.Int8); err != nil { - return item, fmt.Errorf("query CustomTypes: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CustomTypes") + rows, err := q.conn.Query(ctx, customTypesSQL) + if err != nil { + return CustomTypesRow{}, fmt.Errorf("query CustomTypes: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CustomTypesRow]) } const customMyIntSQL = `SELECT '5'::my_int as int5;` // CustomMyInt implements Querier.CustomMyInt. func (q *DBQuerier) CustomMyInt(ctx context.Context) (int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CustomMyInt") - row := q.conn.QueryRow(ctx, customMyIntSQL) - var item int - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query CustomMyInt: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CustomMyInt") + rows, err := q.conn.Query(ctx, customMyIntSQL) + if err != nil { + return 0, fmt.Errorf("query CustomMyInt: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]) } const intArraySQL = `SELECT ARRAY ['5', '6', '7']::int[] as ints;` // IntArray implements Querier.IntArray. func (q *DBQuerier) IntArray(ctx context.Context) ([][]int32, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "IntArray") + ctx = context.WithValue(ctx, QueryName{}, "IntArray") rows, err := q.conn.Query(ctx, intArraySQL) if err != nil { return nil, fmt.Errorf("query IntArray: %w", err) } - defer rows.Close() - items := [][]int32{} - for rows.Next() { - var item []int32 - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan IntArray row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close IntArray rows: %w", err) - } - return items, err -} -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string + return pgx.CollectRows(rows, pgx.RowTo[[]int32]) } - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/custom_types/query.sql_test.go b/example/custom_types/query.sql_test.go index fdc132a0..e2153007 100644 --- a/example/custom_types/query.sql_test.go +++ b/example/custom_types/query.sql_test.go @@ -38,15 +38,15 @@ func TestQuerier_CustomMyInt(t *testing.T) { AND pn.nspname = current_schema() LIMIT 1; `)) - oidVal := uint32Value{} - err := row.Scan(&oidVal) + var oid uint32 + err := row.Scan(&oid) require.NoError(t, err) - t.Logf("my_int oid: %d", oidVal.Uint) + t.Logf("my_int oid: %d", oid) - conn.ConnInfo().RegisterDataType(pgtype.DataType{ - Value: &pgtype.Int2{}, + conn.TypeMap().RegisterType(&pgtype.Type{ Name: "my_int", - OID: oidVal.Uint, + OID: oid, + Codec: pgtype.Int2Codec{}, }) q := NewQuerier(conn) diff --git a/example/device/query.sql.go b/example/device/query.sql.go index cdad2365..85b9ee91 100644 --- a/example/device/query.sql.go +++ b/example/device/query.sql.go @@ -7,9 +7,11 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" + "net" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { FindDevicesByUser(ctx context.Context, id int) ([]FindDevicesByUserRow, error) @@ -24,14 +26,13 @@ type Querier interface { InsertUser(ctx context.Context, userID int, name string) (pgconn.CommandTag, error) - InsertDevice(ctx context.Context, mac pgtype.Macaddr, owner int) (pgconn.CommandTag, error) + InsertDevice(ctx context.Context, mac net.HardwareAddr, owner int) (pgconn.CommandTag, error) } var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -43,13 +44,13 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } // User represents the Postgres composite type "user". type User struct { - ID *int `json:"id"` - Name *string `json:"name"` + ID *int `json:"id" db:"id"` + Name *string `json:"name" db:"name"` } // DeviceType represents the Postgres enum "device_type". @@ -66,95 +67,45 @@ const ( func (d DeviceType) String() string { return string(d) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newUser creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'user'. -func (tr *typeResolver) newUser() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "user", - compositeField{name: "id", typeName: "int8", defaultVal: &pgtype.Int8{}}, - compositeField{name: "name", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"device_type\"") + +var _ = addTypeToRegister("\"user\"") const findDevicesByUserSQL = `SELECT id, @@ -164,31 +115,20 @@ FROM "user" WHERE id = $1;` type FindDevicesByUserRow struct { - ID int `json:"id"` - Name string `json:"name"` - MacAddrs pgtype.MacaddrArray `json:"mac_addrs"` + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + MacAddrs []net.HardwareAddr `json:"mac_addrs" db:"mac_addrs"` } // FindDevicesByUser implements Querier.FindDevicesByUser. func (q *DBQuerier) FindDevicesByUser(ctx context.Context, id int) ([]FindDevicesByUserRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindDevicesByUser") + ctx = context.WithValue(ctx, QueryName{}, "FindDevicesByUser") rows, err := q.conn.Query(ctx, findDevicesByUserSQL, id) if err != nil { return nil, fmt.Errorf("query FindDevicesByUser: %w", err) } - defer rows.Close() - items := []FindDevicesByUserRow{} - for rows.Next() { - var item FindDevicesByUserRow - if err := rows.Scan(&item.ID, &item.Name, &item.MacAddrs); err != nil { - return nil, fmt.Errorf("scan FindDevicesByUser row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindDevicesByUser rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindDevicesByUserRow]) } const compositeUserSQL = `SELECT @@ -199,102 +139,64 @@ FROM device d LEFT JOIN "user" u ON u.id = d.owner;` type CompositeUserRow struct { - Mac pgtype.Macaddr `json:"mac"` - Type DeviceType `json:"type"` - User User `json:"user"` + Mac net.HardwareAddr `json:"mac" db:"mac"` + Type DeviceType `json:"type" db:"type"` + User User `json:"user" db:"user"` } // CompositeUser implements Querier.CompositeUser. func (q *DBQuerier) CompositeUser(ctx context.Context) ([]CompositeUserRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CompositeUser") + ctx = context.WithValue(ctx, QueryName{}, "CompositeUser") rows, err := q.conn.Query(ctx, compositeUserSQL) if err != nil { return nil, fmt.Errorf("query CompositeUser: %w", err) } - defer rows.Close() - items := []CompositeUserRow{} - userRow := q.types.newUser() - for rows.Next() { - var item CompositeUserRow - if err := rows.Scan(&item.Mac, &item.Type, userRow); err != nil { - return nil, fmt.Errorf("scan CompositeUser row: %w", err) - } - if err := userRow.AssignTo(&item.User); err != nil { - return nil, fmt.Errorf("assign CompositeUser row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close CompositeUser rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[CompositeUserRow]) } const compositeUserOneSQL = `SELECT ROW (15, 'qux')::"user" AS "user";` // CompositeUserOne implements Querier.CompositeUserOne. func (q *DBQuerier) CompositeUserOne(ctx context.Context) (User, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CompositeUserOne") - row := q.conn.QueryRow(ctx, compositeUserOneSQL) - var item User - userRow := q.types.newUser() - if err := row.Scan(userRow); err != nil { - return item, fmt.Errorf("query CompositeUserOne: %w", err) - } - if err := userRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign CompositeUserOne row: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CompositeUserOne") + rows, err := q.conn.Query(ctx, compositeUserOneSQL) + if err != nil { + return User{}, fmt.Errorf("query CompositeUserOne: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[User]) } const compositeUserOneTwoColsSQL = `SELECT 1 AS num, ROW (15, 'qux')::"user" AS "user";` type CompositeUserOneTwoColsRow struct { - Num int32 `json:"num"` - User User `json:"user"` + Num int32 `json:"num" db:"num"` + User User `json:"user" db:"user"` } // CompositeUserOneTwoCols implements Querier.CompositeUserOneTwoCols. func (q *DBQuerier) CompositeUserOneTwoCols(ctx context.Context) (CompositeUserOneTwoColsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CompositeUserOneTwoCols") - row := q.conn.QueryRow(ctx, compositeUserOneTwoColsSQL) - var item CompositeUserOneTwoColsRow - userRow := q.types.newUser() - if err := row.Scan(&item.Num, userRow); err != nil { - return item, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) - } - if err := userRow.AssignTo(&item.User); err != nil { - return item, fmt.Errorf("assign CompositeUserOneTwoCols row: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CompositeUserOneTwoCols") + rows, err := q.conn.Query(ctx, compositeUserOneTwoColsSQL) + if err != nil { + return CompositeUserOneTwoColsRow{}, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CompositeUserOneTwoColsRow]) } const compositeUserManySQL = `SELECT ROW (15, 'qux')::"user" AS "user";` // CompositeUserMany implements Querier.CompositeUserMany. func (q *DBQuerier) CompositeUserMany(ctx context.Context) ([]User, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CompositeUserMany") + ctx = context.WithValue(ctx, QueryName{}, "CompositeUserMany") rows, err := q.conn.Query(ctx, compositeUserManySQL) if err != nil { return nil, fmt.Errorf("query CompositeUserMany: %w", err) } - defer rows.Close() - items := []User{} - userRow := q.types.newUser() - for rows.Next() { - var item User - if err := rows.Scan(userRow); err != nil { - return nil, fmt.Errorf("scan CompositeUserMany row: %w", err) - } - if err := userRow.AssignTo(&item); err != nil { - return nil, fmt.Errorf("assign CompositeUserMany row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close CompositeUserMany rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowTo[User]) } const insertUserSQL = `INSERT INTO "user" (id, name) @@ -302,10 +204,10 @@ VALUES ($1, $2);` // InsertUser implements Querier.InsertUser. func (q *DBQuerier) InsertUser(ctx context.Context, userID int, name string) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertUser") + ctx = context.WithValue(ctx, QueryName{}, "InsertUser") cmdTag, err := q.conn.Exec(ctx, insertUserSQL, userID, name) if err != nil { - return cmdTag, fmt.Errorf("exec query InsertUser: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query InsertUser: %w", err) } return cmdTag, err } @@ -314,36 +216,11 @@ const insertDeviceSQL = `INSERT INTO device (mac, owner) VALUES ($1, $2);` // InsertDevice implements Querier.InsertDevice. -func (q *DBQuerier) InsertDevice(ctx context.Context, mac pgtype.Macaddr, owner int) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertDevice") +func (q *DBQuerier) InsertDevice(ctx context.Context, mac net.HardwareAddr, owner int) (pgconn.CommandTag, error) { + ctx = context.WithValue(ctx, QueryName{}, "InsertDevice") cmdTag, err := q.conn.Exec(ctx, insertDeviceSQL, mac, owner) if err != nil { - return cmdTag, fmt.Errorf("exec query InsertDevice: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query InsertDevice: %w", err) } return cmdTag, err } - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/device/query.sql_test.go b/example/device/query.sql_test.go index 9345a043..b0d87518 100644 --- a/example/device/query.sql_test.go +++ b/example/device/query.sql_test.go @@ -4,7 +4,6 @@ import ( "net" "testing" - "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,7 +18,7 @@ func TestQuerier_FindDevicesByUser(t *testing.T) { _, err := q.InsertUser(ctx, userID, "foo") require.NoError(t, err) mac1, _ := net.ParseMAC("11:22:33:44:55:66") - _, err = q.InsertDevice(ctx, pgtype.Macaddr{Status: pgtype.Present, Addr: mac1}, userID) + _, err = q.InsertDevice(ctx, mac1, userID) require.NoError(t, err) t.Run("FindDevicesByUser", func(t *testing.T) { @@ -27,16 +26,9 @@ func TestQuerier_FindDevicesByUser(t *testing.T) { require.NoError(t, err) want := []FindDevicesByUserRow{ { - ID: userID, - Name: "foo", - MacAddrs: pgtype.MacaddrArray{ - Elements: []pgtype.Macaddr{{Addr: mac1, Status: pgtype.Present}}, - Dimensions: []pgtype.ArrayDimension{{ - Length: 1, - LowerBound: 1, - }}, - Status: pgtype.Present, - }, + ID: userID, + Name: "foo", + MacAddrs: []net.HardwareAddr{mac1}, }, } assert.Equal(t, want, val) @@ -56,9 +48,9 @@ func TestQuerier_CompositeUser(t *testing.T) { mac1, _ := net.ParseMAC("11:22:33:44:55:66") mac2, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff") - _, err = q.InsertDevice(ctx, pgtype.Macaddr{Status: pgtype.Present, Addr: mac1}, userID) + _, err = q.InsertDevice(ctx, mac1, userID) require.NoError(t, err) - _, err = q.InsertDevice(ctx, pgtype.Macaddr{Status: pgtype.Present, Addr: mac2}, userID) + _, err = q.InsertDevice(ctx, mac2, userID) require.NoError(t, err) t.Run("CompositeUser", func(t *testing.T) { @@ -66,12 +58,12 @@ func TestQuerier_CompositeUser(t *testing.T) { require.NoError(t, err) want := []CompositeUserRow{ { - Mac: pgtype.Macaddr{Addr: mac1, Status: pgtype.Present}, + Mac: mac1, Type: DeviceTypeUndefined, User: User{ID: &userID, Name: &name}, }, { - Mac: pgtype.Macaddr{Addr: mac2, Status: pgtype.Present}, + Mac: mac2, Type: DeviceTypeUndefined, User: User{ID: &userID, Name: &name}, }, diff --git a/example/domain/query.sql.go b/example/domain/query.sql.go index 3d1de221..059565e4 100644 --- a/example/domain/query.sql.go +++ b/example/domain/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { DomainOne(ctx context.Context) (string, error) @@ -18,8 +19,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -31,72 +31,52 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name + return &DBQuerier{conn: conn} } -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const domainOneSQL = `SELECT '90210'::us_postal_code;` // DomainOne implements Querier.DomainOne. func (q *DBQuerier) DomainOne(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "DomainOne") - row := q.conn.QueryRow(ctx, domainOneSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query DomainOne: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "DomainOne") + rows, err := q.conn.Query(ctx, domainOneSQL) + if err != nil { + return "", fmt.Errorf("query DomainOne: %w", err) } - return item, nil -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/enums/query.sql.go b/example/enums/query.sql.go index c72c9d9c..f361dada 100644 --- a/example/enums/query.sql.go +++ b/example/enums/query.sql.go @@ -7,14 +7,16 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" + "net" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { FindAllDevices(ctx context.Context) ([]FindAllDevicesRow, error) - InsertDevice(ctx context.Context, mac pgtype.Macaddr, typePg DeviceType) (pgconn.CommandTag, error) + InsertDevice(ctx context.Context, mac net.HardwareAddr, typePg DeviceType) (pgconn.CommandTag, error) // Select an array of all device_type enum values. FindOneDeviceArray(ctx context.Context) ([]DeviceType, error) @@ -32,8 +34,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -45,29 +46,13 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } // Device represents the Postgres composite type "device". type Device struct { - Mac pgtype.Macaddr `json:"mac"` - Type DeviceType `json:"type"` -} - -// newDeviceTypeEnum creates a new pgtype.ValueTranscoder for the -// Postgres enum type 'device_type'. -func newDeviceTypeEnum() pgtype.ValueTranscoder { - return pgtype.NewEnumType( - "device_type", - []string{ - string(DeviceTypeUndefined), - string(DeviceTypePhone), - string(DeviceTypeLaptop), - string(DeviceTypeIpad), - string(DeviceTypeDesktop), - string(DeviceTypeIot), - }, - ) + Mac net.HardwareAddr `json:"mac" db:"mac"` + Type DeviceType `json:"type" db:"type"` } // DeviceType represents the Postgres enum "device_type". @@ -84,141 +69,76 @@ const ( func (d DeviceType) String() string { return string(d) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newDevice creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'device'. -func (tr *typeResolver) newDevice() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "device", - compositeField{name: "mac", typeName: "macaddr", defaultVal: &pgtype.Macaddr{}}, - compositeField{name: "type", typeName: "device_type", defaultVal: newDeviceTypeEnum()}, - ) -} +var _ = addTypeToRegister("\"device_type\"") -// newDeviceTypeArray creates a new pgtype.ValueTranscoder for the Postgres -// '_device_type' array type. -func (tr *typeResolver) newDeviceTypeArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_device_type", "device_type", newDeviceTypeEnum) -} +var _ = addTypeToRegister("\"device\"") + +var _ = addTypeToRegister("\"_device_type\"") const findAllDevicesSQL = `SELECT mac, type FROM device;` type FindAllDevicesRow struct { - Mac pgtype.Macaddr `json:"mac"` - Type DeviceType `json:"type"` + Mac net.HardwareAddr `json:"mac" db:"mac"` + Type DeviceType `json:"type" db:"type"` } // FindAllDevices implements Querier.FindAllDevices. func (q *DBQuerier) FindAllDevices(ctx context.Context) ([]FindAllDevicesRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindAllDevices") + ctx = context.WithValue(ctx, QueryName{}, "FindAllDevices") rows, err := q.conn.Query(ctx, findAllDevicesSQL) if err != nil { return nil, fmt.Errorf("query FindAllDevices: %w", err) } - defer rows.Close() - items := []FindAllDevicesRow{} - for rows.Next() { - var item FindAllDevicesRow - if err := rows.Scan(&item.Mac, &item.Type); err != nil { - return nil, fmt.Errorf("scan FindAllDevices row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindAllDevices rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindAllDevicesRow]) } const insertDeviceSQL = `INSERT INTO device (mac, type) VALUES ($1, $2);` // InsertDevice implements Querier.InsertDevice. -func (q *DBQuerier) InsertDevice(ctx context.Context, mac pgtype.Macaddr, typePg DeviceType) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertDevice") +func (q *DBQuerier) InsertDevice(ctx context.Context, mac net.HardwareAddr, typePg DeviceType) (pgconn.CommandTag, error) { + ctx = context.WithValue(ctx, QueryName{}, "InsertDevice") cmdTag, err := q.conn.Exec(ctx, insertDeviceSQL, mac, typePg) if err != nil { - return cmdTag, fmt.Errorf("exec query InsertDevice: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query InsertDevice: %w", err) } return cmdTag, err } @@ -227,17 +147,13 @@ const findOneDeviceArraySQL = `SELECT enum_range(NULL::device_type) AS device_ty // FindOneDeviceArray implements Querier.FindOneDeviceArray. func (q *DBQuerier) FindOneDeviceArray(ctx context.Context) ([]DeviceType, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOneDeviceArray") - row := q.conn.QueryRow(ctx, findOneDeviceArraySQL) - item := []DeviceType{} - deviceTypesArray := q.types.newDeviceTypeArray() - if err := row.Scan(deviceTypesArray); err != nil { - return item, fmt.Errorf("query FindOneDeviceArray: %w", err) - } - if err := deviceTypesArray.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign FindOneDeviceArray row: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindOneDeviceArray") + rows, err := q.conn.Query(ctx, findOneDeviceArraySQL) + if err != nil { + return nil, fmt.Errorf("query FindOneDeviceArray: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]DeviceType]) } const findManyDeviceArraySQL = `SELECT enum_range('ipad'::device_type, 'iot'::device_type) AS device_types @@ -246,28 +162,13 @@ SELECT enum_range(NULL::device_type) AS device_types;` // FindManyDeviceArray implements Querier.FindManyDeviceArray. func (q *DBQuerier) FindManyDeviceArray(ctx context.Context) ([][]DeviceType, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindManyDeviceArray") + ctx = context.WithValue(ctx, QueryName{}, "FindManyDeviceArray") rows, err := q.conn.Query(ctx, findManyDeviceArraySQL) if err != nil { return nil, fmt.Errorf("query FindManyDeviceArray: %w", err) } - defer rows.Close() - items := [][]DeviceType{} - deviceTypesArray := q.types.newDeviceTypeArray() - for rows.Next() { - var item []DeviceType - if err := rows.Scan(deviceTypesArray); err != nil { - return nil, fmt.Errorf("scan FindManyDeviceArray row: %w", err) - } - if err := deviceTypesArray.AssignTo(&item); err != nil { - return nil, fmt.Errorf("assign FindManyDeviceArray row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindManyDeviceArray rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowTo[[]DeviceType]) } const findManyDeviceArrayWithNumSQL = `SELECT 1 AS num, enum_range('ipad'::device_type, 'iot'::device_type) AS device_types @@ -275,74 +176,30 @@ UNION ALL SELECT 2 as num, enum_range(NULL::device_type) AS device_types;` type FindManyDeviceArrayWithNumRow struct { - Num *int32 `json:"num"` - DeviceTypes []DeviceType `json:"device_types"` + Num *int32 `json:"num" db:"num"` + DeviceTypes []DeviceType `json:"device_types" db:"device_types"` } // FindManyDeviceArrayWithNum implements Querier.FindManyDeviceArrayWithNum. func (q *DBQuerier) FindManyDeviceArrayWithNum(ctx context.Context) ([]FindManyDeviceArrayWithNumRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindManyDeviceArrayWithNum") + ctx = context.WithValue(ctx, QueryName{}, "FindManyDeviceArrayWithNum") rows, err := q.conn.Query(ctx, findManyDeviceArrayWithNumSQL) if err != nil { return nil, fmt.Errorf("query FindManyDeviceArrayWithNum: %w", err) } - defer rows.Close() - items := []FindManyDeviceArrayWithNumRow{} - deviceTypesArray := q.types.newDeviceTypeArray() - for rows.Next() { - var item FindManyDeviceArrayWithNumRow - if err := rows.Scan(&item.Num, deviceTypesArray); err != nil { - return nil, fmt.Errorf("scan FindManyDeviceArrayWithNum row: %w", err) - } - if err := deviceTypesArray.AssignTo(&item.DeviceTypes); err != nil { - return nil, fmt.Errorf("assign FindManyDeviceArrayWithNum row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindManyDeviceArrayWithNum rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindManyDeviceArrayWithNumRow]) } const enumInsideCompositeSQL = `SELECT ROW('08:00:2b:01:02:03'::macaddr, 'phone'::device_type) ::device;` // EnumInsideComposite implements Querier.EnumInsideComposite. func (q *DBQuerier) EnumInsideComposite(ctx context.Context) (Device, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "EnumInsideComposite") - row := q.conn.QueryRow(ctx, enumInsideCompositeSQL) - var item Device - rowRow := q.types.newDevice() - if err := row.Scan(rowRow); err != nil { - return item, fmt.Errorf("query EnumInsideComposite: %w", err) - } - if err := rowRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign EnumInsideComposite row: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "EnumInsideComposite") + rows, err := q.conn.Query(ctx, enumInsideCompositeSQL) + if err != nil { + return Device{}, fmt.Errorf("query EnumInsideComposite: %w", err) } - return item, nil -} -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Device]) } - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/enums/query.sql_test.go b/example/enums/query.sql_test.go index c236a3d8..70d9d906 100644 --- a/example/enums/query.sql_test.go +++ b/example/enums/query.sql_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -28,7 +27,7 @@ func TestNewQuerier_FindAllDevices(t *testing.T) { require.NoError(t, err) assert.Equal(t, []FindAllDevicesRow{ - {Mac: pgtype.Macaddr{Addr: mac, Status: pgtype.Present}, Type: DeviceTypeIot}, + {Mac: mac, Type: DeviceTypeIot}, }, devices, ) @@ -107,7 +106,7 @@ func TestNewQuerier_EnumInsideComposite(t *testing.T) { device, err := q.EnumInsideComposite(ctx) require.NoError(t, err) assert.Equal(t, - Device{Mac: pgtype.Macaddr{Addr: mac, Status: pgtype.Present}, Type: DeviceTypePhone}, + Device{Mac: mac, Type: DeviceTypePhone}, device, ) }) @@ -116,7 +115,7 @@ func TestNewQuerier_EnumInsideComposite(t *testing.T) { func insertDevice(t *testing.T, q *DBQuerier, mac net.HardwareAddr, device DeviceType) { t.Helper() _, err := q.InsertDevice(t.Context(), - pgtype.Macaddr{Addr: mac, Status: pgtype.Present}, + mac, device, ) require.NoError(t, err) diff --git a/example/erp/order/customer.sql.go b/example/erp/order/customer.sql.go index 45aca9c8..e91a598d 100644 --- a/example/erp/order/customer.sql.go +++ b/example/erp/order/customer.sql.go @@ -10,6 +10,8 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { CreateTenant(ctx context.Context, key string, name string) (CreateTenantRow, error) @@ -30,8 +32,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -43,36 +44,41 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const createTenantSQL = `INSERT INTO tenant (tenant_id, name) @@ -80,20 +86,20 @@ VALUES (base36_decode($1::text)::tenant_id, $2::text) RETURNING *;` type CreateTenantRow struct { - TenantID int `json:"tenant_id"` - Rname *string `json:"rname"` - Name string `json:"name"` + TenantID int `json:"tenant_id" db:"tenant_id"` + Rname *string `json:"rname" db:"rname"` + Name string `json:"name" db:"name"` } // CreateTenant implements Querier.CreateTenant. func (q *DBQuerier) CreateTenant(ctx context.Context, key string, name string) (CreateTenantRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CreateTenant") - row := q.conn.QueryRow(ctx, createTenantSQL, key, name) - var item CreateTenantRow - if err := row.Scan(&item.TenantID, &item.Rname, &item.Name); err != nil { - return item, fmt.Errorf("query CreateTenant: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CreateTenant") + rows, err := q.conn.Query(ctx, createTenantSQL, key, name) + if err != nil { + return CreateTenantRow{}, fmt.Errorf("query CreateTenant: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CreateTenantRow]) } const findOrdersByCustomerSQL = `SELECT * @@ -101,32 +107,21 @@ FROM orders WHERE customer_id = $1;` type FindOrdersByCustomerRow struct { - OrderID int32 `json:"order_id"` - OrderDate pgtype.Timestamptz `json:"order_date"` - OrderTotal pgtype.Numeric `json:"order_total"` - CustomerID *int32 `json:"customer_id"` + OrderID int32 `json:"order_id" db:"order_id"` + OrderDate pgtype.Timestamptz `json:"order_date" db:"order_date"` + OrderTotal pgtype.Numeric `json:"order_total" db:"order_total"` + CustomerID *int32 `json:"customer_id" db:"customer_id"` } // FindOrdersByCustomer implements Querier.FindOrdersByCustomer. func (q *DBQuerier) FindOrdersByCustomer(ctx context.Context, customerID int32) ([]FindOrdersByCustomerRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOrdersByCustomer") + ctx = context.WithValue(ctx, QueryName{}, "FindOrdersByCustomer") rows, err := q.conn.Query(ctx, findOrdersByCustomerSQL, customerID) if err != nil { return nil, fmt.Errorf("query FindOrdersByCustomer: %w", err) } - defer rows.Close() - items := []FindOrdersByCustomerRow{} - for rows.Next() { - var item FindOrdersByCustomerRow - if err := rows.Scan(&item.OrderID, &item.OrderDate, &item.OrderTotal, &item.CustomerID); err != nil { - return nil, fmt.Errorf("scan FindOrdersByCustomer row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindOrdersByCustomer rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByCustomerRow]) } const findProductsInOrderSQL = `SELECT o.order_id, p.product_id, p.name @@ -136,31 +131,20 @@ FROM orders o WHERE o.order_id = $1;` type FindProductsInOrderRow struct { - OrderID *int32 `json:"order_id"` - ProductID *int32 `json:"product_id"` - Name *string `json:"name"` + OrderID *int32 `json:"order_id" db:"order_id"` + ProductID *int32 `json:"product_id" db:"product_id"` + Name *string `json:"name" db:"name"` } // FindProductsInOrder implements Querier.FindProductsInOrder. func (q *DBQuerier) FindProductsInOrder(ctx context.Context, orderID int32) ([]FindProductsInOrderRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindProductsInOrder") + ctx = context.WithValue(ctx, QueryName{}, "FindProductsInOrder") rows, err := q.conn.Query(ctx, findProductsInOrderSQL, orderID) if err != nil { return nil, fmt.Errorf("query FindProductsInOrder: %w", err) } - defer rows.Close() - items := []FindProductsInOrderRow{} - for rows.Next() { - var item FindProductsInOrderRow - if err := rows.Scan(&item.OrderID, &item.ProductID, &item.Name); err != nil { - return nil, fmt.Errorf("scan FindProductsInOrder row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindProductsInOrder rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindProductsInOrderRow]) } const insertCustomerSQL = `INSERT INTO customer (first_name, last_name, email) @@ -174,21 +158,21 @@ type InsertCustomerParams struct { } type InsertCustomerRow struct { - CustomerID int32 `json:"customer_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` + CustomerID int32 `json:"customer_id" db:"customer_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Email string `json:"email" db:"email"` } // InsertCustomer implements Querier.InsertCustomer. func (q *DBQuerier) InsertCustomer(ctx context.Context, params InsertCustomerParams) (InsertCustomerRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertCustomer") - row := q.conn.QueryRow(ctx, insertCustomerSQL, params.FirstName, params.LastName, params.Email) - var item InsertCustomerRow - if err := row.Scan(&item.CustomerID, &item.FirstName, &item.LastName, &item.Email); err != nil { - return item, fmt.Errorf("query InsertCustomer: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "InsertCustomer") + rows, err := q.conn.Query(ctx, insertCustomerSQL, params.FirstName, params.LastName, params.Email) + if err != nil { + return InsertCustomerRow{}, fmt.Errorf("query InsertCustomer: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertCustomerRow]) } const insertOrderSQL = `INSERT INTO orders (order_date, order_total, customer_id) @@ -202,44 +186,19 @@ type InsertOrderParams struct { } type InsertOrderRow struct { - OrderID int32 `json:"order_id"` - OrderDate pgtype.Timestamptz `json:"order_date"` - OrderTotal pgtype.Numeric `json:"order_total"` - CustomerID *int32 `json:"customer_id"` + OrderID int32 `json:"order_id" db:"order_id"` + OrderDate pgtype.Timestamptz `json:"order_date" db:"order_date"` + OrderTotal pgtype.Numeric `json:"order_total" db:"order_total"` + CustomerID *int32 `json:"customer_id" db:"customer_id"` } // InsertOrder implements Querier.InsertOrder. func (q *DBQuerier) InsertOrder(ctx context.Context, params InsertOrderParams) (InsertOrderRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertOrder") - row := q.conn.QueryRow(ctx, insertOrderSQL, params.OrderDate, params.OrderTotal, params.CustID) - var item InsertOrderRow - if err := row.Scan(&item.OrderID, &item.OrderDate, &item.OrderTotal, &item.CustomerID); err != nil { - return item, fmt.Errorf("query InsertOrder: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "InsertOrder") + rows, err := q.conn.Query(ctx, insertOrderSQL, params.OrderDate, params.OrderTotal, params.CustID) + if err != nil { + return InsertOrderRow{}, fmt.Errorf("query InsertOrder: %w", err) } - return item, nil -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertOrderRow]) } - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/erp/order/customer.sql_test.go b/example/erp/order/customer.sql_test.go index f08a79be..6f757c79 100644 --- a/example/erp/order/customer.sql_test.go +++ b/example/erp/order/customer.sql_test.go @@ -27,8 +27,8 @@ func TestNewQuerier_FindOrdersByCustomer(t *testing.T) { return } order1, err := q.InsertOrder(ctx, InsertOrderParams{ - OrderDate: pgtype.Timestamptz{Time: time.Now(), Status: pgtype.Present}, - OrderTotal: pgtype.Numeric{Int: big.NewInt(77), Status: pgtype.Present}, + OrderDate: pgtype.Timestamptz{Time: time.Now(), Valid: true}, + OrderTotal: pgtype.Numeric{Int: big.NewInt(77), Valid: true}, CustID: cust1.CustomerID, }) if err != nil { diff --git a/example/erp/order/price.sql.go b/example/erp/order/price.sql.go index c4c0b9ab..f610d29b 100644 --- a/example/erp/order/price.sql.go +++ b/example/erp/order/price.sql.go @@ -5,38 +5,28 @@ package order import ( "context" "fmt" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) const findOrdersByPriceSQL = `SELECT * FROM orders WHERE order_total > $1;` type FindOrdersByPriceRow struct { - OrderID int32 `json:"order_id"` - OrderDate pgtype.Timestamptz `json:"order_date"` - OrderTotal pgtype.Numeric `json:"order_total"` - CustomerID *int32 `json:"customer_id"` + OrderID int32 `json:"order_id" db:"order_id"` + OrderDate pgtype.Timestamptz `json:"order_date" db:"order_date"` + OrderTotal pgtype.Numeric `json:"order_total" db:"order_total"` + CustomerID *int32 `json:"customer_id" db:"customer_id"` } // FindOrdersByPrice implements Querier.FindOrdersByPrice. func (q *DBQuerier) FindOrdersByPrice(ctx context.Context, minTotal pgtype.Numeric) ([]FindOrdersByPriceRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOrdersByPrice") + ctx = context.WithValue(ctx, QueryName{}, "FindOrdersByPrice") rows, err := q.conn.Query(ctx, findOrdersByPriceSQL, minTotal) if err != nil { return nil, fmt.Errorf("query FindOrdersByPrice: %w", err) } - defer rows.Close() - items := []FindOrdersByPriceRow{} - for rows.Next() { - var item FindOrdersByPriceRow - if err := rows.Scan(&item.OrderID, &item.OrderDate, &item.OrderTotal, &item.CustomerID); err != nil { - return nil, fmt.Errorf("scan FindOrdersByPrice row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindOrdersByPrice rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByPriceRow]) } const findOrdersMRRSQL = `SELECT date_trunc('month', order_date) AS month, sum(order_total) AS order_mrr @@ -44,28 +34,17 @@ FROM orders GROUP BY date_trunc('month', order_date);` type FindOrdersMRRRow struct { - Month pgtype.Timestamptz `json:"month"` - OrderMRR pgtype.Numeric `json:"order_mrr"` + Month pgtype.Timestamptz `json:"month" db:"month"` + OrderMRR pgtype.Numeric `json:"order_mrr" db:"order_mrr"` } // FindOrdersMRR implements Querier.FindOrdersMRR. func (q *DBQuerier) FindOrdersMRR(ctx context.Context) ([]FindOrdersMRRRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOrdersMRR") + ctx = context.WithValue(ctx, QueryName{}, "FindOrdersMRR") rows, err := q.conn.Query(ctx, findOrdersMRRSQL) if err != nil { return nil, fmt.Errorf("query FindOrdersMRR: %w", err) } - defer rows.Close() - items := []FindOrdersMRRRow{} - for rows.Next() { - var item FindOrdersMRRRow - if err := rows.Scan(&item.Month, &item.OrderMRR); err != nil { - return nil, fmt.Errorf("scan FindOrdersMRR row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindOrdersMRR rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersMRRRow]) } diff --git a/example/function/query.sql.go b/example/function/query.sql.go index c7ba5bd0..d571f1cb 100644 --- a/example/function/query.sql.go +++ b/example/function/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { OutParams(ctx context.Context) ([]OutParamsRow, error) @@ -18,8 +19,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -31,185 +31,77 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } // ListItem represents the Postgres composite type "list_item". type ListItem struct { - Name *string `json:"name"` - Color *string `json:"color"` + Name *string `json:"name" db:"name"` + Color *string `json:"color" db:"color"` } // ListStats represents the Postgres composite type "list_stats". type ListStats struct { - Val1 *string `json:"val1"` - Val2 []*int32 `json:"val2"` -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + Val1 *string `json:"val1" db:"val1"` + Val2 []*int32 `json:"val2" db:"val2"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newListItem creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'list_item'. -func (tr *typeResolver) newListItem() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "list_item", - compositeField{name: "name", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "color", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"list_item\"") -// newListStats creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'list_stats'. -func (tr *typeResolver) newListStats() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "list_stats", - compositeField{name: "val1", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "val2", typeName: "_int4", defaultVal: &pgtype.Int4Array{}}, - ) -} +var _ = addTypeToRegister("\"list_stats\"") -// newListItemArray creates a new pgtype.ValueTranscoder for the Postgres -// '_list_item' array type. -func (tr *typeResolver) newListItemArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_list_item", "list_item", tr.newListItem) -} +var _ = addTypeToRegister("\"_list_item\"") const outParamsSQL = `SELECT * FROM out_params();` type OutParamsRow struct { - Items []ListItem `json:"_items"` - Stats ListStats `json:"_stats"` + Items []ListItem `json:"_items" db:"_items"` + Stats ListStats `json:"_stats" db:"_stats"` } // OutParams implements Querier.OutParams. func (q *DBQuerier) OutParams(ctx context.Context) ([]OutParamsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "OutParams") + ctx = context.WithValue(ctx, QueryName{}, "OutParams") rows, err := q.conn.Query(ctx, outParamsSQL) if err != nil { return nil, fmt.Errorf("query OutParams: %w", err) } - defer rows.Close() - items := []OutParamsRow{} - itemsArray := q.types.newListItemArray() - statsRow := q.types.newListStats() - for rows.Next() { - var item OutParamsRow - if err := rows.Scan(itemsArray, statsRow); err != nil { - return nil, fmt.Errorf("scan OutParams row: %w", err) - } - if err := itemsArray.AssignTo(&item.Items); err != nil { - return nil, fmt.Errorf("assign OutParams row: %w", err) - } - if err := statsRow.AssignTo(&item.Stats); err != nil { - return nil, fmt.Errorf("assign OutParams row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close OutParams rows: %w", err) - } - return items, err -} -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string + return pgx.CollectRows(rows, pgx.RowToStructByName[OutParamsRow]) } - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/go_pointer_types/query.sql.go b/example/go_pointer_types/query.sql.go index 08276e52..49378119 100644 --- a/example/go_pointer_types/query.sql.go +++ b/example/go_pointer_types/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { GenSeries1(ctx context.Context) (*int, error) @@ -28,8 +29,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -41,36 +41,41 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const genSeries1SQL = `SELECT n @@ -79,13 +84,13 @@ LIMIT 1;` // GenSeries1 implements Querier.GenSeries1. func (q *DBQuerier) GenSeries1(ctx context.Context) (*int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GenSeries1") - row := q.conn.QueryRow(ctx, genSeries1SQL) - var item *int - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query GenSeries1: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "GenSeries1") + rows, err := q.conn.Query(ctx, genSeries1SQL) + if err != nil { + return nil, fmt.Errorf("query GenSeries1: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) } const genSeriesSQL = `SELECT n @@ -93,24 +98,13 @@ FROM generate_series(0, 2) n;` // GenSeries implements Querier.GenSeries. func (q *DBQuerier) GenSeries(ctx context.Context) ([]*int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GenSeries") + ctx = context.WithValue(ctx, QueryName{}, "GenSeries") rows, err := q.conn.Query(ctx, genSeriesSQL) if err != nil { return nil, fmt.Errorf("query GenSeries: %w", err) } - defer rows.Close() - items := []*int{} - for rows.Next() { - var item int - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan GenSeries row: %w", err) - } - items = append(items, &item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GenSeries rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowTo[*int]) } const genSeriesArr1SQL = `SELECT array_agg(n) @@ -118,13 +112,13 @@ FROM generate_series(0, 2) n;` // GenSeriesArr1 implements Querier.GenSeriesArr1. func (q *DBQuerier) GenSeriesArr1(ctx context.Context) ([]int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GenSeriesArr1") - row := q.conn.QueryRow(ctx, genSeriesArr1SQL) - item := []int{} - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query GenSeriesArr1: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "GenSeriesArr1") + rows, err := q.conn.Query(ctx, genSeriesArr1SQL) + if err != nil { + return nil, fmt.Errorf("query GenSeriesArr1: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]int]) } const genSeriesArrSQL = `SELECT array_agg(n) @@ -132,24 +126,13 @@ FROM generate_series(0, 2) n;` // GenSeriesArr implements Querier.GenSeriesArr. func (q *DBQuerier) GenSeriesArr(ctx context.Context) ([][]int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GenSeriesArr") + ctx = context.WithValue(ctx, QueryName{}, "GenSeriesArr") rows, err := q.conn.Query(ctx, genSeriesArrSQL) if err != nil { return nil, fmt.Errorf("query GenSeriesArr: %w", err) } - defer rows.Close() - items := [][]int{} - for rows.Next() { - var item []int - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan GenSeriesArr row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GenSeriesArr rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowTo[[]int]) } const genSeriesStr1SQL = `SELECT n::text @@ -158,13 +141,13 @@ LIMIT 1;` // GenSeriesStr1 implements Querier.GenSeriesStr1. func (q *DBQuerier) GenSeriesStr1(ctx context.Context) (*string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GenSeriesStr1") - row := q.conn.QueryRow(ctx, genSeriesStr1SQL) - var item *string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query GenSeriesStr1: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "GenSeriesStr1") + rows, err := q.conn.Query(ctx, genSeriesStr1SQL) + if err != nil { + return nil, fmt.Errorf("query GenSeriesStr1: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*string]) } const genSeriesStrSQL = `SELECT n::text @@ -172,47 +155,11 @@ FROM generate_series(0, 2) n;` // GenSeriesStr implements Querier.GenSeriesStr. func (q *DBQuerier) GenSeriesStr(ctx context.Context) ([]*string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GenSeriesStr") + ctx = context.WithValue(ctx, QueryName{}, "GenSeriesStr") rows, err := q.conn.Query(ctx, genSeriesStrSQL) if err != nil { return nil, fmt.Errorf("query GenSeriesStr: %w", err) } - defer rows.Close() - items := []*string{} - for rows.Next() { - var item string - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan GenSeriesStr row: %w", err) - } - items = append(items, &item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GenSeriesStr rows: %w", err) - } - return items, err -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName + return pgx.CollectRows(rows, pgx.RowTo[*string]) } - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/inline_param_count/inline0/query.sql.go b/example/inline_param_count/inline0/query.sql.go index c63d4b90..0a7aa26f 100644 --- a/example/inline_param_count/inline0/query.sql.go +++ b/example/inline_param_count/inline0/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { // CountAuthors returns the number of authors (zero params). @@ -28,8 +29,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -41,49 +41,54 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const countAuthorsSQL = `SELECT count(*) FROM author;` // CountAuthors implements Querier.CountAuthors. func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CountAuthors") - row := q.conn.QueryRow(ctx, countAuthorsSQL) - var item *int - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query CountAuthors: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CountAuthors") + rows, err := q.conn.Query(ctx, countAuthorsSQL) + if err != nil { + return nil, fmt.Errorf("query CountAuthors: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -93,21 +98,21 @@ type FindAuthorByIDParams struct { } type FindAuthorByIDRow struct { - AuthorID int32 `json:"author_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Suffix *string `json:"suffix"` + AuthorID int32 `json:"author_id" db:"author_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` } // FindAuthorByID implements Querier.FindAuthorByID. func (q *DBQuerier) FindAuthorByID(ctx context.Context, params FindAuthorByIDParams) (FindAuthorByIDRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") - row := q.conn.QueryRow(ctx, findAuthorByIDSQL, params.AuthorID) - var item FindAuthorByIDRow - if err := row.Scan(&item.AuthorID, &item.FirstName, &item.LastName, &item.Suffix); err != nil { - return item, fmt.Errorf("query FindAuthorByID: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") + rows, err := q.conn.Query(ctx, findAuthorByIDSQL, params.AuthorID) + if err != nil { + return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -121,13 +126,13 @@ type InsertAuthorParams struct { // InsertAuthor implements Querier.InsertAuthor. func (q *DBQuerier) InsertAuthor(ctx context.Context, params InsertAuthorParams) (int32, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthor") - row := q.conn.QueryRow(ctx, insertAuthorSQL, params.FirstName, params.LastName) - var item int32 - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query InsertAuthor: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") + rows, err := q.conn.Query(ctx, insertAuthorSQL, params.FirstName, params.LastName) + if err != nil { + return 0, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) } const deleteAuthorsByFullNameSQL = `DELETE @@ -144,35 +149,10 @@ type DeleteAuthorsByFullNameParams struct { // DeleteAuthorsByFullName implements Querier.DeleteAuthorsByFullName. func (q *DBQuerier) DeleteAuthorsByFullName(ctx context.Context, params DeleteAuthorsByFullNameParams) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthorsByFullName") + ctx = context.WithValue(ctx, QueryName{}, "DeleteAuthorsByFullName") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsByFullNameSQL, params.FirstName, params.LastName, params.Suffix) if err != nil { - return cmdTag, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) } return cmdTag, err } - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/inline_param_count/inline1/query.sql.go b/example/inline_param_count/inline1/query.sql.go index 2b55216d..4a09e23f 100644 --- a/example/inline_param_count/inline1/query.sql.go +++ b/example/inline_param_count/inline1/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { // CountAuthors returns the number of authors (zero params). @@ -28,8 +29,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -41,69 +41,74 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const countAuthorsSQL = `SELECT count(*) FROM author;` // CountAuthors implements Querier.CountAuthors. func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CountAuthors") - row := q.conn.QueryRow(ctx, countAuthorsSQL) - var item *int - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query CountAuthors: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CountAuthors") + rows, err := q.conn.Query(ctx, countAuthorsSQL) + if err != nil { + return nil, fmt.Errorf("query CountAuthors: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` type FindAuthorByIDRow struct { - AuthorID int32 `json:"author_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Suffix *string `json:"suffix"` + AuthorID int32 `json:"author_id" db:"author_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` } // FindAuthorByID implements Querier.FindAuthorByID. func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAuthorByIDRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") - row := q.conn.QueryRow(ctx, findAuthorByIDSQL, authorID) - var item FindAuthorByIDRow - if err := row.Scan(&item.AuthorID, &item.FirstName, &item.LastName, &item.Suffix); err != nil { - return item, fmt.Errorf("query FindAuthorByID: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") + rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) + if err != nil { + return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -117,13 +122,13 @@ type InsertAuthorParams struct { // InsertAuthor implements Querier.InsertAuthor. func (q *DBQuerier) InsertAuthor(ctx context.Context, params InsertAuthorParams) (int32, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthor") - row := q.conn.QueryRow(ctx, insertAuthorSQL, params.FirstName, params.LastName) - var item int32 - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query InsertAuthor: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") + rows, err := q.conn.Query(ctx, insertAuthorSQL, params.FirstName, params.LastName) + if err != nil { + return 0, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) } const deleteAuthorsByFullNameSQL = `DELETE @@ -140,35 +145,10 @@ type DeleteAuthorsByFullNameParams struct { // DeleteAuthorsByFullName implements Querier.DeleteAuthorsByFullName. func (q *DBQuerier) DeleteAuthorsByFullName(ctx context.Context, params DeleteAuthorsByFullNameParams) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthorsByFullName") + ctx = context.WithValue(ctx, QueryName{}, "DeleteAuthorsByFullName") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsByFullNameSQL, params.FirstName, params.LastName, params.Suffix) if err != nil { - return cmdTag, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) } return cmdTag, err } - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/inline_param_count/inline2/query.sql.go b/example/inline_param_count/inline2/query.sql.go index 113ecb35..3408f117 100644 --- a/example/inline_param_count/inline2/query.sql.go +++ b/example/inline_param_count/inline2/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { // CountAuthors returns the number of authors (zero params). @@ -28,8 +29,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -41,69 +41,74 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const countAuthorsSQL = `SELECT count(*) FROM author;` // CountAuthors implements Querier.CountAuthors. func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CountAuthors") - row := q.conn.QueryRow(ctx, countAuthorsSQL) - var item *int - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query CountAuthors: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CountAuthors") + rows, err := q.conn.Query(ctx, countAuthorsSQL) + if err != nil { + return nil, fmt.Errorf("query CountAuthors: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` type FindAuthorByIDRow struct { - AuthorID int32 `json:"author_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Suffix *string `json:"suffix"` + AuthorID int32 `json:"author_id" db:"author_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` } // FindAuthorByID implements Querier.FindAuthorByID. func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAuthorByIDRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") - row := q.conn.QueryRow(ctx, findAuthorByIDSQL, authorID) - var item FindAuthorByIDRow - if err := row.Scan(&item.AuthorID, &item.FirstName, &item.LastName, &item.Suffix); err != nil { - return item, fmt.Errorf("query FindAuthorByID: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") + rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) + if err != nil { + return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -112,13 +117,13 @@ RETURNING author_id;` // InsertAuthor implements Querier.InsertAuthor. func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName string) (int32, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthor") - row := q.conn.QueryRow(ctx, insertAuthorSQL, firstName, lastName) - var item int32 - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query InsertAuthor: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") + rows, err := q.conn.Query(ctx, insertAuthorSQL, firstName, lastName) + if err != nil { + return 0, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) } const deleteAuthorsByFullNameSQL = `DELETE @@ -135,35 +140,10 @@ type DeleteAuthorsByFullNameParams struct { // DeleteAuthorsByFullName implements Querier.DeleteAuthorsByFullName. func (q *DBQuerier) DeleteAuthorsByFullName(ctx context.Context, params DeleteAuthorsByFullNameParams) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthorsByFullName") + ctx = context.WithValue(ctx, QueryName{}, "DeleteAuthorsByFullName") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsByFullNameSQL, params.FirstName, params.LastName, params.Suffix) if err != nil { - return cmdTag, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) } return cmdTag, err } - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/inline_param_count/inline3/query.sql.go b/example/inline_param_count/inline3/query.sql.go index 88f0c874..c19d9f29 100644 --- a/example/inline_param_count/inline3/query.sql.go +++ b/example/inline_param_count/inline3/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { // CountAuthors returns the number of authors (zero params). @@ -28,8 +29,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -41,69 +41,74 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const countAuthorsSQL = `SELECT count(*) FROM author;` // CountAuthors implements Querier.CountAuthors. func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CountAuthors") - row := q.conn.QueryRow(ctx, countAuthorsSQL) - var item *int - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query CountAuthors: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "CountAuthors") + rows, err := q.conn.Query(ctx, countAuthorsSQL) + if err != nil { + return nil, fmt.Errorf("query CountAuthors: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` type FindAuthorByIDRow struct { - AuthorID int32 `json:"author_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Suffix *string `json:"suffix"` + AuthorID int32 `json:"author_id" db:"author_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` } // FindAuthorByID implements Querier.FindAuthorByID. func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAuthorByIDRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindAuthorByID") - row := q.conn.QueryRow(ctx, findAuthorByIDSQL, authorID) - var item FindAuthorByIDRow - if err := row.Scan(&item.AuthorID, &item.FirstName, &item.LastName, &item.Suffix); err != nil { - return item, fmt.Errorf("query FindAuthorByID: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") + rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) + if err != nil { + return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -112,13 +117,13 @@ RETURNING author_id;` // InsertAuthor implements Querier.InsertAuthor. func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName string) (int32, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertAuthor") - row := q.conn.QueryRow(ctx, insertAuthorSQL, firstName, lastName) - var item int32 - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query InsertAuthor: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") + rows, err := q.conn.Query(ctx, insertAuthorSQL, firstName, lastName) + if err != nil { + return 0, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) } const deleteAuthorsByFullNameSQL = `DELETE @@ -129,35 +134,10 @@ WHERE first_name = $1 // DeleteAuthorsByFullName implements Querier.DeleteAuthorsByFullName. func (q *DBQuerier) DeleteAuthorsByFullName(ctx context.Context, firstName string, lastName string, suffix string) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "DeleteAuthorsByFullName") + ctx = context.WithValue(ctx, QueryName{}, "DeleteAuthorsByFullName") cmdTag, err := q.conn.Exec(ctx, deleteAuthorsByFullNameSQL, firstName, lastName, suffix) if err != nil { - return cmdTag, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFullName: %w", err) } return cmdTag, err } - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/ltree/codegen_test.go b/example/ltree/codegen_test.go index 1a49a6a0..689005a8 100644 --- a/example/ltree/codegen_test.go +++ b/example/ltree/codegen_test.go @@ -25,7 +25,7 @@ func TestGenerate_Go_Example_ltree(t *testing.T) { InlineParamCount: 2, TypeOverrides: map[string]string{ "ltree": "github.com/jackc/pgx/v5/pgtype.Text", - "_ltree": "github.com/jackc/pgx/v5/pgtype.TextArray", + "_ltree": "[]string", }, }) if err != nil { diff --git a/example/ltree/query.sql.go b/example/ltree/query.sql.go index 537e2343..ced1be19 100644 --- a/example/ltree/query.sql.go +++ b/example/ltree/query.sql.go @@ -10,11 +10,13 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { FindTopScienceChildren(ctx context.Context) ([]pgtype.Text, error) - FindTopScienceChildrenAgg(ctx context.Context) (pgtype.TextArray, error) + FindTopScienceChildrenAgg(ctx context.Context) ([]string, error) InsertSampleData(ctx context.Context) (pgconn.CommandTag, error) @@ -24,8 +26,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -37,36 +38,41 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const findTopScienceChildrenSQL = `SELECT path @@ -75,24 +81,13 @@ WHERE path <@ 'Top.Science';` // FindTopScienceChildren implements Querier.FindTopScienceChildren. func (q *DBQuerier) FindTopScienceChildren(ctx context.Context) ([]pgtype.Text, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindTopScienceChildren") + ctx = context.WithValue(ctx, QueryName{}, "FindTopScienceChildren") rows, err := q.conn.Query(ctx, findTopScienceChildrenSQL) if err != nil { return nil, fmt.Errorf("query FindTopScienceChildren: %w", err) } - defer rows.Close() - items := []pgtype.Text{} - for rows.Next() { - var item pgtype.Text - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan FindTopScienceChildren row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindTopScienceChildren rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowTo[pgtype.Text]) } const findTopScienceChildrenAggSQL = `SELECT array_agg(path) @@ -100,14 +95,14 @@ FROM test WHERE path <@ 'Top.Science';` // FindTopScienceChildrenAgg implements Querier.FindTopScienceChildrenAgg. -func (q *DBQuerier) FindTopScienceChildrenAgg(ctx context.Context) (pgtype.TextArray, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindTopScienceChildrenAgg") - row := q.conn.QueryRow(ctx, findTopScienceChildrenAggSQL) - var item pgtype.TextArray - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) +func (q *DBQuerier) FindTopScienceChildrenAgg(ctx context.Context) ([]string, error) { + ctx = context.WithValue(ctx, QueryName{}, "FindTopScienceChildrenAgg") + rows, err := q.conn.Query(ctx, findTopScienceChildrenAggSQL) + if err != nil { + return nil, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]string]) } const insertSampleDataSQL = `INSERT INTO test @@ -127,10 +122,10 @@ VALUES ('Top'), // InsertSampleData implements Querier.InsertSampleData. func (q *DBQuerier) InsertSampleData(ctx context.Context) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "InsertSampleData") + ctx = context.WithValue(ctx, QueryName{}, "InsertSampleData") cmdTag, err := q.conn.Exec(ctx, insertSampleDataSQL) if err != nil { - return cmdTag, fmt.Errorf("exec query InsertSampleData: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query InsertSampleData: %w", err) } return cmdTag, err } @@ -147,42 +142,17 @@ const findLtreeInputSQL = `SELECT ($2::text[])::ltree[] AS text_arr;` type FindLtreeInputRow struct { - Ltree pgtype.Text `json:"ltree"` - TextArr pgtype.TextArray `json:"text_arr"` + Ltree pgtype.Text `json:"ltree" db:"ltree"` + TextArr []string `json:"text_arr" db:"text_arr"` } // FindLtreeInput implements Querier.FindLtreeInput. func (q *DBQuerier) FindLtreeInput(ctx context.Context, inLtree pgtype.Text, inLtreeArray []string) (FindLtreeInputRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindLtreeInput") - row := q.conn.QueryRow(ctx, findLtreeInputSQL, inLtree, inLtreeArray) - var item FindLtreeInputRow - if err := row.Scan(&item.Ltree, &item.TextArr); err != nil { - return item, fmt.Errorf("query FindLtreeInput: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindLtreeInput") + rows, err := q.conn.Query(ctx, findLtreeInputSQL, inLtree, inLtreeArray) + if err != nil { + return FindLtreeInputRow{}, fmt.Errorf("query FindLtreeInput: %w", err) } - return item, nil -} -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindLtreeInputRow]) } - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/ltree/query.sql_test.go b/example/ltree/query.sql_test.go index 7c8c28b9..7e2369fe 100644 --- a/example/ltree/query.sql_test.go +++ b/example/ltree/query.sql_test.go @@ -24,10 +24,10 @@ func TestQuerier(t *testing.T) { rows, err := q.FindTopScienceChildren(ctx) require.NoError(t, err) want := []pgtype.Text{ - {String: "Top.Science", Status: pgtype.Present}, - {String: "Top.Science.Astronomy", Status: pgtype.Present}, - {String: "Top.Science.Astronomy.Astrophysics", Status: pgtype.Present}, - {String: "Top.Science.Astronomy.Cosmology", Status: pgtype.Present}, + {String: "Top.Science", Valid: true}, + {String: "Top.Science.Astronomy", Valid: true}, + {String: "Top.Science.Astronomy.Astrophysics", Valid: true}, + {String: "Top.Science.Astronomy.Cosmology", Valid: true}, } assert.Equal(t, want, rows) } @@ -35,42 +35,23 @@ func TestQuerier(t *testing.T) { { rows, err := q.FindTopScienceChildrenAgg(ctx) require.NoError(t, err) - want := pgtype.TextArray{ - Elements: []pgtype.Text{ - {String: "Top.Science", Status: pgtype.Present}, - {String: "Top.Science.Astronomy", Status: pgtype.Present}, - {String: "Top.Science.Astronomy.Astrophysics", Status: pgtype.Present}, - {String: "Top.Science.Astronomy.Cosmology", Status: pgtype.Present}, - }, - Status: pgtype.Present, - Dimensions: []pgtype.ArrayDimension{{Length: 4, LowerBound: 1}}, + want := []string{ + "Top.Science", + "Top.Science.Astronomy", + "Top.Science.Astronomy.Astrophysics", + "Top.Science.Astronomy.Cosmology", } assert.Equal(t, want, rows) } { - in1 := pgtype.Text{String: "foo", Status: pgtype.Present} + in1 := pgtype.Text{String: "foo", Valid: true} in2 := []string{"qux", "qux"} - in2Txt := newTextArray(in2) rows, err := q.FindLtreeInput(ctx, in1, in2) require.NoError(t, err) assert.Equal(t, FindLtreeInputRow{ Ltree: in1, - TextArr: in2Txt, + TextArr: in2, }, rows) } } - -// newTextArray creates a one dimensional text array from the string slice with -// no null elements. -func newTextArray(ss []string) pgtype.TextArray { - elems := make([]pgtype.Text, len(ss)) - for i, s := range ss { - elems[i] = pgtype.Text{String: s, Status: pgtype.Present} - } - return pgtype.TextArray{ - Elements: elems, - Dimensions: []pgtype.ArrayDimension{{Length: int32(len(ss)), LowerBound: 1}}, - Status: pgtype.Present, - } -} diff --git a/example/nested/query.sql.go b/example/nested/query.sql.go index f1bc80bd..5dbc94ad 100644 --- a/example/nested/query.sql.go +++ b/example/nested/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { ArrayNested2(ctx context.Context) ([]ProductImageType, error) @@ -20,8 +21,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -33,146 +33,71 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } // Dimensions represents the Postgres composite type "dimensions". type Dimensions struct { - Width int `json:"width"` - Height int `json:"height"` + Width int `json:"width" db:"width"` + Height int `json:"height" db:"height"` } // ProductImageSetType represents the Postgres composite type "product_image_set_type". type ProductImageSetType struct { - Name string `json:"name"` - OrigImage ProductImageType `json:"orig_image"` - Images []ProductImageType `json:"images"` + Name string `json:"name" db:"name"` + OrigImage ProductImageType `json:"orig_image" db:"orig_image"` + Images []ProductImageType `json:"images" db:"images"` } // ProductImageType represents the Postgres composite type "product_image_type". type ProductImageType struct { - Source string `json:"source"` - Dimensions Dimensions `json:"dimensions"` -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.Codec // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + Source string `json:"source" db:"source"` + Dimensions Dimensions `json:"dimensions" db:"dimensions"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newDimensions creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'dimensions'. -func (tr *typeResolver) newDimensions() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "dimensions", - compositeField{name: "width", typeName: "int4", defaultVal: &pgtype.Int4{}}, - compositeField{name: "height", typeName: "int4", defaultVal: &pgtype.Int4{}}, - ) -} +var _ = addTypeToRegister("\"dimensions\"") -// newProductImageSetType creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'product_image_set_type'. -func (tr *typeResolver) newProductImageSetType() pgtype.ValueTranscoder { - pgtype.CompositeCodec{} - return tr.newCompositeValue( - "product_image_set_type", - compositeField{name: "name", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "orig_image", typeName: "product_image_type", defaultVal: tr.newProductImageType()}, - compositeField{name: "images", typeName: "_product_image_type", defaultVal: tr.newProductImageTypeArray()}, - ) -} +var _ = addTypeToRegister("\"product_image_set_type\"") -// newProductImageType creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'product_image_type'. -func (tr *typeResolver) newProductImageType() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "product_image_type", - compositeField{name: "source", typeName: "text", defaultVal: &pgtype.Text{}}, - compositeField{name: "dimensions", typeName: "dimensions", defaultVal: tr.newDimensions()}, - ) -} +var _ = addTypeToRegister("\"product_image_type\"") -// newProductImageTypeArray creates a new pgtype.ValueTranscoder for the Postgres -// '_product_image_type' array type. -func (tr *typeResolver) newProductImageTypeArray() pgtype.ValueTranscoder { - pgtype.CompositeCodec{} - return tr.newArrayValue("_product_image_type", "product_image_type", tr.newProductImageType) -} +var _ = addTypeToRegister("\"_product_image_type\"") const arrayNested2SQL = `SELECT ARRAY [ @@ -182,17 +107,13 @@ const arrayNested2SQL = `SELECT // ArrayNested2 implements Querier.ArrayNested2. func (q *DBQuerier) ArrayNested2(ctx context.Context) ([]ProductImageType, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "ArrayNested2") - row := q.conn.QueryRow(ctx, arrayNested2SQL) - item := []ProductImageType{} - imagesArray := q.types.newProductImageTypeArray() - if err := row.Scan(imagesArray); err != nil { - return item, fmt.Errorf("query ArrayNested2: %w", err) - } - if err := imagesArray.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ArrayNested2 row: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "ArrayNested2") + rows, err := q.conn.Query(ctx, arrayNested2SQL) + if err != nil { + return nil, fmt.Errorf("query ArrayNested2: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]ProductImageType]) } const nested3SQL = `SELECT @@ -207,51 +128,11 @@ const nested3SQL = `SELECT // Nested3 implements Querier.Nested3. func (q *DBQuerier) Nested3(ctx context.Context) ([]ProductImageSetType, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "Nested3") + ctx = context.WithValue(ctx, QueryName{}, "Nested3") rows, err := q.conn.Query(ctx, nested3SQL) if err != nil { return nil, fmt.Errorf("query Nested3: %w", err) } - defer rows.Close() - items := []ProductImageSetType{} - rowRow := q.types.newProductImageSetType() - for rows.Next() { - var item ProductImageSetType - if err := rows.Scan(rowRow); err != nil { - return nil, fmt.Errorf("scan Nested3 row: %w", err) - } - if err := rowRow.AssignTo(&item); err != nil { - return nil, fmt.Errorf("assign Nested3 row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close Nested3 rows: %w", err) - } - return items, err -} -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string + return pgx.CollectRows(rows, pgx.RowTo[ProductImageSetType]) } - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/pgcrypto/query.sql.go b/example/pgcrypto/query.sql.go index ff478512..3eb98c33 100644 --- a/example/pgcrypto/query.sql.go +++ b/example/pgcrypto/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { CreateUser(ctx context.Context, email string, password string) (pgconn.CommandTag, error) @@ -20,8 +21,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -33,36 +33,41 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} + return &DBQuerier{conn: conn} } -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const createUserSQL = `INSERT INTO "user" (email, pass) @@ -70,10 +75,10 @@ VALUES ($1, crypt($2, gen_salt('bf')));` // CreateUser implements Querier.CreateUser. func (q *DBQuerier) CreateUser(ctx context.Context, email string, password string) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "CreateUser") + ctx = context.WithValue(ctx, QueryName{}, "CreateUser") cmdTag, err := q.conn.Exec(ctx, createUserSQL, email, password) if err != nil { - return cmdTag, fmt.Errorf("exec query CreateUser: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query CreateUser: %w", err) } return cmdTag, err } @@ -82,42 +87,17 @@ const findUserSQL = `SELECT email, pass from "user" where email = $1;` type FindUserRow struct { - Email string `json:"email"` - Pass string `json:"pass"` + Email string `json:"email" db:"email"` + Pass string `json:"pass" db:"pass"` } // FindUser implements Querier.FindUser. func (q *DBQuerier) FindUser(ctx context.Context, email string) (FindUserRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindUser") - row := q.conn.QueryRow(ctx, findUserSQL, email) - var item FindUserRow - if err := row.Scan(&item.Email, &item.Pass); err != nil { - return item, fmt.Errorf("query FindUser: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindUser") + rows, err := q.conn.Query(ctx, findUserSQL, email) + if err != nil { + return FindUserRow{}, fmt.Errorf("query FindUser: %w", err) } - return item, nil -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindUserRow]) } - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/separate_out_dir/out/alpha_query.sql.0.go b/example/separate_out_dir/out/alpha_query.sql.0.go index f99f15dc..2b724fc5 100644 --- a/example/separate_out_dir/out/alpha_query.sql.0.go +++ b/example/separate_out_dir/out/alpha_query.sql.0.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { AlphaNested(ctx context.Context) (string, error) @@ -24,8 +25,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -37,160 +37,76 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } // Alpha represents the Postgres composite type "alpha". type Alpha struct { - Key *string `json:"key"` -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + Key *string `json:"key" db:"key"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newAlpha creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'alpha'. -func (tr *typeResolver) newAlpha() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "alpha", - compositeField{name: "key", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"alpha\"") -// newAlphaArray creates a new pgtype.ValueTranscoder for the Postgres -// '_alpha' array type. -func (tr *typeResolver) newAlphaArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_alpha", "alpha", tr.newAlpha) -} +var _ = addTypeToRegister("\"_alpha\"") const alphaNestedSQL = `SELECT 'alpha_nested' as output;` // AlphaNested implements Querier.AlphaNested. func (q *DBQuerier) AlphaNested(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "AlphaNested") - row := q.conn.QueryRow(ctx, alphaNestedSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query AlphaNested: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "AlphaNested") + rows, err := q.conn.Query(ctx, alphaNestedSQL) + if err != nil { + return "", fmt.Errorf("query AlphaNested: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } const alphaCompositeArraySQL = `SELECT ARRAY[ROW('key')]::alpha[];` // AlphaCompositeArray implements Querier.AlphaCompositeArray. func (q *DBQuerier) AlphaCompositeArray(ctx context.Context) ([]Alpha, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "AlphaCompositeArray") - row := q.conn.QueryRow(ctx, alphaCompositeArraySQL) - item := []Alpha{} - arrayArray := q.types.newAlphaArray() - if err := row.Scan(arrayArray); err != nil { - return item, fmt.Errorf("query AlphaCompositeArray: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "AlphaCompositeArray") + rows, err := q.conn.Query(ctx, alphaCompositeArraySQL) + if err != nil { + return nil, fmt.Errorf("query AlphaCompositeArray: %w", err) } - if err := arrayArray.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign AlphaCompositeArray row: %w", err) - } - return item, nil -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]Alpha]) } - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/separate_out_dir/out/alpha_query.sql.1.go b/example/separate_out_dir/out/alpha_query.sql.1.go index c5b10b7c..ae6dcfed 100644 --- a/example/separate_out_dir/out/alpha_query.sql.1.go +++ b/example/separate_out_dir/out/alpha_query.sql.1.go @@ -5,17 +5,18 @@ package out import ( "context" "fmt" + "github.com/jackc/pgx/v5" ) const alphaSQL = `SELECT 'alpha' as output;` // Alpha implements Querier.Alpha. func (q *DBQuerier) Alpha(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "Alpha") - row := q.conn.QueryRow(ctx, alphaSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query Alpha: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "Alpha") + rows, err := q.conn.Query(ctx, alphaSQL) + if err != nil { + return "", fmt.Errorf("query Alpha: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } diff --git a/example/separate_out_dir/out/bravo_query.sql.go b/example/separate_out_dir/out/bravo_query.sql.go index d3869f04..876f6e9e 100644 --- a/example/separate_out_dir/out/bravo_query.sql.go +++ b/example/separate_out_dir/out/bravo_query.sql.go @@ -5,17 +5,18 @@ package out import ( "context" "fmt" + "github.com/jackc/pgx/v5" ) const bravoSQL = `SELECT 'bravo' as output;` // Bravo implements Querier.Bravo. func (q *DBQuerier) Bravo(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "Bravo") - row := q.conn.QueryRow(ctx, bravoSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query Bravo: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "Bravo") + rows, err := q.conn.Query(ctx, bravoSQL) + if err != nil { + return "", fmt.Errorf("query Bravo: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } diff --git a/example/slices/query.sql.go b/example/slices/query.sql.go index ed0fad12..629503bb 100644 --- a/example/slices/query.sql.go +++ b/example/slices/query.sql.go @@ -7,10 +7,11 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" "time" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { GetBools(ctx context.Context, data []bool) ([]bool, error) @@ -25,8 +26,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -38,123 +38,71 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newboolArrayRaw returns all elements for the Postgres array type '_bool' -// as a slice of interface{} for use with the pgtype.Value Set method. -func (tr *typeResolver) newboolArrayRaw(vs []bool) []interface{} { - elems := make([]interface{}, len(vs)) - for i, v := range vs { - elems[i] = v - } - return elems -} + const getBoolsSQL = `SELECT $1::boolean[];` // GetBools implements Querier.GetBools. func (q *DBQuerier) GetBools(ctx context.Context, data []bool) ([]bool, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetBools") - row := q.conn.QueryRow(ctx, getBoolsSQL, data) - item := []bool{} - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query GetBools: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "GetBools") + rows, err := q.conn.Query(ctx, getBoolsSQL, data) + if err != nil { + return nil, fmt.Errorf("query GetBools: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]bool]) } const getOneTimestampSQL = `SELECT $1::timestamp;` // GetOneTimestamp implements Querier.GetOneTimestamp. func (q *DBQuerier) GetOneTimestamp(ctx context.Context, data *time.Time) (*time.Time, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetOneTimestamp") - row := q.conn.QueryRow(ctx, getOneTimestampSQL, data) - var item *time.Time - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query GetOneTimestamp: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "GetOneTimestamp") + rows, err := q.conn.Query(ctx, getOneTimestampSQL, data) + if err != nil { + return nil, fmt.Errorf("query GetOneTimestamp: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*time.Time]) } const getManyTimestamptzsSQL = `SELECT * @@ -162,24 +110,13 @@ FROM unnest($1::timestamptz[]);` // GetManyTimestamptzs implements Querier.GetManyTimestamptzs. func (q *DBQuerier) GetManyTimestamptzs(ctx context.Context, data []time.Time) ([]*time.Time, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetManyTimestamptzs") + ctx = context.WithValue(ctx, QueryName{}, "GetManyTimestamptzs") rows, err := q.conn.Query(ctx, getManyTimestamptzsSQL, data) if err != nil { return nil, fmt.Errorf("query GetManyTimestamptzs: %w", err) } - defer rows.Close() - items := []*time.Time{} - for rows.Next() { - var item time.Time - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan GetManyTimestamptzs row: %w", err) - } - items = append(items, &item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetManyTimestamptzs rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowTo[*time.Time]) } const getManyTimestampsSQL = `SELECT * @@ -187,47 +124,11 @@ FROM unnest($1::timestamp[]);` // GetManyTimestamps implements Querier.GetManyTimestamps. func (q *DBQuerier) GetManyTimestamps(ctx context.Context, data []*time.Time) ([]*time.Time, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GetManyTimestamps") + ctx = context.WithValue(ctx, QueryName{}, "GetManyTimestamps") rows, err := q.conn.Query(ctx, getManyTimestampsSQL, data) if err != nil { return nil, fmt.Errorf("query GetManyTimestamps: %w", err) } - defer rows.Close() - items := []*time.Time{} - for rows.Next() { - var item time.Time - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan GetManyTimestamps row: %w", err) - } - items = append(items, &item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetManyTimestamps rows: %w", err) - } - return items, err -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} + return pgx.CollectRows(rows, pgx.RowTo[*time.Time]) } - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/syntax/query.sql.go b/example/syntax/query.sql.go index 85bf0661..f085f89f 100644 --- a/example/syntax/query.sql.go +++ b/example/syntax/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { // Query to test escaping in generated Go. @@ -42,8 +43,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -55,7 +55,7 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} + return &DBQuerier{conn: conn} } // UnnamedEnum123 represents the Postgres enum "123". @@ -70,178 +70,160 @@ const ( func (u UnnamedEnum123) String() string { return string(u) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} +var typesToRegister = []string{} -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var _ = addTypeToRegister("\"123\"") const backtickSQL = "SELECT '`';" // Backtick implements Querier.Backtick. func (q *DBQuerier) Backtick(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "Backtick") - row := q.conn.QueryRow(ctx, backtickSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query Backtick: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "Backtick") + rows, err := q.conn.Query(ctx, backtickSQL) + if err != nil { + return "", fmt.Errorf("query Backtick: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } const backtickQuoteBacktickSQL = "SELECT '`\"`';" // BacktickQuoteBacktick implements Querier.BacktickQuoteBacktick. func (q *DBQuerier) BacktickQuoteBacktick(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "BacktickQuoteBacktick") - row := q.conn.QueryRow(ctx, backtickQuoteBacktickSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query BacktickQuoteBacktick: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "BacktickQuoteBacktick") + rows, err := q.conn.Query(ctx, backtickQuoteBacktickSQL) + if err != nil { + return "", fmt.Errorf("query BacktickQuoteBacktick: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } const backtickNewlineSQL = "SELECT '`\n';" // BacktickNewline implements Querier.BacktickNewline. func (q *DBQuerier) BacktickNewline(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "BacktickNewline") - row := q.conn.QueryRow(ctx, backtickNewlineSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query BacktickNewline: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "BacktickNewline") + rows, err := q.conn.Query(ctx, backtickNewlineSQL) + if err != nil { + return "", fmt.Errorf("query BacktickNewline: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } const backtickDoubleQuoteSQL = "SELECT '`\"';" // BacktickDoubleQuote implements Querier.BacktickDoubleQuote. func (q *DBQuerier) BacktickDoubleQuote(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "BacktickDoubleQuote") - row := q.conn.QueryRow(ctx, backtickDoubleQuoteSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query BacktickDoubleQuote: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "BacktickDoubleQuote") + rows, err := q.conn.Query(ctx, backtickDoubleQuoteSQL) + if err != nil { + return "", fmt.Errorf("query BacktickDoubleQuote: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } const backtickBackslashNSQL = "SELECT '`\\n';" // BacktickBackslashN implements Querier.BacktickBackslashN. func (q *DBQuerier) BacktickBackslashN(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "BacktickBackslashN") - row := q.conn.QueryRow(ctx, backtickBackslashNSQL) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query BacktickBackslashN: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "BacktickBackslashN") + rows, err := q.conn.Query(ctx, backtickBackslashNSQL) + if err != nil { + return "", fmt.Errorf("query BacktickBackslashN: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } const illegalNameSymbolsSQL = "SELECT '`\\n' as \"$\", $1 as \"foo.bar!@#$%&*()\"\"--+\";" type IllegalNameSymbolsRow struct { - UnnamedColumn0 string `json:"$"` - FooBar string `json:"foo.bar!@#$%&*()\"--+"` + UnnamedColumn0 string `json:"$" db:"$"` + FooBar string `json:"foo.bar!@#$%&*()\"--+" db:"foo.bar!@#$%&*()\"--+"` } // IllegalNameSymbols implements Querier.IllegalNameSymbols. func (q *DBQuerier) IllegalNameSymbols(ctx context.Context, helloWorld string) (IllegalNameSymbolsRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "IllegalNameSymbols") - row := q.conn.QueryRow(ctx, illegalNameSymbolsSQL, helloWorld) - var item IllegalNameSymbolsRow - if err := row.Scan(&item.UnnamedColumn0, &item.FooBar); err != nil { - return item, fmt.Errorf("query IllegalNameSymbols: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "IllegalNameSymbols") + rows, err := q.conn.Query(ctx, illegalNameSymbolsSQL, helloWorld) + if err != nil { + return IllegalNameSymbolsRow{}, fmt.Errorf("query IllegalNameSymbols: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[IllegalNameSymbolsRow]) } const spaceAfterSQL = `SELECT $1;` // SpaceAfter implements Querier.SpaceAfter. func (q *DBQuerier) SpaceAfter(ctx context.Context, space string) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "SpaceAfter") - row := q.conn.QueryRow(ctx, spaceAfterSQL, space) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query SpaceAfter: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "SpaceAfter") + rows, err := q.conn.Query(ctx, spaceAfterSQL, space) + if err != nil { + return "", fmt.Errorf("query SpaceAfter: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } const badEnumNameSQL = `SELECT 'inconvertible_enum_name'::"123";` // BadEnumName implements Querier.BadEnumName. func (q *DBQuerier) BadEnumName(ctx context.Context) (UnnamedEnum123, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "BadEnumName") - row := q.conn.QueryRow(ctx, badEnumNameSQL) - var item UnnamedEnum123 - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query BadEnumName: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "BadEnumName") + rows, err := q.conn.Query(ctx, badEnumNameSQL) + if err != nil { + return "", fmt.Errorf("query BadEnumName: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[UnnamedEnum123]) } const goKeywordSQL = `SELECT $1::text;` // GoKeyword implements Querier.GoKeyword. func (q *DBQuerier) GoKeyword(ctx context.Context, go_ string) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "GoKeyword") - row := q.conn.QueryRow(ctx, goKeywordSQL, go_) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query GoKeyword: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "GoKeyword") + rows, err := q.conn.Query(ctx, goKeywordSQL, go_) + if err != nil { + return "", fmt.Errorf("query GoKeyword: %w", err) } - return item, nil -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} - -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } - -func (t textPreferrer) TypeName() string { - return t.typeName -} - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/example/void/query.sql.go b/example/void/query.sql.go index 146b3cf1..94790852 100644 --- a/example/void/query.sql.go +++ b/example/void/query.sql.go @@ -7,9 +7,10 @@ import ( "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" - "github.com/jackc/pgx/v5/pgtype" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { VoidOnly(ctx context.Context) (pgconn.CommandTag, error) @@ -26,8 +27,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use - types *typeResolver // resolve types by name + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -39,46 +39,51 @@ type genericConn interface { // NewQuerier creates a DBQuerier that implements Querier. func NewQuerier(conn genericConn) *DBQuerier { - return &DBQuerier{conn: conn, types: newTypeResolver()} -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + return &DBQuerier{conn: conn} +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } const voidOnlySQL = `SELECT void_fn();` // VoidOnly implements Querier.VoidOnly. func (q *DBQuerier) VoidOnly(ctx context.Context) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "VoidOnly") + ctx = context.WithValue(ctx, QueryName{}, "VoidOnly") cmdTag, err := q.conn.Exec(ctx, voidOnlySQL) if err != nil { - return cmdTag, fmt.Errorf("exec query VoidOnly: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query VoidOnly: %w", err) } return cmdTag, err } @@ -87,10 +92,10 @@ const voidOnlyTwoParamsSQL = `SELECT void_fn_two_params($1, 'text');` // VoidOnlyTwoParams implements Querier.VoidOnlyTwoParams. func (q *DBQuerier) VoidOnlyTwoParams(ctx context.Context, id int32) (pgconn.CommandTag, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "VoidOnlyTwoParams") + ctx = context.WithValue(ctx, QueryName{}, "VoidOnlyTwoParams") cmdTag, err := q.conn.Exec(ctx, voidOnlyTwoParamsSQL, id) if err != nil { - return cmdTag, fmt.Errorf("exec query VoidOnlyTwoParams: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query VoidOnlyTwoParams: %w", err) } return cmdTag, err } @@ -99,78 +104,60 @@ const voidTwoSQL = `SELECT void_fn(), 'foo' as name;` // VoidTwo implements Querier.VoidTwo. func (q *DBQuerier) VoidTwo(ctx context.Context) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "VoidTwo") - row := q.conn.QueryRow(ctx, voidTwoSQL) + ctx = context.WithValue(ctx, QueryName{}, "VoidTwo") + rows, err := q.conn.Query(ctx, voidTwoSQL) + if err != nil { + return "", fmt.Errorf("query VoidTwo: %w", err) + } + + return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (string, error) { var item string if err := row.Scan(nil, &item); err != nil { - return item, fmt.Errorf("query VoidTwo: %w", err) + return item, err } return item, nil +}) } const voidThreeSQL = `SELECT void_fn(), 'foo' as foo, 'bar' as bar;` type VoidThreeRow struct { - Foo string `json:"foo"` - Bar string `json:"bar"` + Foo string `json:"foo" db:"foo"` + Bar string `json:"bar" db:"bar"` } // VoidThree implements Querier.VoidThree. func (q *DBQuerier) VoidThree(ctx context.Context) (VoidThreeRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "VoidThree") - row := q.conn.QueryRow(ctx, voidThreeSQL) + ctx = context.WithValue(ctx, QueryName{}, "VoidThree") + rows, err := q.conn.Query(ctx, voidThreeSQL) + if err != nil { + return VoidThreeRow{}, fmt.Errorf("query VoidThree: %w", err) + } + + return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (VoidThreeRow, error) { var item VoidThreeRow if err := row.Scan(nil, &item.Foo, &item.Bar); err != nil { - return item, fmt.Errorf("query VoidThree: %w", err) + return item, err } return item, nil +}) } const voidThree2SQL = `SELECT 'foo' as foo, void_fn(), void_fn();` // VoidThree2 implements Querier.VoidThree2. func (q *DBQuerier) VoidThree2(ctx context.Context) ([]string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "VoidThree2") + ctx = context.WithValue(ctx, QueryName{}, "VoidThree2") rows, err := q.conn.Query(ctx, voidThree2SQL) if err != nil { return nil, fmt.Errorf("query VoidThree2: %w", err) } - defer rows.Close() - items := []string{} - for rows.Next() { - var item string - if err := rows.Scan(&item, nil, nil); err != nil { - return nil, fmt.Errorf("scan VoidThree2 row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close VoidThree2 rows: %w", err) - } - return items, err -} - -// textPreferrer wraps a pgtype.ValueTranscoder and sets the preferred encoding -// format to text instead binary (the default). pggen uses the text format -// when the OID is unknownOID because the binary format requires the OID. -// Typically occurs for unregistered types. -type textPreferrer struct { - pgtype.ValueTranscoder - typeName string -} -// PreferredParamFormat implements pgtype.ParamFormatPreferrer. -func (t textPreferrer) PreferredParamFormat() int16 { return pgtype.TextFormatCode } - -func (t textPreferrer) NewTypeValue() pgtype.Value { - return textPreferrer{ValueTranscoder: pgtype.NewValue(t.ValueTranscoder).(pgtype.ValueTranscoder), typeName: t.typeName} -} - -func (t textPreferrer) TypeName() string { - return t.typeName + return pgx.CollectRows(rows, func(row pgx.CollectableRow) (string, error) { + var item string + if err := row.Scan(&item, nil, nil); err != nil { + return item, err + } + return item, nil +}) } - -// unknownOID means we don't know the OID for a type. This is okay for decoding -// because pgx call DecodeText or DecodeBinary without requiring the OID. For -// encoding parameters, pggen uses textPreferrer if the OID is unknown. -const unknownOID = 0 diff --git a/internal/codegen/golang/declarer.go b/internal/codegen/golang/declarer.go index aa1347a1..e35ed33e 100644 --- a/internal/codegen/golang/declarer.go +++ b/internal/codegen/golang/declarer.go @@ -2,6 +2,7 @@ package golang import ( "sort" + "strings" "github.com/jschaf/pggen/internal/codegen/golang/gotype" ) @@ -43,6 +44,10 @@ func (d DeclarerSet) ListAll() []Declarer { return decls } +func pgTypeNameForLoadType(name string) string { + return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` +} + // FindInputDeclarers finds all necessary Declarers for types that appear in // the input parameters. Returns nil if no declarers are needed. func FindInputDeclarers(typ gotype.Type) DeclarerSet { @@ -70,7 +75,7 @@ func FindInputDeclarers(typ gotype.Type) DeclarerSet { decls.AddAll(NewTypeResolverInitDeclarer()) // always add findInputDeclsHelper(typ, decls) // Inputs depend on output transcoders. - findOutputDeclsHelper(typ, decls /*hadCompositeParent*/, false) + findOutputDeclsHelper(typ, decls) return decls } @@ -103,21 +108,17 @@ func findInputDeclsHelper(typ gotype.Type, decls DeclarerSet) { func FindOutputDeclarers(typ gotype.Type) DeclarerSet { decls := NewDeclarerSet() decls.AddAll(NewTypeResolverInitDeclarer()) // always add - findOutputDeclsHelper(typ, decls, false) + findOutputDeclsHelper(typ, decls) return decls } -func findOutputDeclsHelper(typ gotype.Type, decls DeclarerSet, hadCompositeParent bool) { +func findOutputDeclsHelper(typ gotype.Type, decls DeclarerSet) { switch typ := gotype.UnwrapNestedType(typ).(type) { case *gotype.EnumType: decls.AddAll( NewEnumTypeDeclarer(typ), + NewEnumTranscoderDeclarer(typ), ) - if hadCompositeParent { - // We can use a string as the decoder except if the enum is part of a - // composite type. - decls.AddAll(NewEnumTranscoderDeclarer(typ)) - } case *gotype.CompositeType: decls.AddAll( @@ -126,7 +127,7 @@ func findOutputDeclsHelper(typ gotype.Type, decls DeclarerSet, hadCompositeParen NewTypeResolverDeclarer(), ) for _, childType := range typ.FieldTypes { - findOutputDeclsHelper(childType, decls, true) + findOutputDeclsHelper(childType, decls) } case *gotype.ArrayType: @@ -138,7 +139,7 @@ func findOutputDeclsHelper(typ gotype.Type, decls DeclarerSet, hadCompositeParen case *gotype.CompositeType, *gotype.EnumType: decls.AddAll(NewArrayDecoderDeclarer(typ)) } - findOutputDeclsHelper(typ.Elem, decls, hadCompositeParent) + findOutputDeclsHelper(typ.Elem, decls) default: return @@ -158,33 +159,38 @@ func NewConstantDeclarer(key, str string) ConstantDeclarer { func (c ConstantDeclarer) DedupeKey() string { return c.key } func (c ConstantDeclarer) Declare(string) (string, error) { return c.str, nil } -const typeResolverInitDecl = `// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false +const typeResolverInitDecl = `// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} }` // NewTypeResolverInitDeclarer declare type resolver init code always needed. @@ -192,56 +198,7 @@ func NewTypeResolverInitDeclarer() ConstantDeclarer { return NewConstantDeclarer("type_resolver::00_common", typeResolverInitDecl) } -const typeResolverBodyDecl = `type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use -} - -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} - -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -}` +const typeResolverBodyDecl = `` // NewTypeResolverDeclarer declares type resolver body code sometimes needed. func NewTypeResolverDeclarer() ConstantDeclarer { diff --git a/internal/codegen/golang/declarer_array.go b/internal/codegen/golang/declarer_array.go index bb9a58a3..1aac1ab0 100644 --- a/internal/codegen/golang/declarer_array.go +++ b/internal/codegen/golang/declarer_array.go @@ -1,24 +1,18 @@ package golang import ( - "fmt" "strconv" "strings" "github.com/jschaf/pggen/internal/codegen/golang/gotype" ) -// NameArrayTranscoderFunc returns the function name that creates a -// pgtype.ValueTranscoder for the array type that's used to decode rows returned -// by Postgres. +// NameArrayTranscoderFunc returns the legacy array helper name. func NameArrayTranscoderFunc(typ *gotype.ArrayType) string { return "new" + typ.Elem.BaseName() + "Array" } -// NameArrayInitFunc returns the name for the function that creates an -// initialized pgtype.ValueTranscoder for the array type that's used to encode -// query parameters. This function is only necessary for top-level types. -// Descendant types use the raw functions, named by NameArrayRawFunc. +// NameArrayInitFunc returns the legacy array input helper name. func NameArrayInitFunc(typ *gotype.ArrayType) string { elem := typ.Elem if t, ok := elem.(*gotype.ImportType); ok { @@ -36,9 +30,7 @@ func NameArrayInitFunc(typ *gotype.ArrayType) string { } } -// NameArrayRawFunc returns the function name that create the []interface{} -// array for the array type so that we can use it with a parent encoder -// function, like NameCompositeInitFunc, in the pgtype.Value Set call. +// NameArrayRawFunc returns the legacy raw array helper name. func NameArrayRawFunc(typ *gotype.ArrayType) string { elem := typ.Elem if t, ok := elem.(*gotype.ImportType); ok { @@ -56,8 +48,7 @@ func NameArrayRawFunc(typ *gotype.ArrayType) string { } } -// ArrayTranscoderDeclarer declares a new Go function that creates a -// pgtype.ValueTranscoder decoder for an array Postgres type. +// ArrayTranscoderDeclarer declares a PostgreSQL array type registration. type ArrayTranscoderDeclarer struct { typ *gotype.ArrayType } @@ -72,57 +63,14 @@ func (a ArrayTranscoderDeclarer) DedupeKey() string { func (a ArrayTranscoderDeclarer) Declare(string) (string, error) { sb := &strings.Builder{} - funcName := NameArrayTranscoderFunc(a.typ) - - // Doc comment - sb.WriteString("// ") - sb.WriteString(funcName) - sb.WriteString(" creates a new pgtype.ValueTranscoder for the Postgres\n") - sb.WriteString("// '") - sb.WriteString(a.typ.PgArray.Name) - sb.WriteString("' array type.\n") - - // Function signature - sb.WriteString("func (tr *typeResolver) ") - sb.WriteString(funcName) - sb.WriteString("() pgtype.ValueTranscoder {\n\t") - - // newArrayValue call - sb.WriteString("return tr.newArrayValue(") - sb.WriteString(strconv.Quote(a.typ.PgArray.Name)) - sb.WriteString(", ") - sb.WriteString(strconv.Quote(a.typ.PgArray.Elem.String())) - sb.WriteString(", ") - - // Default element transcoder - switch elem := gotype.UnwrapNestedType(a.typ.Elem).(type) { - case *gotype.CompositeType: - sb.WriteString("tr.") - sb.WriteString(NameCompositeTranscoderFunc(elem)) - case *gotype.EnumType: - sb.WriteString(NameEnumTranscoderFunc(elem)) - default: - return "", fmt.Errorf("array composite decoder only supports composite and enum elems; got %T", a.typ.Elem) - } + sb.WriteString("var _ = addTypeToRegister(") + sb.WriteString(strconv.Quote(pgTypeNameForLoadType(a.typ.PgArray.Name))) sb.WriteString(")") - sb.WriteString("\n") - sb.WriteString("}") - return sb.String(), nil } -// ArrayInitDeclarer declares a new Go function that creates an *initialized* -// pgtype.ValueTranscoder for the Postgres type represented by the -// gotype.ArrayType. -// -// We need a separate declarer from ArrayTranscoderDeclarer because setting a -// pgtype.ValueTranscoder is much less flexible on the values allowed compared -// to AssignTo. We can assign a pgtype.ArrayType to any struct but we can only -// set it with an [][]interface{} if the array elements are composite types. -// -// Additionally, we need to use the Postgres text format exclusively because the -// Postgres binary format requires the type OID but pggen doesn't necessarily -// know the OIDs of the types. The text format, however, doesn't require OIDs. +// ArrayInitDeclarer is retained for the old declarer flow. pgx v5 encodes +// registered array types directly. type ArrayInitDeclarer struct { typ *gotype.ArrayType } @@ -136,45 +84,10 @@ func (a ArrayInitDeclarer) DedupeKey() string { } func (a ArrayInitDeclarer) Declare(string) (string, error) { - funcName := NameArrayInitFunc(a.typ) - sb := &strings.Builder{} - sb.Grow(256) - - // Doc comment - sb.WriteString("// ") - sb.WriteString(funcName) - sb.WriteString(" creates an initialized pgtype.ValueTranscoder for the\n") - sb.WriteString("// Postgres array type '") - sb.WriteString(a.typ.PgArray.Name) - sb.WriteString("' to encode query parameters.\n") - - // Function signature - sb.WriteString("func (tr *typeResolver) ") - sb.WriteString(funcName) - sb.WriteString("(ps ") - sb.WriteString(a.typ.BaseName()) - sb.WriteString(") pgtype.ValueTranscoder {\n\t") - - // Function body - sb.WriteString("dec := tr.") - sb.WriteString(NameArrayTranscoderFunc(a.typ)) - sb.WriteString("()\n\t") - sb.WriteString("if err := dec.Set(tr.") - sb.WriteString(NameArrayRawFunc(a.typ)) - sb.WriteString("(ps)); err != nil {\n\t\t") - sb.WriteString(fmt.Sprintf(`panic("encode %s: " + err.Error())`, a.typ.BaseName())) - sb.WriteString(" // should always succeed\n\t") - sb.WriteString("}\n\t") - sb.WriteString("return textPreferrer{ValueTranscoder: dec, typeName: ") - sb.WriteString(strconv.Quote(a.typ.PgArray.Name)) - sb.WriteString("}\n") - sb.WriteString("}") - return sb.String(), nil + return "", nil } -// ArrayRawDeclarer declares a new Go function that returns all fields -// as a generic array: []interface{}. Necessary because we can only set -// pgtype.ArrayType from a []interface{}. +// ArrayRawDeclarer is retained for the old declarer flow. type ArrayRawDeclarer struct { typ *gotype.ArrayType } @@ -188,39 +101,5 @@ func (a ArrayRawDeclarer) DedupeKey() string { } func (a ArrayRawDeclarer) Declare(pkgPath string) (string, error) { - funcName := NameArrayRawFunc(a.typ) - sb := &strings.Builder{} - sb.Grow(256) - - // Doc comment - sb.WriteString("// ") - sb.WriteString(funcName) - sb.WriteString(" returns all elements for the Postgres array type '") - sb.WriteString(a.typ.PgArray.Name) - sb.WriteString("'\n// as a slice of interface{} for use with the pgtype.Value Set method.\n") - - // Function signature - sb.WriteString("func (tr *typeResolver) ") - sb.WriteString(funcName) - sb.WriteString("(vs ") - sb.WriteString(gotype.QualifyType(a.typ, pkgPath)) - sb.WriteString(") []interface{} {\n\t") - - // Function body - sb.WriteString("elems := make([]interface{}, len(vs))\n\t") - sb.WriteString("for i, v := range vs {\n\t\t") - sb.WriteString("elems[i] = ") - switch elem := gotype.UnwrapNestedType(a.typ.Elem).(type) { - case *gotype.CompositeType: - sb.WriteString("tr.") - sb.WriteString(NameCompositeRawFunc(elem)) - sb.WriteString("(v)") - default: - sb.WriteString("v") - } - sb.WriteString("\n\t") - sb.WriteString("}\n\t") - sb.WriteString("return elems\n") - sb.WriteString("}") - return sb.String(), nil + return "", nil } diff --git a/internal/codegen/golang/declarer_composite.go b/internal/codegen/golang/declarer_composite.go index a2c2a34c..409308a2 100644 --- a/internal/codegen/golang/declarer_composite.go +++ b/internal/codegen/golang/declarer_composite.go @@ -5,28 +5,19 @@ import ( "strings" "github.com/jschaf/pggen/internal/codegen/golang/gotype" - "github.com/jschaf/pggen/internal/pg" ) -// NameCompositeTranscoderFunc returns the function name that creates a -// pgtype.ValueTranscoder for the composite type that's used to decode rows -// returned by Postgres. +// NameCompositeTranscoderFunc returns the legacy composite helper name. func NameCompositeTranscoderFunc(typ *gotype.CompositeType) string { return "new" + typ.Name } -// NameCompositeInitFunc returns the name of the function that creates an -// initialized pgtype.ValueTranscoder for the composite type used as a query -// parameters. This function is only necessary for top-level types. Descendant -// types use the raw functions, named by NameCompositeRawFunc. +// NameCompositeInitFunc returns the legacy composite input helper name. func NameCompositeInitFunc(typ *gotype.CompositeType) string { return "new" + typ.Name + "Init" } -// NameCompositeRawFunc returns the function name that creates the -// []interface{} array for the composite type so that we can use it with a -// parent encoder function, like NameCompositeInitFunc, in the pgtype.Value -// Set call. +// NameCompositeRawFunc returns the legacy raw composite helper name. func NameCompositeRawFunc(typ *gotype.CompositeType) string { return "new" + typ.Name + "Raw" } @@ -78,6 +69,8 @@ func (c CompositeTypeDeclarer) Declare(pkgPath string) (string, error) { sb.WriteString(strings.Repeat(" ", typeLen-len(qualType))) sb.WriteString("`json:") sb.WriteString(strconv.Quote(c.comp.PgComposite.ColumnNames[i])) + sb.WriteString(" db:") + sb.WriteString(strconv.Quote(c.comp.PgComposite.ColumnNames[i])) sb.WriteString("`") sb.WriteRune('\n') } @@ -122,100 +115,16 @@ func (c CompositeTranscoderDeclarer) DedupeKey() string { } func (c CompositeTranscoderDeclarer) Declare(pkgPath string) (string, error) { - funcName := NameCompositeTranscoderFunc(c.typ) sb := &strings.Builder{} sb.Grow(256) - - // Doc comment - sb.WriteString("// ") - sb.WriteString(funcName) - sb.WriteString(" creates a new pgtype.ValueTranscoder for the Postgres\n") - sb.WriteString("// composite type '") - sb.WriteString(c.typ.PgComposite.Name) - sb.WriteString("'.\n") - - // Function signature - sb.WriteString("func (tr *typeResolver) ") - sb.WriteString(funcName) - sb.WriteString("() pgtype.ValueTranscoder {\n\t") - - // newCompositeValue call - sb.WriteString("return tr.newCompositeValue(\n\t\t") - sb.WriteString(strconv.Quote(c.typ.PgComposite.Name)) - sb.WriteString(",") - - // newCompositeValue - field names of the composite type - for i := range c.typ.FieldNames { - sb.WriteString("\n\t\t") - sb.WriteString(`compositeField{name: `) - sb.WriteString(strconv.Quote(c.typ.PgComposite.ColumnNames[i])) // field name - sb.WriteString(", typeName: ") - sb.WriteString(strconv.Quote(c.typ.PgComposite.ColumnTypes[i].String())) // field type name - sb.WriteString(", defaultVal: ") - - // field default pgtype.ValueTranscoder - switch fieldType := gotype.UnwrapNestedType(c.typ.FieldTypes[i]).(type) { - case *gotype.CompositeType: - childFuncName := NameCompositeTranscoderFunc(fieldType) - sb.WriteString("tr.") - sb.WriteString(childFuncName) - sb.WriteString("()") - case *gotype.EnumType: - sb.WriteString(NameEnumTranscoderFunc(fieldType)) - sb.WriteString("()") - case *gotype.ArrayType: - if typ, ok := gotype.FindKnownTypePgx(fieldType.PgArray.OID()); ok { - sb.WriteString("&") // pgx needs pointers to types - sb.WriteString(gotype.QualifyType(typ, pkgPath)) - sb.WriteString("{}") - break - } - sb.WriteString("tr.") - sb.WriteString(NameArrayTranscoderFunc(fieldType)) - sb.WriteString("()") - case *gotype.VoidType: - // skip - default: - sb.WriteString("&") // pgx needs pointers to types - pgType := c.typ.PgComposite.ColumnTypes[i] - if pgType == nil || pgType == (pg.VoidType{}) { - sb.WriteString("nil,") - } else { - var qualType string - if decoderType, ok := gotype.FindKnownTypePgx(pgType.OID()); ok { - // Look for the pgx variant it matches the interface expected by - // newCompositeValue, pgtype.ValueTranscoder. - qualType = gotype.QualifyType(decoderType, pkgPath) - } else { - // Attempt to use the original, provided type. - qualType = gotype.QualifyType(c.typ.FieldTypes[i], pkgPath) - } - sb.WriteString(qualType) - sb.WriteString("{}") - } - } - sb.WriteString(`},`) - } - - sb.WriteString("\n\t") + sb.WriteString("var _ = addTypeToRegister(") + sb.WriteString(strconv.Quote(pgTypeNameForLoadType(c.typ.PgComposite.Name))) sb.WriteString(")") - sb.WriteString("\n") - sb.WriteString("}") return sb.String(), nil } -// CompositeInitDeclarer declares a new Go function that creates an initialized -// pgtype.ValueTranscoder for the Postgres type represented by the -// gotype.CompositeType. -// -// We need a separate encoder because setting a pgtype.ValueTranscoder is much -// less flexible on the values allowed compared to AssignTo. We can assign a -// pgtype.CompositeType to any struct but we can only set it with an -// []interface{}. -// -// Additionally, we need to use the Postgres text format exclusively because the -// Postgres binary format requires the type OID but pggen doesn't necessarily -// know the OIDs of the types. The text format, however, doesn't require OIDs. +// CompositeInitDeclarer is retained for the old declarer flow. pgx v5 encodes +// registered composite types directly. type CompositeInitDeclarer struct { typ *gotype.CompositeType } @@ -229,41 +138,10 @@ func (c CompositeInitDeclarer) DedupeKey() string { } func (c CompositeInitDeclarer) Declare(string) (string, error) { - funcName := NameCompositeInitFunc(c.typ) - sb := &strings.Builder{} - sb.Grow(256) - - // Doc comment - sb.WriteString("// ") - sb.WriteString(funcName) - sb.WriteString(" creates an initialized pgtype.ValueTranscoder for the\n") - sb.WriteString("// Postgres composite type '") - sb.WriteString(c.typ.PgComposite.Name) - sb.WriteString("' to encode query parameters.\n") - - // Function signature - sb.WriteString("func (tr *typeResolver) ") - sb.WriteString(funcName) - sb.WriteString("(v ") - sb.WriteString(c.typ.Name) - sb.WriteString(") pgtype.ValueTranscoder {\n\t") - - // Function body - sb.WriteString("return tr.setValue(tr.") - sb.WriteString(NameCompositeTranscoderFunc(c.typ)) - sb.WriteString("(), tr.") - sb.WriteString(NameCompositeRawFunc(c.typ)) - sb.WriteString("(v))\n") - sb.WriteString("}") - return sb.String(), nil + return "", nil } -// CompositeRawDeclarer declares a new Go function that returns all fields -// of a composite type as a generic array: []interface{}. Necessary because we -// can only set pgtype.CompositeType from a []interface{}. -// -// Revisit after https://github.com/jackc/pgx/v5/pgtype/pull/100 to see if we can -// simplify. +// CompositeRawDeclarer is retained for the old declarer flow. type CompositeRawDeclarer struct { typ *gotype.CompositeType } @@ -277,62 +155,5 @@ func (c CompositeRawDeclarer) DedupeKey() string { } func (c CompositeRawDeclarer) Declare(string) (string, error) { - funcName := NameCompositeRawFunc(c.typ) - sb := &strings.Builder{} - sb.Grow(256) - - // Doc comment - sb.WriteString("// ") - sb.WriteString(funcName) - sb.WriteString(" returns all composite fields for the Postgres composite\n") - sb.WriteString("// type '") - sb.WriteString(c.typ.PgComposite.Name) - sb.WriteString("' as a slice of interface{} to encode query parameters.\n") - - // Function signature - sb.WriteString("func (tr *typeResolver) ") - sb.WriteString(funcName) - sb.WriteString("(v ") - sb.WriteString(c.typ.Name) - sb.WriteString(") []interface{} {\n\t") - - // Function body - sb.WriteString("return []interface{}{") - - // Field Assigners of the composite type - for i, fieldType := range c.typ.FieldTypes { - fieldName := c.typ.FieldNames[i] - sb.WriteString("\n\t\t") - switch fieldType := gotype.UnwrapNestedType(fieldType).(type) { - case *gotype.CompositeType: - childFuncName := NameCompositeRawFunc(fieldType) - sb.WriteString("tr.") - sb.WriteString(childFuncName) - sb.WriteString("(v.") - sb.WriteString(fieldName) - sb.WriteString(")") - case *gotype.ArrayType: - if _, ok := gotype.FindKnownTypePgx(fieldType.PgArray.OID()); ok { - sb.WriteString("v.") - sb.WriteString(fieldName) - break - } - sb.WriteString("tr.") - sb.WriteString(NameArrayRawFunc(fieldType)) - sb.WriteString("(v.") - sb.WriteString(fieldName) - sb.WriteString(")") - case *gotype.VoidType: - sb.WriteString("nil") - default: - sb.WriteString("v.") - sb.WriteString(fieldName) - } - sb.WriteString(",") - } - sb.WriteString("\n\t") - sb.WriteString("}") - sb.WriteString("\n") - sb.WriteString("}") - return sb.String(), nil + return "", nil } diff --git a/internal/codegen/golang/declarer_enum.go b/internal/codegen/golang/declarer_enum.go index 268b13e1..f1437340 100644 --- a/internal/codegen/golang/declarer_enum.go +++ b/internal/codegen/golang/declarer_enum.go @@ -80,43 +80,13 @@ func NewEnumTranscoderDeclarer(enum *gotype.EnumType) EnumTranscoderDeclarer { } func (e EnumTranscoderDeclarer) DedupeKey() string { - return "enum_decoder::" + e.typ.Name + return "type_resolver::" + e.typ.Name + "_01_transcoder" } func (e EnumTranscoderDeclarer) Declare(string) (string, error) { sb := &strings.Builder{} - funcName := NameEnumTranscoderFunc(e.typ) - - // Doc comment - sb.WriteString("// ") - sb.WriteString(funcName) - sb.WriteString(" creates a new pgtype.ValueTranscoder for the\n") - sb.WriteString("// Postgres enum type '") - sb.WriteString(e.typ.PgEnum.Name) - sb.WriteString("'.\n") - - // Function signature - sb.WriteString("func ") - sb.WriteString(funcName) - sb.WriteString("() pgtype.ValueTranscoder {\n\t") - - // NewEnumType call - sb.WriteString("return pgtype.NewEnumType(\n\t\t") - sb.WriteString(strconv.Quote(e.typ.PgEnum.Name)) - sb.WriteString(",\n\t\t") - sb.WriteString(`[]string{`) - for _, label := range e.typ.Labels { - sb.WriteString("\n\t\t\t") - sb.WriteString("string(") - sb.WriteString(label) - sb.WriteString("),") - } - sb.WriteString("\n\t\t") - sb.WriteString("},") - sb.WriteString("\n\t") + sb.WriteString("var _ = addTypeToRegister(") + sb.WriteString(strconv.Quote(pgTypeNameForLoadType(e.typ.PgEnum.Name))) sb.WriteString(")") - sb.WriteString("\n") - sb.WriteString("}") - return sb.String(), nil } diff --git a/internal/codegen/golang/gotype/known_types.go b/internal/codegen/golang/gotype/known_types.go index 8a813647..3322a6ba 100644 --- a/internal/codegen/golang/gotype/known_types.go +++ b/internal/codegen/golang/gotype/known_types.go @@ -94,7 +94,7 @@ var ( //nolint:gochecknoglobals var ( PgBool = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Bool", pg.Bool) - PgQChar = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.QChar", pg.QChar) + PgQChar = MustParseKnownType("byte", pg.QChar) PgName = MustParseKnownType("string", pg.Name) PgInt8 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int8", pg.Int8) PgInt2 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int2", pg.Int2) @@ -105,61 +105,61 @@ var ( PgTID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.TID", pg.TID) PgXID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.XID", pg.XID) PgCID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.CID", pg.CID) - PgJSON = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.JSON", pg.JSON) + PgJSON = MustParseKnownType("*string", pg.JSON) PgPoint = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Point", pg.Point) PgLseg = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Lseg", pg.Lseg) PgPath = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Path", pg.Path) PgBox = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Box", pg.Box) PgPolygon = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Polygon", pg.Polygon) PgLine = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Line", pg.Line) - PgCIDR = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.CIDR", pg.CIDR) - PgCIDRArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.CIDRArray", pg.CIDRArray) + PgCIDR = MustParseKnownType("net/netip.Prefix", pg.CIDR) + PgCIDRArray = MustParseKnownType("[]net/netip.Prefix", pg.CIDRArray) PgFloat4 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Float4", pg.Float4) PgFloat8 = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Float8", pg.Float8) - PgUnknown = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Unknown", pg.Unknown) + PgUnknown = MustParseKnownType("string", pg.Unknown) PgCircle = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Circle", pg.Circle) - PgMacaddr = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Macaddr", pg.Macaddr) - PgInet = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Inet", pg.Inet) - PgBoolArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.BoolArray", pg.BoolArray) - PgByteaArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.ByteaArray", pg.ByteaArray) - PgInt2Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int2Array", pg.Int2Array) - PgInt4Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int4Array", pg.Int4Array) - PgTextArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.TextArray", pg.TextArray) - PgBPCharArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.BPCharArray", pg.BPCharArray) - PgVarcharArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.VarcharArray", pg.VarcharArray) - PgInt8Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int8Array", pg.Int8Array) - PgFloat4Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Float4Array", pg.Float4Array) - PgFloat8Array = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Float8Array", pg.Float8Array) - PgACLItem = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.ACLItem", pg.ACLItem) - PgACLItemArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.ACLItemArray", pg.ACLItemArray) - PgInetArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.InetArray", pg.InetArray) - PgMacaddrArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.MacaddrArray", pg.MacaddrArray) + PgMacaddr = MustParseKnownType("net.HardwareAddr", pg.Macaddr) + PgInet = MustParseKnownType("net/netip.Addr", pg.Inet) + PgBoolArray = MustParseKnownType("[]bool", pg.BoolArray) + PgByteaArray = MustParseKnownType("[][]byte", pg.ByteaArray) + PgInt2Array = MustParseKnownType("[]int16", pg.Int2Array) + PgInt4Array = MustParseKnownType("[]int32", pg.Int4Array) + PgTextArray = MustParseKnownType("[]string", pg.TextArray) + PgBPCharArray = MustParseKnownType("[]string", pg.BPCharArray) + PgVarcharArray = MustParseKnownType("[]string", pg.VarcharArray) + PgInt8Array = MustParseKnownType("[]int64", pg.Int8Array) + PgFloat4Array = MustParseKnownType("[]float32", pg.Float4Array) + PgFloat8Array = MustParseKnownType("[]float64", pg.Float8Array) + PgACLItem = MustParseKnownType("string", pg.ACLItem) + PgACLItemArray = MustParseKnownType("[]string", pg.ACLItemArray) + PgInetArray = MustParseKnownType("[]net/netip.Addr", pg.InetArray) + PgMacaddrArray = MustParseKnownType("[]net.HardwareAddr", pg.MacaddrArray) PgBPChar = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.BPChar", pg.BPChar) PgVarchar = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Varchar", pg.Varchar) PgDate = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Date", pg.Date) PgTime = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Time", pg.Time) PgTimestamp = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Timestamp", pg.Timestamp) - PgTimestampArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.TimestampArray", pg.TimestampArray) - PgDateArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.DateArray", pg.DateArray) + PgTimestampArray = MustParseKnownType("[]github.com/jackc/pgx/v5/pgtype.Timestamp", pg.TimestampArray) + PgDateArray = MustParseKnownType("[]github.com/jackc/pgx/v5/pgtype.Date", pg.DateArray) PgTimestamptz = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Timestamptz", pg.Timestamptz) - PgTimestamptzArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.TimestamptzArray", pg.TimestamptzArray) + PgTimestamptzArray = MustParseKnownType("[]github.com/jackc/pgx/v5/pgtype.Timestamptz", pg.TimestamptzArray) PgInterval = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Interval", pg.Interval) - PgNumericArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.NumericArray", pg.NumericArray) + PgNumericArray = MustParseKnownType("[]github.com/jackc/pgx/v5/pgtype.Numeric", pg.NumericArray) PgBit = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Bit", pg.Bit) PgVarbit = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Varbit", pg.Varbit) PgVoid = &VoidType{} PgNumeric = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Numeric", pg.Numeric) - PgRecord = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Record", pg.Record) + PgRecord = MustParseKnownType("any", pg.Record) PgUUID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.UUID", pg.UUID) - PgUUIDArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.UUIDArray", pg.UUIDArray) - PgJSONB = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.JSONB", pg.JSONB) - PgJSONBArray = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.JSONBArray", pg.JSONBArray) - PgInt4range = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int4range", pg.Int4range) - PgNumrange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Numrange", pg.Numrange) - PgTsrange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Tsrange", pg.Tsrange) - PgTstzrange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Tstzrange", pg.Tstzrange) - PgDaterange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Daterange", pg.Daterange) - PgInt8range = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Int8range", pg.Int8range) + PgUUIDArray = MustParseKnownType("[]github.com/jackc/pgx/v5/pgtype.UUID", pg.UUIDArray) + PgJSONB = MustParseKnownType("*string", pg.JSONB) + PgJSONBArray = MustParseKnownType("[]*string", pg.JSONBArray) + PgInt4range = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Range[int32]", pg.Int4range) + PgNumrange = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Range[float64]", pg.Numrange) + PgTsrange = MustParseKnownType("string", pg.Tsrange) + PgTstzrange = MustParseKnownType("string", pg.Tstzrange) + PgDaterange = MustParseKnownType("string", pg.Daterange) + PgInt8range = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Range[int64]", pg.Int8range) ) // knownGoType is the native pgtype type, the nullable and non-nullable types @@ -167,8 +167,7 @@ var ( // // pgNative means a type that implements the pgx decoder methods directly. // Such types are typically provided by the pgtype package. Used as the fallback -// type and for cases like composite types where we need a -// pgtype.ValueTranscoder. +// type and for cases where no native Go type is configured. // // A nullable type is one that can represent a nullable column, like *string for // a Postgres text type that can be null. A nullable type is nicer to work with diff --git a/internal/codegen/golang/import_set.go b/internal/codegen/golang/import_set.go index 274f225d..14cb2a8e 100644 --- a/internal/codegen/golang/import_set.go +++ b/internal/codegen/golang/import_set.go @@ -24,13 +24,20 @@ func (s *ImportSet) AddPackage(p string) { // AddType adds all fully qualified package paths needed for type and any child // types. func (s *ImportSet) AddType(typ gotype.Type) { - s.AddPackage(typ.Import()) - comp, ok := typ.(*gotype.CompositeType) - if !ok { - return - } - for _, childType := range comp.FieldTypes { - s.AddType(childType) + switch typ := typ.(type) { + case *gotype.ArrayType: + s.AddType(typ.Elem) + case *gotype.CompositeType: + for _, childType := range typ.FieldTypes { + s.AddType(childType) + } + case *gotype.ImportType: + s.AddPackage(typ.PkgPath) + s.AddType(typ.Type) + case *gotype.PointerType: + s.AddType(typ.Elem) + default: + s.AddPackage(typ.Import()) } } diff --git a/internal/codegen/golang/query.gotemplate b/internal/codegen/golang/query.gotemplate index d6c823f0..b1dafd3f 100644 --- a/internal/codegen/golang/query.gotemplate +++ b/internal/codegen/golang/query.gotemplate @@ -13,6 +13,8 @@ import ( {{- if .IsLeader -}} {{- "\n\n" -}} +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { {{- range $pkgFile := .Pkg.Files -}} @@ -27,7 +29,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -42,6 +44,7 @@ func NewQuerier(conn genericConn) *DBQuerier { return &DBQuerier{conn: conn} } +{{- range .Declarers}}{{- "\n\n" -}}{{ .Declare $.PkgPath }}{{ end -}} {{- end -}} {{- range $i, $q := .Queries -}} @@ -52,7 +55,7 @@ const {{ $q.SQLVarName }} = {{ $q.EmitPreparedSQL }} {{- "\n\n" -}} // {{ $q.Name }} implements Querier.{{ $q.Name }}. func (q *DBQuerier) {{ $q.Name }}(ctx context.Context {{- $q.EmitParams }}) ({{ $q.EmitResultType }}, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "{{ $q.Name }}") + ctx = context.WithValue(ctx, QueryName{}, "{{ $q.Name }}") {{- if eq $q.ResultKind ":exec" }} cmdTag, err := q.conn.Exec(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) if err != nil { @@ -64,77 +67,10 @@ func (q *DBQuerier) {{ $q.Name }}(ctx context.Context {{- $q.EmitParams }}) ({{ if err != nil { return {{ $q.EmitZeroResult }}, fmt.Errorf("query {{ $q.Name }}: %w", err) } - fds := rows.FieldDescriptions() - {{- range $i, $col := $q.Outputs }} - {{ $q.EmitPlanScan $i $col}} - {{- end }} - return {{ $q.EmitCollectionFunc }}(rows, func(row pgx.CollectableRow) ({{ $q.EmitSingularResultType }}, error) { - vals := row.RawValues() - var item {{ $q.EmitSingularResultType }} - {{- range $i, $col := $q.Outputs }} - {{ $q.EmitScanColumn $i $col }} - {{- end }} - return item, nil - }) + return {{ $q.EmitCollectionFunc }}(rows, {{ $q.EmitRowMapper }}) {{- end }} } {{- end -}} {{- "\n" -}} -{{ if .IsLeader }} -type scanCacheKey struct { - oid uint32 - format int16 - typeName string -} - -var ( - plans = make(map[scanCacheKey]pgtype.ScanPlan, 16) - plansMu sync.RWMutex -) - -func planScan(codec pgtype.Codec, fd pgconn.FieldDescription, target any) pgtype.ScanPlan { - key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("%T", target)} - plansMu.RLock() - plan := plans[key] - plansMu.RUnlock() - if plan != nil { - return plan - } - plan = codec.PlanScan(nil, fd.DataTypeOID, fd.Format, target) - plansMu.Lock() - plans[key] = plan - plansMu.Unlock() - return plan -} - -type ptrScanner[T any] struct { - basePlan pgtype.ScanPlan -} - -func (s ptrScanner[T]) Scan(src []byte, dst any) error { - if src == nil { - return nil - } - d := dst.(**T) - *d = new(T) - return s.basePlan.Scan(src, *d) -} - -func planPtrScan[T any](codec pgtype.Codec, fd pgconn.FieldDescription, target *T) pgtype.ScanPlan { - key := scanCacheKey{fd.DataTypeOID, fd.Format, fmt.Sprintf("*%T", target)} - plansMu.RLock() - plan := plans[key] - plansMu.RUnlock() - if plan != nil { - return plan - } - basePlan := planScan(codec, fd, target) - ptrPlan := ptrScanner[T]{basePlan} - plansMu.Lock() - plans[key] = plan - plansMu.Unlock() - return ptrPlan -} -{{- end -}} {{- end -}} diff --git a/internal/codegen/golang/templated_file.go b/internal/codegen/golang/templated_file.go index 5c40a856..674eadeb 100644 --- a/internal/codegen/golang/templated_file.go +++ b/internal/codegen/golang/templated_file.go @@ -153,46 +153,20 @@ func (tq TemplatedQuery) EmitParamStruct() string { // EmitParamNames emits the TemplatedQuery.Inputs into comma separated names // for use in a method invocation. func (tq TemplatedQuery) EmitParamNames() string { - appendParam := func(sb *strings.Builder, typ gotype.Type, name string) { - switch typ := gotype.UnwrapNestedType(typ).(type) { - case *gotype.CompositeType: - sb.WriteString("q.types.") - sb.WriteString(NameCompositeInitFunc(typ)) - sb.WriteString("(") - sb.WriteString(name) - sb.WriteString(")") - case *gotype.ArrayType: - if gotype.IsPgxSupportedArray(typ) { - sb.WriteString(name) - break - } - switch gotype.UnwrapNestedType(typ.Elem).(type) { - case *gotype.CompositeType, *gotype.EnumType: - sb.WriteString("q.types.") - sb.WriteString(NameArrayInitFunc(typ)) - sb.WriteString("(") - sb.WriteString(name) - sb.WriteString(")") - default: - sb.WriteString(name) - } - default: - sb.WriteString(name) - } - } switch { case tq.isInlineParams(): sb := &strings.Builder{} for _, input := range tq.Inputs { sb.WriteString(", ") - appendParam(sb, input.Type, input.LowerName) + sb.WriteString(input.LowerName) } return sb.String() default: sb := &strings.Builder{} for _, input := range tq.Inputs { sb.WriteString(", ") - appendParam(sb, input.Type, "params."+input.UpperName) + sb.WriteString("params.") + sb.WriteString(input.UpperName) } return sb.String() } @@ -296,6 +270,58 @@ func (tq TemplatedQuery) EmitCollectionFunc() (string, error) { } } +func (tq TemplatedQuery) EmitRowToFunc() (string, error) { + switch tq.ResultKind { + case ast.ResultKindExec: + return "", fmt.Errorf("cannot EmitRowToFunc for :exec query %s", tq.Name) + case ast.ResultKindMany, ast.ResultKindOne: + if len(tq.Outputs) == 1 { + return "pgx.RowTo", nil + } + return "pgx.RowToStructByName", nil + default: + return "", fmt.Errorf("unhandled EmitRowToFunc type: %s", tq.ResultKind) + } +} + +func (tq TemplatedQuery) EmitRowMapper() (string, error) { + rowToFunc, err := tq.EmitRowToFunc() + if err != nil { + return "", err + } + resultType := tq.EmitSingularResultType() + if len(tq.ScanCols) == len(tq.Outputs) { + return fmt.Sprintf("%s[%s]", rowToFunc, resultType), nil + } + scanTargets, err := tq.EmitScanTargets() + if err != nil { + return "", err + } + return fmt.Sprintf(`func(row pgx.CollectableRow) (%s, error) { + var item %s + if err := row.Scan(%s); err != nil { + return item, err + } + return item, nil +}`, resultType, resultType, scanTargets), nil +} + +func (tq TemplatedQuery) EmitScanTargets() (string, error) { + targets := make([]string, 0, len(tq.ScanCols)) + for _, col := range tq.ScanCols { + if _, ok := gotype.UnwrapNestedType(col.Type).(*gotype.VoidType); ok { + targets = append(targets, "nil") + continue + } + if len(tq.Outputs) == 1 { + targets = append(targets, "&item") + continue + } + targets = append(targets, "&item."+col.UpperName) + } + return strings.Join(targets, ", "), nil +} + // EmitSingularResultType returns the string representing a single element // of the overall query result type, like FindAuthorsRow when the overall return // type is []FindAuthorsRow. @@ -384,15 +410,18 @@ func (tq TemplatedQuery) EmitZeroResult() (string, error) { case strings.HasPrefix(typ, "*"): return "nil", nil // nil pointer } + if _, ok := gotype.UnwrapNestedType(tq.Outputs[0].Type).(*gotype.EnumType); ok { + return `""`, nil + } switch typ { - case "int", "int32", "int64", "float32", "float64": + case "int", "int32", "int64", "float32", "float64", "uint", "uint32", "uint64": return "0", nil case "string": return `""`, nil case "bool": return "false", nil default: - return typ + "{}", nil // won't work for type Foo int + return tq.Outputs[0].QualType + "{}", nil // won't work for type Foo int } default: return "", fmt.Errorf("unhandled EmitZeroResult kind: %s", tq.ResultKind) @@ -477,6 +506,8 @@ func (tq TemplatedQuery) EmitRowStruct() string { sb.WriteString(strings.Repeat(" ", maxTypeLen-len(out.QualType))) sb.WriteString("`json:") sb.WriteString(strconv.Quote(out.PgName)) + sb.WriteString(" db:") + sb.WriteString(strconv.Quote(out.PgName)) sb.WriteString("`") sb.WriteRune('\n') } diff --git a/internal/codegen/golang/templater.go b/internal/codegen/golang/templater.go index 5c050936..7cef961e 100644 --- a/internal/codegen/golang/templater.go +++ b/internal/codegen/golang/templater.go @@ -117,7 +117,6 @@ func (tm Templater) templateFile(file codegen.QueryFile, isLeader bool) (Templat imports.AddPackage("fmt") imports.AddPackage("github.com/jackc/pgx/v5/pgconn") if isLeader { - imports.AddPackage("github.com/jackc/pgx/v5/pgtype") imports.AddPackage("github.com/jackc/pgx/v5") } @@ -190,6 +189,9 @@ func (tm Templater) templateFile(file codegen.QueryFile, isLeader bool) (Templat if len(nonVoidCols) == 0 { resultKind = ast.ResultKindExec } + if resultKind != ast.ResultKindExec { + imports.AddPackage("github.com/jackc/pgx/v5") + } queries = append(queries, TemplatedQuery{ Name: tm.caser.ToUpperGoIdent(query.Name), SQLVarName: tm.caser.ToLowerGoIdent(query.Name) + "SQL", diff --git a/internal/codegen/golang/testdata/declarer_composite.input.golden b/internal/codegen/golang/testdata/declarer_composite.input.golden index 8cfddeb6..690e99a7 100644 --- a/internal/codegen/golang/testdata/declarer_composite.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite.input.golden @@ -1,110 +1,46 @@ // SomeTable represents the Postgres composite type "some_table". type SomeTable struct { - Foo int16 `json:"foo"` - BarBaz pgtype.Text `json:"bar_baz"` -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + Foo int16 `json:"foo" db:"foo"` + BarBaz pgtype.Text `json:"bar_baz" db:"bar_baz"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newSomeTable creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'some_table'. -func (tr *typeResolver) newSomeTable() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "some_table", - compositeField{name: "foo", typeName: "int2", defaultVal: &pgtype.Int2{}}, - compositeField{name: "bar_baz", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"some_table\"") + -// newSomeTableInit creates an initialized pgtype.ValueTranscoder for the -// Postgres composite type 'some_table' to encode query parameters. -func (tr *typeResolver) newSomeTableInit(v SomeTable) pgtype.ValueTranscoder { - return tr.setValue(tr.newSomeTable(), tr.newSomeTableRaw(v)) -} -// newSomeTableRaw returns all composite fields for the Postgres composite -// type 'some_table' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newSomeTableRaw(v SomeTable) []interface{} { - return []interface{}{ - v.Foo, - v.BarBaz, - } -} \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite.output.golden b/internal/codegen/golang/testdata/declarer_composite.output.golden index 8c697bf3..49f809b6 100644 --- a/internal/codegen/golang/testdata/declarer_composite.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite.output.golden @@ -1,95 +1,43 @@ // SomeTable represents the Postgres composite type "some_table". type SomeTable struct { - Foo int16 `json:"foo"` - BarBaz pgtype.Text `json:"bar_baz"` -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + Foo int16 `json:"foo" db:"foo"` + BarBaz pgtype.Text `json:"bar_baz" db:"bar_baz"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newSomeTable creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'some_table'. -func (tr *typeResolver) newSomeTable() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "some_table", - compositeField{name: "foo", typeName: "int2", defaultVal: &pgtype.Int2{}}, - compositeField{name: "bar_baz", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} \ No newline at end of file +var _ = addTypeToRegister("\"some_table\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_array.input.golden b/internal/codegen/golang/testdata/declarer_composite_array.input.golden index 887fbca1..6c4e9a20 100644 --- a/internal/codegen/golang/testdata/declarer_composite_array.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite_array.input.golden @@ -1,130 +1,50 @@ // SomeTable represents the Postgres composite type "some_table". type SomeTable struct { - Foo int16 `json:"foo"` - BarBaz pgtype.Text `json:"bar_baz"` + Foo int16 `json:"foo" db:"foo"` + BarBaz pgtype.Text `json:"bar_baz" db:"bar_baz"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} +var typesToRegister = []string{} -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true -} -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use -} +var _ = addTypeToRegister("\"some_table\"") -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newSomeTable creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'some_table'. -func (tr *typeResolver) newSomeTable() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "some_table", - compositeField{name: "foo", typeName: "int2", defaultVal: &pgtype.Int2{}}, - compositeField{name: "bar_baz", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"_some_array\"") -// newSomeTableRaw returns all composite fields for the Postgres composite -// type 'some_table' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newSomeTableRaw(v SomeTable) []interface{} { - return []interface{}{ - v.Foo, - v.BarBaz, - } -} - -// newSomeTableArray creates a new pgtype.ValueTranscoder for the Postgres -// '_some_array' array type. -func (tr *typeResolver) newSomeTableArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_some_array", "some_table", tr.newSomeTable) -} -// newSomeTableArrayInit creates an initialized pgtype.ValueTranscoder for the -// Postgres array type '_some_array' to encode query parameters. -func (tr *typeResolver) newSomeTableArrayInit(ps []SomeTable) pgtype.ValueTranscoder { - dec := tr.newSomeTableArray() - if err := dec.Set(tr.newSomeTableArrayRaw(ps)); err != nil { - panic("encode []SomeTable: " + err.Error()) // should always succeed - } - return textPreferrer{ValueTranscoder: dec, typeName: "_some_array"} -} -// newSomeTableArrayRaw returns all elements for the Postgres array type '_some_array' -// as a slice of interface{} for use with the pgtype.Value Set method. -func (tr *typeResolver) newSomeTableArrayRaw(vs []SomeTable) []interface{} { - elems := make([]interface{}, len(vs)) - for i, v := range vs { - elems[i] = tr.newSomeTableRaw(v) - } - return elems -} \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_array.output.golden b/internal/codegen/golang/testdata/declarer_composite_array.output.golden index 9947597c..31714f83 100644 --- a/internal/codegen/golang/testdata/declarer_composite_array.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite_array.output.golden @@ -1,101 +1,45 @@ // SomeTable represents the Postgres composite type "some_table". type SomeTable struct { - Foo int16 `json:"foo"` - BarBaz pgtype.Text `json:"bar_baz"` -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + Foo int16 `json:"foo" db:"foo"` + BarBaz pgtype.Text `json:"bar_baz" db:"bar_baz"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newSomeTable creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'some_table'. -func (tr *typeResolver) newSomeTable() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "some_table", - compositeField{name: "foo", typeName: "int2", defaultVal: &pgtype.Int2{}}, - compositeField{name: "bar_baz", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"some_table\"") -// newSomeTableArray creates a new pgtype.ValueTranscoder for the Postgres -// '_some_array' array type. -func (tr *typeResolver) newSomeTableArray() pgtype.ValueTranscoder { - return tr.newArrayValue("_some_array", "some_table", tr.newSomeTable) -} \ No newline at end of file +var _ = addTypeToRegister("\"_some_array\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_enum.input.golden b/internal/codegen/golang/testdata/declarer_composite_enum.input.golden index 567b2626..12afb781 100644 --- a/internal/codegen/golang/testdata/declarer_composite_enum.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite_enum.input.golden @@ -1,18 +1,6 @@ // SomeTableEnum represents the Postgres composite type "some_table_enum". type SomeTableEnum struct { - Foo DeviceType `json:"foo"` -} - -// newDeviceTypeEnum creates a new pgtype.ValueTranscoder for the -// Postgres enum type 'device_type'. -func newDeviceTypeEnum() pgtype.ValueTranscoder { - return pgtype.NewEnumType( - "device_type", - []string{ - string(DeviceTypeIOS), - string(DeviceTypeMobile), - }, - ) + Foo DeviceType `json:"foo" db:"foo"` } // DeviceType represents the Postgres enum "device_type". @@ -25,105 +13,45 @@ const ( func (d DeviceType) String() string { return string(d) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} +var typesToRegister = []string{} -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use -} -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} +var _ = addTypeToRegister("\"device_type\"") -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} +var _ = addTypeToRegister("\"some_table_enum\"") -// newSomeTableEnum creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'some_table_enum'. -func (tr *typeResolver) newSomeTableEnum() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "some_table_enum", - compositeField{name: "foo", typeName: "some_table_enum", defaultVal: newDeviceTypeEnum()}, - ) -} -// newSomeTableEnumInit creates an initialized pgtype.ValueTranscoder for the -// Postgres composite type 'some_table_enum' to encode query parameters. -func (tr *typeResolver) newSomeTableEnumInit(v SomeTableEnum) pgtype.ValueTranscoder { - return tr.setValue(tr.newSomeTableEnum(), tr.newSomeTableEnumRaw(v)) -} -// newSomeTableEnumRaw returns all composite fields for the Postgres composite -// type 'some_table_enum' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newSomeTableEnumRaw(v SomeTableEnum) []interface{} { - return []interface{}{ - v.Foo, - } -} \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_enum.output.golden b/internal/codegen/golang/testdata/declarer_composite_enum.output.golden index a0daad58..5dcdb847 100644 --- a/internal/codegen/golang/testdata/declarer_composite_enum.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite_enum.output.golden @@ -1,18 +1,6 @@ // SomeTableEnum represents the Postgres composite type "some_table_enum". type SomeTableEnum struct { - Foo DeviceType `json:"foo"` -} - -// newDeviceTypeEnum creates a new pgtype.ValueTranscoder for the -// Postgres enum type 'device_type'. -func newDeviceTypeEnum() pgtype.ValueTranscoder { - return pgtype.NewEnumType( - "device_type", - []string{ - string(DeviceTypeIOS), - string(DeviceTypeMobile), - }, - ) + Foo DeviceType `json:"foo" db:"foo"` } // DeviceType represents the Postgres enum "device_type". @@ -25,91 +13,42 @@ const ( func (d DeviceType) String() string { return string(d) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newSomeTableEnum creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'some_table_enum'. -func (tr *typeResolver) newSomeTableEnum() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "some_table_enum", - compositeField{name: "foo", typeName: "some_table_enum", defaultVal: newDeviceTypeEnum()}, - ) -} \ No newline at end of file +var _ = addTypeToRegister("\"device_type\"") + +var _ = addTypeToRegister("\"some_table_enum\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_nested.input.golden b/internal/codegen/golang/testdata/declarer_composite_nested.input.golden index 058642fd..034a967e 100644 --- a/internal/codegen/golang/testdata/declarer_composite_nested.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite_nested.input.golden @@ -1,132 +1,55 @@ // FooType represents the Postgres composite type "foo_type". type FooType struct { - Alpha pgtype.Text `json:"alpha"` + Alpha pgtype.Text `json:"alpha" db:"alpha"` } // SomeTableNested represents the Postgres composite type "some_table_nested". type SomeTableNested struct { - Foo FooType `json:"foo"` - BarBaz pgtype.Text `json:"bar_baz"` + Foo FooType `json:"foo" db:"foo"` + BarBaz pgtype.Text `json:"bar_baz" db:"bar_baz"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} +var typesToRegister = []string{} -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true -} -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} - -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use -} -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} +var _ = addTypeToRegister("\"foo_type\"") -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newFooType creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'foo_type'. -func (tr *typeResolver) newFooType() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "foo_type", - compositeField{name: "alpha", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} -// newFooTypeRaw returns all composite fields for the Postgres composite -// type 'foo_type' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newFooTypeRaw(v FooType) []interface{} { - return []interface{}{ - v.Alpha, - } -} +var _ = addTypeToRegister("\"some_table_nested\"") -// newSomeTableNested creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'some_table_nested'. -func (tr *typeResolver) newSomeTableNested() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "some_table_nested", - compositeField{name: "foo", typeName: "foo_type", defaultVal: tr.newFooType()}, - compositeField{name: "bar_baz", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} -// newSomeTableNestedInit creates an initialized pgtype.ValueTranscoder for the -// Postgres composite type 'some_table_nested' to encode query parameters. -func (tr *typeResolver) newSomeTableNestedInit(v SomeTableNested) pgtype.ValueTranscoder { - return tr.setValue(tr.newSomeTableNested(), tr.newSomeTableNestedRaw(v)) -} -// newSomeTableNestedRaw returns all composite fields for the Postgres composite -// type 'some_table_nested' as a slice of interface{} to encode query parameters. -func (tr *typeResolver) newSomeTableNestedRaw(v SomeTableNested) []interface{} { - return []interface{}{ - tr.newFooTypeRaw(v.Foo), - v.BarBaz, - } -} \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_nested.output.golden b/internal/codegen/golang/testdata/declarer_composite_nested.output.golden index 12719532..c7658db8 100644 --- a/internal/codegen/golang/testdata/declarer_composite_nested.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite_nested.output.golden @@ -1,109 +1,50 @@ // FooType represents the Postgres composite type "foo_type". type FooType struct { - Alpha pgtype.Text `json:"alpha"` + Alpha pgtype.Text `json:"alpha" db:"alpha"` } // SomeTableNested represents the Postgres composite type "some_table_nested". type SomeTableNested struct { - Foo FooType `json:"foo"` - BarBaz pgtype.Text `json:"bar_baz"` -} - -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name -} - -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} - -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false + Foo FooType `json:"foo" db:"foo"` + BarBaz pgtype.Text `json:"bar_baz" db:"bar_baz"` +} + +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true + return nil } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} +var typesToRegister = []string{} -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -func (tr *typeResolver) newCompositeValue(name string, fields ...compositeField) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - fs := make([]pgtype.CompositeTypeField, len(fields)) - vals := make([]pgtype.ValueTranscoder, len(fields)) - isBinaryOk := true - for i, field := range fields { - oid, val, ok := tr.findValue(field.typeName) - if !ok { - oid = unknownOID - val = field.defaultVal - } - isBinaryOk = isBinaryOk && oid != unknownOID - fs[i] = pgtype.CompositeTypeField{Name: field.name, OID: oid} - vals[i] = val - } - // Okay to ignore error because it's only thrown when the number of field - // names does not equal the number of ValueTranscoders. - typ, _ := pgtype.NewCompositeTypeValues(name, fs, vals) - if !isBinaryOk { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -func (tr *typeResolver) newArrayValue(name, elemName string, defaultVal func() pgtype.ValueTranscoder) pgtype.ValueTranscoder { - if _, val, ok := tr.findValue(name); ok { - return val - } - elemOID, elemVal, ok := tr.findValue(elemName) - elemValFunc := func() pgtype.ValueTranscoder { - return pgtype.NewValue(elemVal).(pgtype.ValueTranscoder) - } - if !ok { - elemOID = unknownOID - elemValFunc = defaultVal - } - typ := pgtype.NewArrayType(name, elemOID, elemValFunc) - if elemOID == unknownOID { - return textPreferrer{ValueTranscoder: typ, typeName: name} - } - return typ -} -// newFooType creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'foo_type'. -func (tr *typeResolver) newFooType() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "foo_type", - compositeField{name: "alpha", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} +var _ = addTypeToRegister("\"foo_type\"") -// newSomeTableNested creates a new pgtype.ValueTranscoder for the Postgres -// composite type 'some_table_nested'. -func (tr *typeResolver) newSomeTableNested() pgtype.ValueTranscoder { - return tr.newCompositeValue( - "some_table_nested", - compositeField{name: "foo", typeName: "foo_type", defaultVal: tr.newFooType()}, - compositeField{name: "bar_baz", typeName: "text", defaultVal: &pgtype.Text{}}, - ) -} \ No newline at end of file +var _ = addTypeToRegister("\"some_table_nested\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_enum_escaping.input.golden b/internal/codegen/golang/testdata/declarer_enum_escaping.input.golden index 8b4b9c34..4e05f106 100644 --- a/internal/codegen/golang/testdata/declarer_enum_escaping.input.golden +++ b/internal/codegen/golang/testdata/declarer_enum_escaping.input.golden @@ -8,31 +8,38 @@ const ( func (q Quoting) String() string { return string(q) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} +var typesToRegister = []string{} -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} \ No newline at end of file +var _ = addTypeToRegister("\"quoting\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_enum_escaping.output.golden b/internal/codegen/golang/testdata/declarer_enum_escaping.output.golden index 8b4b9c34..4e05f106 100644 --- a/internal/codegen/golang/testdata/declarer_enum_escaping.output.golden +++ b/internal/codegen/golang/testdata/declarer_enum_escaping.output.golden @@ -8,31 +8,38 @@ const ( func (q Quoting) String() string { return string(q) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} +var typesToRegister = []string{} -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} \ No newline at end of file +var _ = addTypeToRegister("\"quoting\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_enum_simple.input.golden b/internal/codegen/golang/testdata/declarer_enum_simple.input.golden index a6bfe1ab..1ffeb7b8 100644 --- a/internal/codegen/golang/testdata/declarer_enum_simple.input.golden +++ b/internal/codegen/golang/testdata/declarer_enum_simple.input.golden @@ -8,31 +8,38 @@ const ( func (d DeviceType) String() string { return string(d) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} +var typesToRegister = []string{} -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} \ No newline at end of file +var _ = addTypeToRegister("\"device_type\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_enum_simple.output.golden b/internal/codegen/golang/testdata/declarer_enum_simple.output.golden index a6bfe1ab..1ffeb7b8 100644 --- a/internal/codegen/golang/testdata/declarer_enum_simple.output.golden +++ b/internal/codegen/golang/testdata/declarer_enum_simple.output.golden @@ -8,31 +8,38 @@ const ( func (d DeviceType) String() string { return string(d) } -// typeResolver looks up the pgtype.ValueTranscoder by Postgres type name. -type typeResolver struct { - connInfo *pgtype.ConnInfo // types by Postgres type name +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil } -func newTypeResolver() *typeResolver { - ci := pgtype.NewConnInfo() - return &typeResolver{connInfo: ci} -} +var typesToRegister = []string{} -// findValue find the OID, and pgtype.ValueTranscoder for a Postgres type name. -func (tr *typeResolver) findValue(name string) (uint32, pgtype.ValueTranscoder, bool) { - typ, ok := tr.connInfo.DataTypeForName(name) - if !ok { - return 0, nil, false - } - v := pgtype.NewValue(typ.Value) - return typ.OID, v.(pgtype.ValueTranscoder), true +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// setValue sets the value of a ValueTranscoder to a value that should always -// work and panics if it fails. -func (tr *typeResolver) setValue(vt pgtype.ValueTranscoder, val interface{}) pgtype.ValueTranscoder { - if err := vt.Set(val); err != nil { - panic(fmt.Sprintf("set ValueTranscoder %T to %+v: %s", vt, val, err)) - } - return vt -} \ No newline at end of file +var _ = addTypeToRegister("\"device_type\"") \ No newline at end of file diff --git a/internal/pg/query.sql.go b/internal/pg/query.sql.go index af7b16a3..815a8c72 100644 --- a/internal/pg/query.sql.go +++ b/internal/pg/query.sql.go @@ -9,6 +9,8 @@ import ( "github.com/jackc/pgx/v5/pgconn" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { FindEnumTypes(ctx context.Context, oids []uint32) ([]FindEnumTypesRow, error) @@ -34,7 +36,7 @@ type Querier interface { var _ Querier = &DBQuerier{} type DBQuerier struct { - conn genericConn // underlying Postgres transport to use + conn genericConn } // genericConn is a connection like *pgx.Conn, pgx.Tx, or *pgxpool.Pool. @@ -49,6 +51,42 @@ func NewQuerier(conn genericConn) *DBQuerier { return &DBQuerier{conn: conn} } +// RegisterTypes loads custom PostgreSQL types into conn's pgx type map. +func RegisterTypes(ctx context.Context, conn *pgx.Conn) error { + pending := append([]string(nil), typesToRegister...) + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + var lastErr error + var lastType string + for _, typ := range pending { + dt, err := conn.LoadType(ctx, typ) + if err != nil { + lastErr = err + lastType = typ + remaining = append(remaining, typ) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining + } + return nil +} + +var typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} +} + + + const findEnumTypesSQL = `WITH enums AS ( SELECT enumtypid::int8 AS enum_type, @@ -94,35 +132,24 @@ WHERE typ.typisdefined AND typ.oid = ANY ($1::oid[]);` type FindEnumTypesRow struct { - OID uint32 `json:"oid"` - TypeName string `json:"type_name"` - ChildOIDs []int `json:"child_oids"` - Orders []float32 `json:"orders"` - Labels []string `json:"labels"` - TypeKind byte `json:"type_kind"` - DefaultExpr string `json:"default_expr"` + OID uint32 `json:"oid" db:"oid"` + TypeName string `json:"type_name" db:"type_name"` + ChildOIDs []int `json:"child_oids" db:"child_oids"` + Orders []float32 `json:"orders" db:"orders"` + Labels []string `json:"labels" db:"labels"` + TypeKind byte `json:"type_kind" db:"type_kind"` + DefaultExpr string `json:"default_expr" db:"default_expr"` } // FindEnumTypes implements Querier.FindEnumTypes. func (q *DBQuerier) FindEnumTypes(ctx context.Context, oids []uint32) ([]FindEnumTypesRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindEnumTypes") + ctx = context.WithValue(ctx, QueryName{}, "FindEnumTypes") rows, err := q.conn.Query(ctx, findEnumTypesSQL, oids) if err != nil { return nil, fmt.Errorf("query FindEnumTypes: %w", err) } - defer rows.Close() - items := []FindEnumTypesRow{} - for rows.Next() { - var item FindEnumTypesRow - if err := rows.Scan(&item.OID, &item.TypeName, &item.ChildOIDs, &item.Orders, &item.Labels, &item.TypeKind, &item.DefaultExpr); err != nil { - return nil, fmt.Errorf("scan FindEnumTypes row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindEnumTypes rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindEnumTypesRow]) } const findArrayTypesSQL = `SELECT @@ -155,32 +182,21 @@ WHERE arr_typ.typisdefined AND arr_typ.oid = ANY ($1::oid[]);` type FindArrayTypesRow struct { - OID uint32 `json:"oid"` - TypeName string `json:"type_name"` - ElemOID uint32 `json:"elem_oid"` - TypeKind byte `json:"type_kind"` + OID uint32 `json:"oid" db:"oid"` + TypeName string `json:"type_name" db:"type_name"` + ElemOID uint32 `json:"elem_oid" db:"elem_oid"` + TypeKind byte `json:"type_kind" db:"type_kind"` } // FindArrayTypes implements Querier.FindArrayTypes. func (q *DBQuerier) FindArrayTypes(ctx context.Context, oids []uint32) ([]FindArrayTypesRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindArrayTypes") + ctx = context.WithValue(ctx, QueryName{}, "FindArrayTypes") rows, err := q.conn.Query(ctx, findArrayTypesSQL, oids) if err != nil { return nil, fmt.Errorf("query FindArrayTypes: %w", err) } - defer rows.Close() - items := []FindArrayTypesRow{} - for rows.Next() { - var item FindArrayTypesRow - if err := rows.Scan(&item.OID, &item.TypeName, &item.ElemOID, &item.TypeKind); err != nil { - return nil, fmt.Errorf("scan FindArrayTypes row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindArrayTypes rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindArrayTypesRow]) } const findCompositeTypesSQL = `WITH table_cols AS ( @@ -214,36 +230,25 @@ WHERE typ.oid = ANY ($1::oid[]) AND typ.typtype = 'c';` type FindCompositeTypesRow struct { - TableTypeName string `json:"table_type_name"` - TableTypeOID uint32 `json:"table_type_oid"` - TableName string `json:"table_name"` - ColNames []string `json:"col_names"` - ColOIDs []int `json:"col_oids"` - ColOrders []int `json:"col_orders"` - ColNotNulls []bool `json:"col_not_nulls"` - ColTypeNames []string `json:"col_type_names"` + TableTypeName string `json:"table_type_name" db:"table_type_name"` + TableTypeOID uint32 `json:"table_type_oid" db:"table_type_oid"` + TableName string `json:"table_name" db:"table_name"` + ColNames []string `json:"col_names" db:"col_names"` + ColOIDs []int `json:"col_oids" db:"col_oids"` + ColOrders []int `json:"col_orders" db:"col_orders"` + ColNotNulls []bool `json:"col_not_nulls" db:"col_not_nulls"` + ColTypeNames []string `json:"col_type_names" db:"col_type_names"` } // FindCompositeTypes implements Querier.FindCompositeTypes. func (q *DBQuerier) FindCompositeTypes(ctx context.Context, oids []uint32) ([]FindCompositeTypesRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindCompositeTypes") + ctx = context.WithValue(ctx, QueryName{}, "FindCompositeTypes") rows, err := q.conn.Query(ctx, findCompositeTypesSQL, oids) if err != nil { return nil, fmt.Errorf("query FindCompositeTypes: %w", err) } - defer rows.Close() - items := []FindCompositeTypesRow{} - for rows.Next() { - var item FindCompositeTypesRow - if err := rows.Scan(&item.TableTypeName, &item.TableTypeOID, &item.TableName, &item.ColNames, &item.ColOIDs, &item.ColOrders, &item.ColNotNulls, &item.ColTypeNames); err != nil { - return nil, fmt.Errorf("scan FindCompositeTypes row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindCompositeTypes rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindCompositeTypesRow]) } const findDescendantOIDsSQL = `WITH RECURSIVE oid_descs(oid) AS ( @@ -276,24 +281,13 @@ FROM oid_descs;` // FindDescendantOIDs implements Querier.FindDescendantOIDs. func (q *DBQuerier) FindDescendantOIDs(ctx context.Context, oids []uint32) ([]uint32, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindDescendantOIDs") + ctx = context.WithValue(ctx, QueryName{}, "FindDescendantOIDs") rows, err := q.conn.Query(ctx, findDescendantOIDsSQL, oids) if err != nil { return nil, fmt.Errorf("query FindDescendantOIDs: %w", err) } - defer rows.Close() - items := []uint32{} - for rows.Next() { - var item uint32 - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan FindDescendantOIDs row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindDescendantOIDs rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowTo[uint32]) } const findOIDByNameSQL = `SELECT oid @@ -304,13 +298,13 @@ LIMIT 1;` // FindOIDByName implements Querier.FindOIDByName. func (q *DBQuerier) FindOIDByName(ctx context.Context, name string) (uint32, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOIDByName") - row := q.conn.QueryRow(ctx, findOIDByNameSQL, name) - var item uint32 - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query FindOIDByName: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindOIDByName") + rows, err := q.conn.Query(ctx, findOIDByNameSQL, name) + if err != nil { + return 0, fmt.Errorf("query FindOIDByName: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[uint32]) } const findOIDNameSQL = `SELECT typname AS name @@ -319,13 +313,13 @@ WHERE oid = $1;` // FindOIDName implements Querier.FindOIDName. func (q *DBQuerier) FindOIDName(ctx context.Context, oid uint32) (string, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOIDName") - row := q.conn.QueryRow(ctx, findOIDNameSQL, oid) - var item string - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query FindOIDName: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "FindOIDName") + rows, err := q.conn.Query(ctx, findOIDNameSQL, oid) + if err != nil { + return "", fmt.Errorf("query FindOIDName: %w", err) } - return item, nil + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) } const findOIDNamesSQL = `SELECT oid, typname AS name, typtype AS kind @@ -333,29 +327,18 @@ FROM pg_type WHERE oid = ANY ($1::oid[]);` type FindOIDNamesRow struct { - OID uint32 `json:"oid"` - Name string `json:"name"` - Kind byte `json:"kind"` + OID uint32 `json:"oid" db:"oid"` + Name string `json:"name" db:"name"` + Kind byte `json:"kind" db:"kind"` } // FindOIDNames implements Querier.FindOIDNames. func (q *DBQuerier) FindOIDNames(ctx context.Context, oid []uint32) ([]FindOIDNamesRow, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOIDNames") + ctx = context.WithValue(ctx, QueryName{}, "FindOIDNames") rows, err := q.conn.Query(ctx, findOIDNamesSQL, oid) if err != nil { return nil, fmt.Errorf("query FindOIDNames: %w", err) } - defer rows.Close() - items := []FindOIDNamesRow{} - for rows.Next() { - var item FindOIDNamesRow - if err := rows.Scan(&item.OID, &item.Name, &item.Kind); err != nil { - return nil, fmt.Errorf("scan FindOIDNames row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindOIDNames rows: %w", err) - } - return items, err + + return pgx.CollectRows(rows, pgx.RowToStructByName[FindOIDNamesRow]) } diff --git a/internal/pg/type_fetcher.go b/internal/pg/type_fetcher.go index e081f34f..00cb0ab5 100644 --- a/internal/pg/type_fetcher.go +++ b/internal/pg/type_fetcher.go @@ -40,9 +40,7 @@ func (tf *TypeFetcher) FindTypesByOIDs(oids ...uint32) (map[uint32]Type, error) return nil, fmt.Errorf("find descendant oids: %w", err) } allOIDs := make([]uint32, len(descOIDs)) - for i, d := range descOIDs { - allOIDs[i] = uint32(d) - } + copy(allOIDs, descOIDs) types, uncached := tf.cache.getOIDs(allOIDs...) enums, err := tf.findEnumTypes(ctx, uncached) @@ -189,7 +187,7 @@ func (tf *TypeFetcher) findArrayTypes(ctx context.Context, uncached map[uint32]s } types := make([]ArrayType, len(rows)) for i, row := range rows { - elemType, ok := tf.cache.getOID(uint32(row.ElemOID)) + elemType, ok := tf.cache.getOID(row.ElemOID) if !ok { return nil, fmt.Errorf("find type for array elem %s oid=%d", row.TypeName, row.OID) } @@ -249,7 +247,7 @@ func (tf *TypeFetcher) resolvePlaceholderTypes(knownTypes map[uint32]Type) error func oidKeys(os map[uint32]struct{}) []uint32 { oids := make([]uint32, 0, len(os)) for oid := range os { - oids = append(oids, uint32(oid)) + oids = append(oids, oid) } return oids } diff --git a/internal/pg/type_fetcher_test.go b/internal/pg/type_fetcher_test.go index fef06a27..5f1900b4 100644 --- a/internal/pg/type_fetcher_test.go +++ b/internal/pg/type_fetcher_test.go @@ -195,7 +195,7 @@ func TestNewTypeFetcher(t *testing.T) { // Act. fetcher := NewTypeFetcher(conn) oid := findOIDVal(t, tt.fetchOID, querier) - gotTypeMap, err := fetcher.FindTypesByOIDs(uint32(oid)) + gotTypeMap, err := fetcher.FindTypesByOIDs(oid) if err != nil { t.Fatal(err) } diff --git a/internal/pginfer/pginfer.go b/internal/pginfer/pginfer.go index 4d65d93e..35f08758 100644 --- a/internal/pginfer/pginfer.go +++ b/internal/pginfer/pginfer.go @@ -157,7 +157,7 @@ func (inf *Inferrer) prepareTypes(query *ast.SourceQuery) (_a []InputParam, _ [] return nil, nil, fmt.Errorf("fetch oid types: %w", err) } for i, oid := range stmtDesc.ParamOIDs { - inputType, ok := types[uint32(oid)] + inputType, ok := types[oid] if !ok { return nil, nil, fmt.Errorf("no postgres type name found for parameter %s with oid %d", query.ParamNames[i], oid) } @@ -187,12 +187,12 @@ func (inf *Inferrer) prepareTypes(query *ast.SourceQuery) (_a []InputParam, _ [] // Create output columns var outputColumns []OutputColumn for i, desc := range stmtDesc.Fields { - pgType, ok := outputTypes[uint32(desc.DataTypeOID)] + pgType, ok := outputTypes[desc.DataTypeOID] if !ok { - return nil, nil, fmt.Errorf("no postgrestype name found for column %s with oid %d", string(desc.Name), desc.DataTypeOID) + return nil, nil, fmt.Errorf("no postgrestype name found for column %s with oid %d", desc.Name, desc.DataTypeOID) } outputColumns = append(outputColumns, OutputColumn{ - PgName: string(desc.Name), + PgName: desc.Name, PgType: pgType, Nullable: nullables[i], }) @@ -215,7 +215,7 @@ func (inf *Inferrer) inferOutputNullability(query *ast.SourceQuery, descs []pgco for i, desc := range descs { if desc.TableOID > 0 { columnKeys[i] = pg.ColumnKey{ - TableOID: uint32(desc.TableOID), + TableOID: desc.TableOID, Number: desc.TableAttributeNumber, } } diff --git a/internal/pgtest/pg_test_db.go b/internal/pgtest/pg_test_db.go index 3c236362..c850d69f 100644 --- a/internal/pgtest/pg_test_db.go +++ b/internal/pgtest/pg_test_db.go @@ -11,6 +11,7 @@ import ( "time" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) // CleanupFunc deletes the schema and all database objects. @@ -53,6 +54,12 @@ func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Con if _, err := schemaConn.Exec(ctx, sql); err != nil { t.Fatalf("run sql: %s", err) } + if err := registerExtensionTypes(ctx, schemaConn); err != nil { + t.Fatalf("register extension types: %s", err) + } + if err := registerSchemaTypes(ctx, schemaConn); err != nil { + t.Fatalf("register schema types: %s", err) + } cleanup := func() { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) @@ -70,6 +77,79 @@ func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Con return schemaConn, cleanup } +func registerExtensionTypes(ctx context.Context, conn *pgx.Conn) error { + rows, err := conn.Query(ctx, ` +SELECT typname, oid +FROM pg_type +WHERE typname IN ('citext') + AND pg_type_is_visible(oid)`) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var name string + var oid uint32 + if err := rows.Scan(&name, &oid); err != nil { + return err + } + conn.TypeMap().RegisterType(&pgtype.Type{ + Name: name, + OID: oid, + Codec: pgtype.TextCodec{}, + }) + } + return rows.Err() +} + +func registerSchemaTypes(ctx context.Context, conn *pgx.Conn) error { + rows, err := conn.Query(ctx, ` +SELECT format('%I.%I', nsp.nspname, typ.typname) +FROM pg_type typ +JOIN pg_namespace nsp ON nsp.oid = typ.typnamespace +WHERE typ.typnamespace = current_schema()::regnamespace + AND (typ.typtype IN ('c', 'd', 'e') OR (typ.typtype = 'b' AND typ.typelem <> 0 AND typ.typname LIKE '\_%')) +ORDER BY typ.typname`) + if err != nil { + return err + } + var typeNames []string + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + rows.Close() + return err + } + typeNames = append(typeNames, name) + } + if err := rows.Err(); err != nil { + rows.Close() + return err + } + rows.Close() + + pending := typeNames + for len(pending) > 0 { + remaining := pending[:0] + loaded := 0 + for _, name := range pending { + dt, err := conn.LoadType(ctx, name) + if err != nil { + remaining = append(remaining, name) + continue + } + conn.TypeMap().RegisterType(dt) + loaded++ + } + if loaded == 0 { + return nil + } + pending = remaining + } + return nil +} + func postgresConnString() string { if connStr := os.Getenv("PGURL"); connStr != "" { return connStr From ebeee45a92af65b87a3bb2cc307454e4428216c6 Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 19:01:13 +0000 Subject: [PATCH 10/16] Restore generated type registration test coverage --- example/complex_params/query.sql_test.go | 6 ++ example/composite/query.sql_test.go | 23 ++++++ example/device/query.sql_test.go | 3 + example/enums/query.sql_test.go | 5 ++ example/function/query.sql_test.go | 1 + example/nested/query.sql_test.go | 3 + .../separate_out_dir/out/query.sql_test.go | 8 ++ example/syntax/query.sql_test.go | 8 +- internal/pgtest/pg_test_db.go | 81 ------------------- 9 files changed, 56 insertions(+), 82 deletions(-) diff --git a/example/complex_params/query.sql_test.go b/example/complex_params/query.sql_test.go index 64616694..42327d16 100644 --- a/example/complex_params/query.sql_test.go +++ b/example/complex_params/query.sql_test.go @@ -11,6 +11,7 @@ import ( func TestNewQuerier_ParamArrayInt(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() @@ -27,6 +28,7 @@ func TestNewQuerier_ParamArrayInt(t *testing.T) { func TestNewQuerier_ParamNested1(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() @@ -43,6 +45,7 @@ func TestNewQuerier_ParamNested1(t *testing.T) { func TestNewQuerier_ParamNested2(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() @@ -62,6 +65,7 @@ func TestNewQuerier_ParamNested2(t *testing.T) { func TestNewQuerier_ParamNested2Array(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() @@ -81,6 +85,7 @@ func TestNewQuerier_ParamNested2Array(t *testing.T) { func TestNewQuerier_ParamNested3(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() @@ -105,6 +110,7 @@ func TestNewQuerier_ParamNested3_QueryAllDataTypes(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() ctx := t.Context() + require.NoError(t, RegisterTypes(ctx, conn)) // dataTypes, err := QueryAllDataTypes(ctx, conn) // require.NoError(t, err) q := NewQuerier(conn) diff --git a/example/composite/query.sql_test.go b/example/composite/query.sql_test.go index 1724785a..1da4ea88 100644 --- a/example/composite/query.sql_test.go +++ b/example/composite/query.sql_test.go @@ -3,6 +3,7 @@ package composite import ( "testing" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/difftest" "github.com/jschaf/pggen/internal/pgtest" @@ -14,6 +15,8 @@ import ( func TestNewQuerier_SearchScreenshots(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + registerCitext(t, conn) + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) screenshotID := 99 @@ -61,6 +64,8 @@ func TestNewQuerier_SearchScreenshots(t *testing.T) { func TestNewQuerier_ArraysInput(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + registerCitext(t, conn) + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) @@ -80,6 +85,8 @@ func TestNewQuerier_ArraysInput(t *testing.T) { func TestNewQuerier_UserEmails(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + registerCitext(t, conn) + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) @@ -98,3 +105,19 @@ func insertScreenshotBlock(t *testing.T, q *DBQuerier, screenID int, body string require.NoError(t, err, "insert screenshot blocks") return row } + +func registerCitext(t *testing.T, conn *pgx.Conn) { + t.Helper() + row := conn.QueryRow(t.Context(), ` +SELECT oid +FROM pg_type +WHERE typname = 'citext' + AND pg_type_is_visible(oid)`) + var oid uint32 + require.NoError(t, row.Scan(&oid)) + conn.TypeMap().RegisterType(&pgtype.Type{ + Name: "citext", + OID: oid, + Codec: pgtype.TextCodec{}, + }) +} diff --git a/example/device/query.sql_test.go b/example/device/query.sql_test.go index b0d87518..571ac386 100644 --- a/example/device/query.sql_test.go +++ b/example/device/query.sql_test.go @@ -12,6 +12,7 @@ import ( func TestQuerier_FindDevicesByUser(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() userID := 18 @@ -38,6 +39,7 @@ func TestQuerier_FindDevicesByUser(t *testing.T) { func TestQuerier_CompositeUser(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() @@ -75,6 +77,7 @@ func TestQuerier_CompositeUser(t *testing.T) { func TestQuerier_CompositeUserOne(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() id := 15 diff --git a/example/enums/query.sql_test.go b/example/enums/query.sql_test.go index 70d9d906..ae39ba95 100644 --- a/example/enums/query.sql_test.go +++ b/example/enums/query.sql_test.go @@ -16,6 +16,7 @@ func TestNewQuerier_FindAllDevices(t *testing.T) { defer cancel() conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(ctx, conn)) q := NewQuerier(conn) mac, _ := net.ParseMAC("00:00:5e:00:53:01") @@ -49,6 +50,7 @@ func TestNewQuerier_FindOneDeviceArray(t *testing.T) { defer cancel() conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(ctx, conn)) q := NewQuerier(conn) @@ -64,6 +66,7 @@ func TestNewQuerier_FindManyDeviceArray(t *testing.T) { defer cancel() conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(ctx, conn)) q := NewQuerier(conn) @@ -79,6 +82,7 @@ func TestNewQuerier_FindManyDeviceArrayWithNum(t *testing.T) { defer cancel() conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(ctx, conn)) q := NewQuerier(conn) one, two := int32(1), int32(2) @@ -98,6 +102,7 @@ func TestNewQuerier_EnumInsideComposite(t *testing.T) { defer cancel() conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(ctx, conn)) q := NewQuerier(conn) mac, _ := net.ParseMAC("08:00:2b:01:02:03") diff --git a/example/function/query.sql_test.go b/example/function/query.sql_test.go index 4e290263..07ef3b58 100644 --- a/example/function/query.sql_test.go +++ b/example/function/query.sql_test.go @@ -13,6 +13,7 @@ import ( func TestNewQuerier_OutParams(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) diff --git a/example/nested/query.sql_test.go b/example/nested/query.sql_test.go index 4eeb4b7c..8d3612d0 100644 --- a/example/nested/query.sql_test.go +++ b/example/nested/query.sql_test.go @@ -5,11 +5,13 @@ import ( "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNewQuerier_ArrayNested2(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() @@ -30,6 +32,7 @@ func TestNewQuerier_ArrayNested2(t *testing.T) { func TestNewQuerier_Nested3(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() diff --git a/example/separate_out_dir/out/query.sql_test.go b/example/separate_out_dir/out/query.sql_test.go index c90e72fc..2e3db92d 100644 --- a/example/separate_out_dir/out/query.sql_test.go +++ b/example/separate_out_dir/out/query.sql_test.go @@ -12,6 +12,7 @@ import ( func TestNewQuerier_FindAuthorByID(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"../schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) @@ -27,6 +28,13 @@ func TestNewQuerier_FindAuthorByID(t *testing.T) { assert.Equal(t, "alpha", got) }) + t.Run("AlphaCompositeArray", func(t *testing.T) { + key := "key" + got, err := q.AlphaCompositeArray(t.Context()) + require.NoError(t, err) + assert.Equal(t, []Alpha{{Key: &key}}, got) + }) + t.Run("Bravo", func(t *testing.T) { got, err := q.Bravo(t.Context()) require.NoError(t, err) diff --git a/example/syntax/query.sql_test.go b/example/syntax/query.sql_test.go index 79041464..46676caa 100644 --- a/example/syntax/query.sql_test.go +++ b/example/syntax/query.sql_test.go @@ -5,11 +5,13 @@ import ( "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestQuerier(t *testing.T) { - conn, cleanup := pgtest.NewPostgresSchema(t, nil) + conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() + require.NoError(t, RegisterTypes(t.Context(), conn)) q := NewQuerier(conn) ctx := t.Context() @@ -32,4 +34,8 @@ func TestQuerier(t *testing.T) { val, err = q.BacktickBackslashN(ctx) assert.NoError(t, err, "BacktickBackslashN") assert.Equal(t, "`\\n", val, "BacktickBackslashN") + + enumVal, err := q.BadEnumName(ctx) + assert.NoError(t, err, "BadEnumName") + assert.Equal(t, UnnamedEnum123InconvertibleEnumName, enumVal, "BadEnumName") } diff --git a/internal/pgtest/pg_test_db.go b/internal/pgtest/pg_test_db.go index c850d69f..08703035 100644 --- a/internal/pgtest/pg_test_db.go +++ b/internal/pgtest/pg_test_db.go @@ -11,7 +11,6 @@ import ( "time" "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" ) // CleanupFunc deletes the schema and all database objects. @@ -54,13 +53,6 @@ func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Con if _, err := schemaConn.Exec(ctx, sql); err != nil { t.Fatalf("run sql: %s", err) } - if err := registerExtensionTypes(ctx, schemaConn); err != nil { - t.Fatalf("register extension types: %s", err) - } - if err := registerSchemaTypes(ctx, schemaConn); err != nil { - t.Fatalf("register schema types: %s", err) - } - cleanup := func() { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() @@ -77,79 +69,6 @@ func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Con return schemaConn, cleanup } -func registerExtensionTypes(ctx context.Context, conn *pgx.Conn) error { - rows, err := conn.Query(ctx, ` -SELECT typname, oid -FROM pg_type -WHERE typname IN ('citext') - AND pg_type_is_visible(oid)`) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var name string - var oid uint32 - if err := rows.Scan(&name, &oid); err != nil { - return err - } - conn.TypeMap().RegisterType(&pgtype.Type{ - Name: name, - OID: oid, - Codec: pgtype.TextCodec{}, - }) - } - return rows.Err() -} - -func registerSchemaTypes(ctx context.Context, conn *pgx.Conn) error { - rows, err := conn.Query(ctx, ` -SELECT format('%I.%I', nsp.nspname, typ.typname) -FROM pg_type typ -JOIN pg_namespace nsp ON nsp.oid = typ.typnamespace -WHERE typ.typnamespace = current_schema()::regnamespace - AND (typ.typtype IN ('c', 'd', 'e') OR (typ.typtype = 'b' AND typ.typelem <> 0 AND typ.typname LIKE '\_%')) -ORDER BY typ.typname`) - if err != nil { - return err - } - var typeNames []string - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - rows.Close() - return err - } - typeNames = append(typeNames, name) - } - if err := rows.Err(); err != nil { - rows.Close() - return err - } - rows.Close() - - pending := typeNames - for len(pending) > 0 { - remaining := pending[:0] - loaded := 0 - for _, name := range pending { - dt, err := conn.LoadType(ctx, name) - if err != nil { - remaining = append(remaining, name) - continue - } - conn.TypeMap().RegisterType(dt) - loaded++ - } - if loaded == 0 { - return nil - } - pending = remaining - } - return nil -} - func postgresConnString() string { if connStr := os.Getenv("PGURL"); connStr != "" { return connStr From 8b76a4d96d02afbdcd60e77f193e498bdc0965ec Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 19:16:03 +0000 Subject: [PATCH 11/16] Add pgx ltree array override coverage --- example/acceptance_test.go | 2 +- example/ltree/codegen_test.go | 2 +- example/ltree/query.sql.go | 12 +++++----- example/ltree/query.sql_test.go | 25 ++++++++++++++------ internal/codegen/golang/gotype/types.go | 21 +++++++++++++++- internal/codegen/golang/gotype/types_test.go | 13 ++++++++++ 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/example/acceptance_test.go b/example/acceptance_test.go index 8fac5cf5..cfe30a7e 100644 --- a/example/acceptance_test.go +++ b/example/acceptance_test.go @@ -180,7 +180,7 @@ func TestExamples(t *testing.T) { "--schema-glob", "example/ltree/schema.sql", "--query-glob", "example/ltree/query.sql", "--go-type", "ltree=github.com/jackc/pgx/v5/pgtype.Text", - "--go-type", "_ltree=[]string", + "--go-type", "_ltree=github.com/jackc/pgx/v5/pgtype.Array[pgtype.Text]", }, }, { diff --git a/example/ltree/codegen_test.go b/example/ltree/codegen_test.go index 689005a8..bc62aadf 100644 --- a/example/ltree/codegen_test.go +++ b/example/ltree/codegen_test.go @@ -25,7 +25,7 @@ func TestGenerate_Go_Example_ltree(t *testing.T) { InlineParamCount: 2, TypeOverrides: map[string]string{ "ltree": "github.com/jackc/pgx/v5/pgtype.Text", - "_ltree": "[]string", + "_ltree": "github.com/jackc/pgx/v5/pgtype.Array[pgtype.Text]", }, }) if err != nil { diff --git a/example/ltree/query.sql.go b/example/ltree/query.sql.go index ced1be19..d771c53d 100644 --- a/example/ltree/query.sql.go +++ b/example/ltree/query.sql.go @@ -16,7 +16,7 @@ type QueryName struct{} type Querier interface { FindTopScienceChildren(ctx context.Context) ([]pgtype.Text, error) - FindTopScienceChildrenAgg(ctx context.Context) ([]string, error) + FindTopScienceChildrenAgg(ctx context.Context) (pgtype.Array[pgtype.Text], error) InsertSampleData(ctx context.Context) (pgconn.CommandTag, error) @@ -95,14 +95,14 @@ FROM test WHERE path <@ 'Top.Science';` // FindTopScienceChildrenAgg implements Querier.FindTopScienceChildrenAgg. -func (q *DBQuerier) FindTopScienceChildrenAgg(ctx context.Context) ([]string, error) { +func (q *DBQuerier) FindTopScienceChildrenAgg(ctx context.Context) (pgtype.Array[pgtype.Text], error) { ctx = context.WithValue(ctx, QueryName{}, "FindTopScienceChildrenAgg") rows, err := q.conn.Query(ctx, findTopScienceChildrenAggSQL) if err != nil { - return nil, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) + return pgtype.Array[pgtype.Text]{}, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]string]) + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[pgtype.Array[pgtype.Text]]) } const insertSampleDataSQL = `INSERT INTO test @@ -142,8 +142,8 @@ const findLtreeInputSQL = `SELECT ($2::text[])::ltree[] AS text_arr;` type FindLtreeInputRow struct { - Ltree pgtype.Text `json:"ltree" db:"ltree"` - TextArr []string `json:"text_arr" db:"text_arr"` + Ltree pgtype.Text `json:"ltree" db:"ltree"` + TextArr pgtype.Array[pgtype.Text] `json:"text_arr" db:"text_arr"` } // FindLtreeInput implements Querier.FindLtreeInput. diff --git a/example/ltree/query.sql_test.go b/example/ltree/query.sql_test.go index 7e2369fe..14041b45 100644 --- a/example/ltree/query.sql_test.go +++ b/example/ltree/query.sql_test.go @@ -35,11 +35,15 @@ func TestQuerier(t *testing.T) { { rows, err := q.FindTopScienceChildrenAgg(ctx) require.NoError(t, err) - want := []string{ - "Top.Science", - "Top.Science.Astronomy", - "Top.Science.Astronomy.Astrophysics", - "Top.Science.Astronomy.Cosmology", + want := pgtype.Array[pgtype.Text]{ + Elements: []pgtype.Text{ + {String: "Top.Science", Valid: true}, + {String: "Top.Science.Astronomy", Valid: true}, + {String: "Top.Science.Astronomy.Astrophysics", Valid: true}, + {String: "Top.Science.Astronomy.Cosmology", Valid: true}, + }, + Dims: []pgtype.ArrayDimension{{Length: 4, LowerBound: 1}}, + Valid: true, } assert.Equal(t, want, rows) } @@ -50,8 +54,15 @@ func TestQuerier(t *testing.T) { rows, err := q.FindLtreeInput(ctx, in1, in2) require.NoError(t, err) assert.Equal(t, FindLtreeInputRow{ - Ltree: in1, - TextArr: in2, + Ltree: in1, + TextArr: pgtype.Array[pgtype.Text]{ + Elements: []pgtype.Text{ + {String: "qux", Valid: true}, + {String: "qux", Valid: true}, + }, + Dims: []pgtype.ArrayDimension{{Length: 2, LowerBound: 1}}, + Valid: true, + }, }, rows) } } diff --git a/internal/codegen/golang/gotype/types.go b/internal/codegen/golang/gotype/types.go index b74a6a98..e02b0808 100644 --- a/internal/codegen/golang/gotype/types.go +++ b/internal/codegen/golang/gotype/types.go @@ -206,7 +206,7 @@ func ParseOpaqueType(qualType string, pgType pg.Type) (Type, error) { if isPtr { bs = bs[1:] } - idx := bytes.LastIndexByte(bs, '.') + idx := lastIndexByteOutsideBrackets(bs, '.') name := string(bs[idx+1:]) var typ Type = &OpaqueType{Name: name} // On array types, the PgType goes on the Array. In all other cases, it @@ -240,6 +240,25 @@ func ParseOpaqueType(qualType string, pgType pg.Type) (Type, error) { return typ, nil } +func lastIndexByteOutsideBrackets(bs []byte, ch byte) int { + depth := 0 + for i := len(bs) - 1; i >= 0; i-- { + switch bs[i] { + case ']': + depth++ + case '[': + if depth > 0 { + depth-- + } + case ch: + if depth == 0 { + return i + } + } + } + return -1 +} + // MustParseKnownType creates a gotype.Type by parsing a fully qualified Go type // that pgx supports natively like "github.com/jackc/pgx/v5/pgtype.Int4Array", or most // builtin types like "string" and []*int16. diff --git a/internal/codegen/golang/gotype/types_test.go b/internal/codegen/golang/gotype/types_test.go index 485cb205..207b9d77 100644 --- a/internal/codegen/golang/gotype/types_test.go +++ b/internal/codegen/golang/gotype/types_test.go @@ -55,6 +55,13 @@ func TestMustParseKnownType(t *testing.T) { Elem: &ImportType{PkgPath: "util/custom/times", Type: &OpaqueType{Name: "Interval"}}, }, }, + { + qualType: "github.com/jackc/pgx/v5/pgtype.Array[pgtype.Text]", + want: &ImportType{ + PkgPath: "github.com/jackc/pgx/v5/pgtype", + Type: &OpaqueType{Name: "Array[pgtype.Text]"}, + }, + }, } for _, tt := range tests { t.Run(tt.qualType, func(t *testing.T) { @@ -115,6 +122,12 @@ func TestQualifyType(t *testing.T) { otherPkg: "example.com/foo", want: "[]Bar", }, + { + name: "github.com/jackc/pgx/v5/pgtype.Array[pgtype.Text] - example.com/foo", + typ: &ImportType{PkgPath: "github.com/jackc/pgx/v5/pgtype", Type: &OpaqueType{Name: "Array[pgtype.Text]"}}, + otherPkg: "example.com/foo", + want: "pgtype.Array[pgtype.Text]", + }, } for _, tt := range tests { From 47c914855b558509371e9a1a94a4325f89ee1a37 Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 19:33:43 +0000 Subject: [PATCH 12/16] Trim blank declarer output --- example/complex_params/query.sql.go | 18 ------------ example/composite/query.sql.go | 8 ----- example/device/query.sql.go | 2 -- example/enums/query.sql.go | 2 -- example/function/query.sql.go | 2 -- example/nested/query.sql.go | 2 -- .../separate_out_dir/out/alpha_query.sql.0.go | 2 -- example/slices/query.sql.go | 4 --- internal/codegen/golang/declarer.go | 15 ++++++++++ internal/codegen/golang/declarer_test.go | 29 ++++--------------- internal/codegen/golang/generate.go | 4 ++- internal/codegen/golang/query.gotemplate | 2 +- .../testdata/declarer_composite.input.golden | 7 +---- .../testdata/declarer_composite.output.golden | 2 -- .../declarer_composite_array.input.golden | 9 +----- .../declarer_composite_array.output.golden | 2 -- .../declarer_composite_enum.input.golden | 7 +---- .../declarer_composite_enum.output.golden | 2 -- .../declarer_composite_nested.input.golden | 9 +----- .../declarer_composite_nested.output.golden | 2 -- internal/pg/query.sql.go | 2 -- 21 files changed, 29 insertions(+), 103 deletions(-) diff --git a/example/complex_params/query.sql.go b/example/complex_params/query.sql.go index 6f4c9e84..e881273a 100644 --- a/example/complex_params/query.sql.go +++ b/example/complex_params/query.sql.go @@ -95,32 +95,14 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"dimensions\"") - - - - var _ = addTypeToRegister("\"product_image_set_type\"") - - - - var _ = addTypeToRegister("\"product_image_type\"") - - - - var _ = addTypeToRegister("\"_product_image_type\"") - - - - const paramArrayIntSQL = `SELECT $1::bigint[];` // ParamArrayInt implements Querier.ParamArrayInt. diff --git a/example/composite/query.sql.go b/example/composite/query.sql.go index 87632eb1..94dc3ec1 100644 --- a/example/composite/query.sql.go +++ b/example/composite/query.sql.go @@ -98,22 +98,14 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"arrays\"") - - - - var _ = addTypeToRegister("\"blocks\"") var _ = addTypeToRegister("\"user_email\"") var _ = addTypeToRegister("\"_blocks\"") - - const searchScreenshotsSQL = `SELECT ss.id, array_agg(bl) AS blocks diff --git a/example/device/query.sql.go b/example/device/query.sql.go index 85b9ee91..2cffae10 100644 --- a/example/device/query.sql.go +++ b/example/device/query.sql.go @@ -101,8 +101,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"device_type\"") var _ = addTypeToRegister("\"user\"") diff --git a/example/enums/query.sql.go b/example/enums/query.sql.go index f361dada..fe1f8848 100644 --- a/example/enums/query.sql.go +++ b/example/enums/query.sql.go @@ -103,8 +103,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"device_type\"") var _ = addTypeToRegister("\"device\"") diff --git a/example/function/query.sql.go b/example/function/query.sql.go index d571f1cb..eb07b9c1 100644 --- a/example/function/query.sql.go +++ b/example/function/query.sql.go @@ -80,8 +80,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"list_item\"") var _ = addTypeToRegister("\"list_stats\"") diff --git a/example/nested/query.sql.go b/example/nested/query.sql.go index 5dbc94ad..a355faf8 100644 --- a/example/nested/query.sql.go +++ b/example/nested/query.sql.go @@ -89,8 +89,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"dimensions\"") var _ = addTypeToRegister("\"product_image_set_type\"") diff --git a/example/separate_out_dir/out/alpha_query.sql.0.go b/example/separate_out_dir/out/alpha_query.sql.0.go index 2b724fc5..a23c0727 100644 --- a/example/separate_out_dir/out/alpha_query.sql.0.go +++ b/example/separate_out_dir/out/alpha_query.sql.0.go @@ -79,8 +79,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"alpha\"") var _ = addTypeToRegister("\"_alpha\"") diff --git a/example/slices/query.sql.go b/example/slices/query.sql.go index 629503bb..2fb73c70 100644 --- a/example/slices/query.sql.go +++ b/example/slices/query.sql.go @@ -75,10 +75,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - - - const getBoolsSQL = `SELECT $1::boolean[];` // GetBools implements Querier.GetBools. diff --git a/internal/codegen/golang/declarer.go b/internal/codegen/golang/declarer.go index e35ed33e..61ca79fe 100644 --- a/internal/codegen/golang/declarer.go +++ b/internal/codegen/golang/declarer.go @@ -44,6 +44,21 @@ func (d DeclarerSet) ListAll() []Declarer { return decls } +func emitDeclarers(decls []Declarer, pkgPath string) (string, error) { + declarations := make([]string, 0, len(decls)) + for _, decl := range decls { + declaration, err := decl.Declare(pkgPath) + if err != nil { + return "", err + } + if strings.TrimSpace(declaration) == "" { + continue + } + declarations = append(declarations, declaration) + } + return strings.Join(declarations, "\n\n"), nil +} + func pgTypeNameForLoadType(name string) string { return `"` + strings.ReplaceAll(name, `"`, `""`) + `"` } diff --git a/internal/codegen/golang/declarer_test.go b/internal/codegen/golang/declarer_test.go index e1c141f5..96c4ef3a 100644 --- a/internal/codegen/golang/declarer_test.go +++ b/internal/codegen/golang/declarer_test.go @@ -3,7 +3,6 @@ package golang import ( "flag" "os" - "strings" "testing" "github.com/jschaf/pggen/internal/casing" @@ -132,18 +131,10 @@ func TestDeclarers(t *testing.T) { t.Run(tt.name+"_input", func(t *testing.T) { golden := "testdata/declarer_" + tt.name + ".input.golden" decls := FindInputDeclarers(tt.typ).ListAll() - sb := &strings.Builder{} - for i, decl := range decls { - s, err := decl.Declare(tt.pkgPath) - if err != nil { - t.Fatal(err) - } - sb.WriteString(s) - if i < len(decls)-1 { - sb.WriteString("\n\n") - } + got, err := emitDeclarers(decls, tt.pkgPath) + if err != nil { + t.Fatal(err) } - got := sb.String() if *update { err := os.WriteFile(golden, []byte(got), 0o600) @@ -159,18 +150,10 @@ func TestDeclarers(t *testing.T) { t.Run(tt.name+"_output", func(t *testing.T) { golden := "testdata/declarer_" + tt.name + ".output.golden" decls := FindOutputDeclarers(tt.typ).ListAll() - sb := &strings.Builder{} - for i, decl := range decls { - s, err := decl.Declare(tt.pkgPath) - if err != nil { - t.Fatal(err) - } - sb.WriteString(s) - if i < len(decls)-1 { - sb.WriteString("\n\n") - } + got, err := emitDeclarers(decls, tt.pkgPath) + if err != nil { + t.Fatal(err) } - got := sb.String() if *update { err := os.WriteFile(golden, []byte(got), 0o600) diff --git a/internal/codegen/golang/generate.go b/internal/codegen/golang/generate.go index ffb9726f..080f2036 100644 --- a/internal/codegen/golang/generate.go +++ b/internal/codegen/golang/generate.go @@ -71,7 +71,9 @@ func Generate(opts GenerateOptions, queryFiles []codegen.QueryFile) error { var queryTemplate string func parseQueryTemplate() (*template.Template, error) { - tmpl, err := template.New("gen_query").Parse(queryTemplate) + tmpl, err := template.New("gen_query").Funcs(template.FuncMap{ + "emitDeclarers": emitDeclarers, + }).Parse(queryTemplate) if err != nil { return nil, fmt.Errorf("parse query.gotemplate: %w", err) } diff --git a/internal/codegen/golang/query.gotemplate b/internal/codegen/golang/query.gotemplate index b1dafd3f..4b8729e8 100644 --- a/internal/codegen/golang/query.gotemplate +++ b/internal/codegen/golang/query.gotemplate @@ -44,7 +44,7 @@ func NewQuerier(conn genericConn) *DBQuerier { return &DBQuerier{conn: conn} } -{{- range .Declarers}}{{- "\n\n" -}}{{ .Declare $.PkgPath }}{{ end -}} +{{- with emitDeclarers .Declarers $.PkgPath }}{{- "\n\n" -}}{{ . }}{{- end -}} {{- end -}} {{- range $i, $q := .Queries -}} diff --git a/internal/codegen/golang/testdata/declarer_composite.input.golden b/internal/codegen/golang/testdata/declarer_composite.input.golden index 690e99a7..2b86b954 100644 --- a/internal/codegen/golang/testdata/declarer_composite.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite.input.golden @@ -38,9 +38,4 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - -var _ = addTypeToRegister("\"some_table\"") - - - +var _ = addTypeToRegister("\"some_table\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite.output.golden b/internal/codegen/golang/testdata/declarer_composite.output.golden index 49f809b6..2b86b954 100644 --- a/internal/codegen/golang/testdata/declarer_composite.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite.output.golden @@ -38,6 +38,4 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"some_table\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_array.input.golden b/internal/codegen/golang/testdata/declarer_composite_array.input.golden index 6c4e9a20..2ca012c7 100644 --- a/internal/codegen/golang/testdata/declarer_composite_array.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite_array.input.golden @@ -38,13 +38,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"some_table\"") - - -var _ = addTypeToRegister("\"_some_array\"") - - - +var _ = addTypeToRegister("\"_some_array\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_array.output.golden b/internal/codegen/golang/testdata/declarer_composite_array.output.golden index 31714f83..2ca012c7 100644 --- a/internal/codegen/golang/testdata/declarer_composite_array.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite_array.output.golden @@ -38,8 +38,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"some_table\"") var _ = addTypeToRegister("\"_some_array\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_enum.input.golden b/internal/codegen/golang/testdata/declarer_composite_enum.input.golden index 12afb781..f5430b1b 100644 --- a/internal/codegen/golang/testdata/declarer_composite_enum.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite_enum.input.golden @@ -47,11 +47,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"device_type\"") -var _ = addTypeToRegister("\"some_table_enum\"") - - - +var _ = addTypeToRegister("\"some_table_enum\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_enum.output.golden b/internal/codegen/golang/testdata/declarer_composite_enum.output.golden index 5dcdb847..f5430b1b 100644 --- a/internal/codegen/golang/testdata/declarer_composite_enum.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite_enum.output.golden @@ -47,8 +47,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"device_type\"") var _ = addTypeToRegister("\"some_table_enum\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_nested.input.golden b/internal/codegen/golang/testdata/declarer_composite_nested.input.golden index 034a967e..4fc1b1d3 100644 --- a/internal/codegen/golang/testdata/declarer_composite_nested.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite_nested.input.golden @@ -43,13 +43,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"foo_type\"") - - -var _ = addTypeToRegister("\"some_table_nested\"") - - - +var _ = addTypeToRegister("\"some_table_nested\"") \ No newline at end of file diff --git a/internal/codegen/golang/testdata/declarer_composite_nested.output.golden b/internal/codegen/golang/testdata/declarer_composite_nested.output.golden index c7658db8..4fc1b1d3 100644 --- a/internal/codegen/golang/testdata/declarer_composite_nested.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite_nested.output.golden @@ -43,8 +43,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - var _ = addTypeToRegister("\"foo_type\"") var _ = addTypeToRegister("\"some_table_nested\"") \ No newline at end of file diff --git a/internal/pg/query.sql.go b/internal/pg/query.sql.go index 815a8c72..e07077b7 100644 --- a/internal/pg/query.sql.go +++ b/internal/pg/query.sql.go @@ -85,8 +85,6 @@ func addTypeToRegister(typ string) struct{} { return struct{}{} } - - const findEnumTypesSQL = `WITH enums AS ( SELECT enumtypid::int8 AS enum_type, From ddc4ef26b84cecc040fb364c1317a04d583c16a0 Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 21:11:52 +0000 Subject: [PATCH 13/16] Fix zero returns for custom scalar results --- Makefile | 3 +++ example/author/query.sql.go | 24 +++++++++++------ example/complex_params/query.sql.go | 15 +++++++---- example/composite/query.sql.go | 15 +++++++---- example/custom_types/query.sql | 3 +++ example/custom_types/query.sql.go | 25 ++++++++++++++--- example/custom_types/query.sql_test.go | 11 ++++++++ example/device/query.sql.go | 15 +++++++---- example/domain/query.sql.go | 3 ++- example/enums/query.sql.go | 15 +++++++---- example/erp/order/customer.sql.go | 15 +++++++---- example/erp/order/price.sql.go | 6 +++-- example/function/query.sql.go | 3 ++- example/go_pointer_types/query.sql.go | 18 ++++++++----- .../inline_param_count/inline0/query.sql.go | 9 ++++--- .../inline_param_count/inline1/query.sql.go | 9 ++++--- .../inline_param_count/inline2/query.sql.go | 9 ++++--- .../inline_param_count/inline3/query.sql.go | 9 ++++--- example/ltree/query.sql.go | 9 ++++--- example/nested/query.sql.go | 6 +++-- example/pgcrypto/query.sql.go | 3 ++- .../separate_out_dir/out/alpha_query.sql.0.go | 6 +++-- .../separate_out_dir/out/alpha_query.sql.1.go | 3 ++- .../separate_out_dir/out/bravo_query.sql.go | 3 ++- example/slices/query.sql.go | 12 ++++++--- example/syntax/query.sql.go | 27 ++++++++++++------- example/void/query.sql.go | 9 ++++--- internal/codegen/golang/query.gotemplate | 3 ++- internal/codegen/golang/templated_file.go | 11 +++++++- internal/pg/query.sql.go | 21 ++++++++++----- 30 files changed, 227 insertions(+), 93 deletions(-) diff --git a/Makefile b/Makefile index 6de01999..282c8f72 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,9 @@ ldflags := -ldflags "-X 'main.version=${version}' -X 'main.commit=${commit}'" .PHONY: all all: lint test acceptance-test +.PHONY: check +check: lint test update-acceptance-test + .PHONY: start start: docker-compose up -d diff --git a/example/author/query.sql.go b/example/author/query.sql.go index f950e443..45ed19d2 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -112,7 +112,8 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) if err != nil { - return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) @@ -132,7 +133,8 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu ctx = context.WithValue(ctx, QueryName{}, "FindAuthors") rows, err := q.conn.Query(ctx, findAuthorsSQL, firstName) if err != nil { - return nil, fmt.Errorf("query FindAuthors: %w", err) + var zero []FindAuthorsRow + return zero, fmt.Errorf("query FindAuthors: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorsRow]) @@ -150,7 +152,8 @@ func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]Find ctx = context.WithValue(ctx, QueryName{}, "FindAuthorNames") rows, err := q.conn.Query(ctx, findAuthorNamesSQL, authorID) if err != nil { - return nil, fmt.Errorf("query FindAuthorNames: %w", err) + var zero []FindAuthorNamesRow + return zero, fmt.Errorf("query FindAuthorNames: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorNamesRow]) @@ -163,7 +166,8 @@ func (q *DBQuerier) FindFirstNames(ctx context.Context, authorID int32) ([]*stri ctx = context.WithValue(ctx, QueryName{}, "FindFirstNames") rows, err := q.conn.Query(ctx, findFirstNamesSQL, authorID) if err != nil { - return nil, fmt.Errorf("query FindFirstNames: %w", err) + var zero []*string + return zero, fmt.Errorf("query FindFirstNames: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[*string]) @@ -224,7 +228,8 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") rows, err := q.conn.Query(ctx, insertAuthorSQL, firstName, lastName) if err != nil { - return 0, fmt.Errorf("query InsertAuthor: %w", err) + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) @@ -252,7 +257,8 @@ func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorS ctx = context.WithValue(ctx, QueryName{}, "InsertAuthorSuffix") rows, err := q.conn.Query(ctx, insertAuthorSuffixSQL, params.FirstName, params.LastName, params.Suffix) if err != nil { - return InsertAuthorSuffixRow{}, fmt.Errorf("query InsertAuthorSuffix: %w", err) + var zero InsertAuthorSuffixRow + return zero, fmt.Errorf("query InsertAuthorSuffix: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertAuthorSuffixRow]) @@ -265,7 +271,8 @@ func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*st ctx = context.WithValue(ctx, QueryName{}, "StringAggFirstName") rows, err := q.conn.Query(ctx, stringAggFirstNameSQL, authorID) if err != nil { - return nil, fmt.Errorf("query StringAggFirstName: %w", err) + var zero *string + return zero, fmt.Errorf("query StringAggFirstName: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*string]) @@ -278,7 +285,8 @@ func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]st ctx = context.WithValue(ctx, QueryName{}, "ArrayAggFirstName") rows, err := q.conn.Query(ctx, arrayAggFirstNameSQL, authorID) if err != nil { - return nil, fmt.Errorf("query ArrayAggFirstName: %w", err) + var zero []string + return zero, fmt.Errorf("query ArrayAggFirstName: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]string]) diff --git a/example/complex_params/query.sql.go b/example/complex_params/query.sql.go index e881273a..b4aae0f7 100644 --- a/example/complex_params/query.sql.go +++ b/example/complex_params/query.sql.go @@ -110,7 +110,8 @@ func (q *DBQuerier) ParamArrayInt(ctx context.Context, ints []int) ([]int, error ctx = context.WithValue(ctx, QueryName{}, "ParamArrayInt") rows, err := q.conn.Query(ctx, paramArrayIntSQL, ints) if err != nil { - return nil, fmt.Errorf("query ParamArrayInt: %w", err) + var zero []int + return zero, fmt.Errorf("query ParamArrayInt: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]int]) @@ -123,7 +124,8 @@ func (q *DBQuerier) ParamNested1(ctx context.Context, dimensions Dimensions) (Di ctx = context.WithValue(ctx, QueryName{}, "ParamNested1") rows, err := q.conn.Query(ctx, paramNested1SQL, dimensions) if err != nil { - return Dimensions{}, fmt.Errorf("query ParamNested1: %w", err) + var zero Dimensions + return zero, fmt.Errorf("query ParamNested1: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Dimensions]) @@ -136,7 +138,8 @@ func (q *DBQuerier) ParamNested2(ctx context.Context, image ProductImageType) (P ctx = context.WithValue(ctx, QueryName{}, "ParamNested2") rows, err := q.conn.Query(ctx, paramNested2SQL, image) if err != nil { - return ProductImageType{}, fmt.Errorf("query ParamNested2: %w", err) + var zero ProductImageType + return zero, fmt.Errorf("query ParamNested2: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[ProductImageType]) @@ -149,7 +152,8 @@ func (q *DBQuerier) ParamNested2Array(ctx context.Context, images []ProductImage ctx = context.WithValue(ctx, QueryName{}, "ParamNested2Array") rows, err := q.conn.Query(ctx, paramNested2ArraySQL, images) if err != nil { - return nil, fmt.Errorf("query ParamNested2Array: %w", err) + var zero []ProductImageType + return zero, fmt.Errorf("query ParamNested2Array: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]ProductImageType]) @@ -162,7 +166,8 @@ func (q *DBQuerier) ParamNested3(ctx context.Context, imageSet ProductImageSetTy ctx = context.WithValue(ctx, QueryName{}, "ParamNested3") rows, err := q.conn.Query(ctx, paramNested3SQL, imageSet) if err != nil { - return ProductImageSetType{}, fmt.Errorf("query ParamNested3: %w", err) + var zero ProductImageSetType + return zero, fmt.Errorf("query ParamNested3: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[ProductImageSetType]) diff --git a/example/composite/query.sql.go b/example/composite/query.sql.go index 94dc3ec1..e13e59b8 100644 --- a/example/composite/query.sql.go +++ b/example/composite/query.sql.go @@ -132,7 +132,8 @@ func (q *DBQuerier) SearchScreenshots(ctx context.Context, params SearchScreensh ctx = context.WithValue(ctx, QueryName{}, "SearchScreenshots") rows, err := q.conn.Query(ctx, searchScreenshotsSQL, params.Body, params.Limit, params.Offset) if err != nil { - return nil, fmt.Errorf("query SearchScreenshots: %w", err) + var zero []SearchScreenshotsRow + return zero, fmt.Errorf("query SearchScreenshots: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[SearchScreenshotsRow]) @@ -158,7 +159,8 @@ func (q *DBQuerier) SearchScreenshotsOneCol(ctx context.Context, params SearchSc ctx = context.WithValue(ctx, QueryName{}, "SearchScreenshotsOneCol") rows, err := q.conn.Query(ctx, searchScreenshotsOneColSQL, params.Body, params.Limit, params.Offset) if err != nil { - return nil, fmt.Errorf("query SearchScreenshotsOneCol: %w", err) + var zero [][]Blocks + return zero, fmt.Errorf("query SearchScreenshotsOneCol: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[[]Blocks]) @@ -184,7 +186,8 @@ func (q *DBQuerier) InsertScreenshotBlocks(ctx context.Context, screenshotID int ctx = context.WithValue(ctx, QueryName{}, "InsertScreenshotBlocks") rows, err := q.conn.Query(ctx, insertScreenshotBlocksSQL, screenshotID, body) if err != nil { - return InsertScreenshotBlocksRow{}, fmt.Errorf("query InsertScreenshotBlocks: %w", err) + var zero InsertScreenshotBlocksRow + return zero, fmt.Errorf("query InsertScreenshotBlocks: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertScreenshotBlocksRow]) @@ -197,7 +200,8 @@ func (q *DBQuerier) ArraysInput(ctx context.Context, arrays Arrays) (Arrays, err ctx = context.WithValue(ctx, QueryName{}, "ArraysInput") rows, err := q.conn.Query(ctx, arraysInputSQL, arrays) if err != nil { - return Arrays{}, fmt.Errorf("query ArraysInput: %w", err) + var zero Arrays + return zero, fmt.Errorf("query ArraysInput: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Arrays]) @@ -210,7 +214,8 @@ func (q *DBQuerier) UserEmails(ctx context.Context) (UserEmail, error) { ctx = context.WithValue(ctx, QueryName{}, "UserEmails") rows, err := q.conn.Query(ctx, userEmailsSQL) if err != nil { - return UserEmail{}, fmt.Errorf("query UserEmails: %w", err) + var zero UserEmail + return zero, fmt.Errorf("query UserEmails: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[UserEmail]) diff --git a/example/custom_types/query.sql b/example/custom_types/query.sql index 653aa7ed..c4050ed4 100644 --- a/example/custom_types/query.sql +++ b/example/custom_types/query.sql @@ -1,6 +1,9 @@ -- name: CustomTypes :one SELECT 'some_text', 1::bigint; +-- name: CustomString :one +SELECT 'some_text'::text; + -- name: CustomMyInt :one SELECT '5'::my_int as int5; diff --git a/example/custom_types/query.sql.go b/example/custom_types/query.sql.go index 6b4dbc74..32c5ee4f 100644 --- a/example/custom_types/query.sql.go +++ b/example/custom_types/query.sql.go @@ -16,6 +16,8 @@ type QueryName struct{} type Querier interface { CustomTypes(ctx context.Context) (CustomTypesRow, error) + CustomString(ctx context.Context) (mytype.String, error) + CustomMyInt(ctx context.Context) (int, error) IntArray(ctx context.Context) ([][]int32, error) @@ -85,12 +87,27 @@ func (q *DBQuerier) CustomTypes(ctx context.Context) (CustomTypesRow, error) { ctx = context.WithValue(ctx, QueryName{}, "CustomTypes") rows, err := q.conn.Query(ctx, customTypesSQL) if err != nil { - return CustomTypesRow{}, fmt.Errorf("query CustomTypes: %w", err) + var zero CustomTypesRow + return zero, fmt.Errorf("query CustomTypes: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CustomTypesRow]) } +const customStringSQL = `SELECT 'some_text'::text;` + +// CustomString implements Querier.CustomString. +func (q *DBQuerier) CustomString(ctx context.Context) (mytype.String, error) { + ctx = context.WithValue(ctx, QueryName{}, "CustomString") + rows, err := q.conn.Query(ctx, customStringSQL) + if err != nil { + var zero mytype.String + return zero, fmt.Errorf("query CustomString: %w", err) + } + + return pgx.CollectExactlyOneRow(rows, pgx.RowTo[mytype.String]) +} + const customMyIntSQL = `SELECT '5'::my_int as int5;` // CustomMyInt implements Querier.CustomMyInt. @@ -98,7 +115,8 @@ func (q *DBQuerier) CustomMyInt(ctx context.Context) (int, error) { ctx = context.WithValue(ctx, QueryName{}, "CustomMyInt") rows, err := q.conn.Query(ctx, customMyIntSQL) if err != nil { - return 0, fmt.Errorf("query CustomMyInt: %w", err) + var zero int + return zero, fmt.Errorf("query CustomMyInt: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]) @@ -111,7 +129,8 @@ func (q *DBQuerier) IntArray(ctx context.Context) ([][]int32, error) { ctx = context.WithValue(ctx, QueryName{}, "IntArray") rows, err := q.conn.Query(ctx, intArraySQL) if err != nil { - return nil, fmt.Errorf("query IntArray: %w", err) + var zero [][]int32 + return zero, fmt.Errorf("query IntArray: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[[]int32]) diff --git a/example/custom_types/query.sql_test.go b/example/custom_types/query.sql_test.go index e2153007..77d8d422 100644 --- a/example/custom_types/query.sql_test.go +++ b/example/custom_types/query.sql_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/jackc/pgx/v5/pgtype" + "github.com/jschaf/pggen/example/custom_types/mytype" "github.com/jschaf/pggen/internal/pgtest" "github.com/jschaf/pggen/internal/texts" "github.com/stretchr/testify/assert" @@ -27,6 +28,16 @@ func TestQuerier_CustomTypes(t *testing.T) { }) } +func TestQuerier_CustomString(t *testing.T) { + conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) + defer cleanup() + q := NewQuerier(conn) + + val, err := q.CustomString(t.Context()) + require.NoError(t, err) + assert.Equal(t, mytype.String("some_text"), val) +} + func TestQuerier_CustomMyInt(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() diff --git a/example/device/query.sql.go b/example/device/query.sql.go index 2cffae10..73c7cb3a 100644 --- a/example/device/query.sql.go +++ b/example/device/query.sql.go @@ -123,7 +123,8 @@ func (q *DBQuerier) FindDevicesByUser(ctx context.Context, id int) ([]FindDevice ctx = context.WithValue(ctx, QueryName{}, "FindDevicesByUser") rows, err := q.conn.Query(ctx, findDevicesByUserSQL, id) if err != nil { - return nil, fmt.Errorf("query FindDevicesByUser: %w", err) + var zero []FindDevicesByUserRow + return zero, fmt.Errorf("query FindDevicesByUser: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindDevicesByUserRow]) @@ -147,7 +148,8 @@ func (q *DBQuerier) CompositeUser(ctx context.Context) ([]CompositeUserRow, erro ctx = context.WithValue(ctx, QueryName{}, "CompositeUser") rows, err := q.conn.Query(ctx, compositeUserSQL) if err != nil { - return nil, fmt.Errorf("query CompositeUser: %w", err) + var zero []CompositeUserRow + return zero, fmt.Errorf("query CompositeUser: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[CompositeUserRow]) @@ -160,7 +162,8 @@ func (q *DBQuerier) CompositeUserOne(ctx context.Context) (User, error) { ctx = context.WithValue(ctx, QueryName{}, "CompositeUserOne") rows, err := q.conn.Query(ctx, compositeUserOneSQL) if err != nil { - return User{}, fmt.Errorf("query CompositeUserOne: %w", err) + var zero User + return zero, fmt.Errorf("query CompositeUserOne: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[User]) @@ -178,7 +181,8 @@ func (q *DBQuerier) CompositeUserOneTwoCols(ctx context.Context) (CompositeUserO ctx = context.WithValue(ctx, QueryName{}, "CompositeUserOneTwoCols") rows, err := q.conn.Query(ctx, compositeUserOneTwoColsSQL) if err != nil { - return CompositeUserOneTwoColsRow{}, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) + var zero CompositeUserOneTwoColsRow + return zero, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CompositeUserOneTwoColsRow]) @@ -191,7 +195,8 @@ func (q *DBQuerier) CompositeUserMany(ctx context.Context) ([]User, error) { ctx = context.WithValue(ctx, QueryName{}, "CompositeUserMany") rows, err := q.conn.Query(ctx, compositeUserManySQL) if err != nil { - return nil, fmt.Errorf("query CompositeUserMany: %w", err) + var zero []User + return zero, fmt.Errorf("query CompositeUserMany: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[User]) diff --git a/example/domain/query.sql.go b/example/domain/query.sql.go index 059565e4..333ca36c 100644 --- a/example/domain/query.sql.go +++ b/example/domain/query.sql.go @@ -75,7 +75,8 @@ func (q *DBQuerier) DomainOne(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "DomainOne") rows, err := q.conn.Query(ctx, domainOneSQL) if err != nil { - return "", fmt.Errorf("query DomainOne: %w", err) + var zero string + return zero, fmt.Errorf("query DomainOne: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) diff --git a/example/enums/query.sql.go b/example/enums/query.sql.go index fe1f8848..e7b266f6 100644 --- a/example/enums/query.sql.go +++ b/example/enums/query.sql.go @@ -122,7 +122,8 @@ func (q *DBQuerier) FindAllDevices(ctx context.Context) ([]FindAllDevicesRow, er ctx = context.WithValue(ctx, QueryName{}, "FindAllDevices") rows, err := q.conn.Query(ctx, findAllDevicesSQL) if err != nil { - return nil, fmt.Errorf("query FindAllDevices: %w", err) + var zero []FindAllDevicesRow + return zero, fmt.Errorf("query FindAllDevices: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindAllDevicesRow]) @@ -148,7 +149,8 @@ func (q *DBQuerier) FindOneDeviceArray(ctx context.Context) ([]DeviceType, error ctx = context.WithValue(ctx, QueryName{}, "FindOneDeviceArray") rows, err := q.conn.Query(ctx, findOneDeviceArraySQL) if err != nil { - return nil, fmt.Errorf("query FindOneDeviceArray: %w", err) + var zero []DeviceType + return zero, fmt.Errorf("query FindOneDeviceArray: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]DeviceType]) @@ -163,7 +165,8 @@ func (q *DBQuerier) FindManyDeviceArray(ctx context.Context) ([][]DeviceType, er ctx = context.WithValue(ctx, QueryName{}, "FindManyDeviceArray") rows, err := q.conn.Query(ctx, findManyDeviceArraySQL) if err != nil { - return nil, fmt.Errorf("query FindManyDeviceArray: %w", err) + var zero [][]DeviceType + return zero, fmt.Errorf("query FindManyDeviceArray: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[[]DeviceType]) @@ -183,7 +186,8 @@ func (q *DBQuerier) FindManyDeviceArrayWithNum(ctx context.Context) ([]FindManyD ctx = context.WithValue(ctx, QueryName{}, "FindManyDeviceArrayWithNum") rows, err := q.conn.Query(ctx, findManyDeviceArrayWithNumSQL) if err != nil { - return nil, fmt.Errorf("query FindManyDeviceArrayWithNum: %w", err) + var zero []FindManyDeviceArrayWithNumRow + return zero, fmt.Errorf("query FindManyDeviceArrayWithNum: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindManyDeviceArrayWithNumRow]) @@ -196,7 +200,8 @@ func (q *DBQuerier) EnumInsideComposite(ctx context.Context) (Device, error) { ctx = context.WithValue(ctx, QueryName{}, "EnumInsideComposite") rows, err := q.conn.Query(ctx, enumInsideCompositeSQL) if err != nil { - return Device{}, fmt.Errorf("query EnumInsideComposite: %w", err) + var zero Device + return zero, fmt.Errorf("query EnumInsideComposite: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Device]) diff --git a/example/erp/order/customer.sql.go b/example/erp/order/customer.sql.go index e91a598d..259365f5 100644 --- a/example/erp/order/customer.sql.go +++ b/example/erp/order/customer.sql.go @@ -96,7 +96,8 @@ func (q *DBQuerier) CreateTenant(ctx context.Context, key string, name string) ( ctx = context.WithValue(ctx, QueryName{}, "CreateTenant") rows, err := q.conn.Query(ctx, createTenantSQL, key, name) if err != nil { - return CreateTenantRow{}, fmt.Errorf("query CreateTenant: %w", err) + var zero CreateTenantRow + return zero, fmt.Errorf("query CreateTenant: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CreateTenantRow]) @@ -118,7 +119,8 @@ func (q *DBQuerier) FindOrdersByCustomer(ctx context.Context, customerID int32) ctx = context.WithValue(ctx, QueryName{}, "FindOrdersByCustomer") rows, err := q.conn.Query(ctx, findOrdersByCustomerSQL, customerID) if err != nil { - return nil, fmt.Errorf("query FindOrdersByCustomer: %w", err) + var zero []FindOrdersByCustomerRow + return zero, fmt.Errorf("query FindOrdersByCustomer: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByCustomerRow]) @@ -141,7 +143,8 @@ func (q *DBQuerier) FindProductsInOrder(ctx context.Context, orderID int32) ([]F ctx = context.WithValue(ctx, QueryName{}, "FindProductsInOrder") rows, err := q.conn.Query(ctx, findProductsInOrderSQL, orderID) if err != nil { - return nil, fmt.Errorf("query FindProductsInOrder: %w", err) + var zero []FindProductsInOrderRow + return zero, fmt.Errorf("query FindProductsInOrder: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindProductsInOrderRow]) @@ -169,7 +172,8 @@ func (q *DBQuerier) InsertCustomer(ctx context.Context, params InsertCustomerPar ctx = context.WithValue(ctx, QueryName{}, "InsertCustomer") rows, err := q.conn.Query(ctx, insertCustomerSQL, params.FirstName, params.LastName, params.Email) if err != nil { - return InsertCustomerRow{}, fmt.Errorf("query InsertCustomer: %w", err) + var zero InsertCustomerRow + return zero, fmt.Errorf("query InsertCustomer: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertCustomerRow]) @@ -197,7 +201,8 @@ func (q *DBQuerier) InsertOrder(ctx context.Context, params InsertOrderParams) ( ctx = context.WithValue(ctx, QueryName{}, "InsertOrder") rows, err := q.conn.Query(ctx, insertOrderSQL, params.OrderDate, params.OrderTotal, params.CustID) if err != nil { - return InsertOrderRow{}, fmt.Errorf("query InsertOrder: %w", err) + var zero InsertOrderRow + return zero, fmt.Errorf("query InsertOrder: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertOrderRow]) diff --git a/example/erp/order/price.sql.go b/example/erp/order/price.sql.go index f610d29b..a021695a 100644 --- a/example/erp/order/price.sql.go +++ b/example/erp/order/price.sql.go @@ -23,7 +23,8 @@ func (q *DBQuerier) FindOrdersByPrice(ctx context.Context, minTotal pgtype.Numer ctx = context.WithValue(ctx, QueryName{}, "FindOrdersByPrice") rows, err := q.conn.Query(ctx, findOrdersByPriceSQL, minTotal) if err != nil { - return nil, fmt.Errorf("query FindOrdersByPrice: %w", err) + var zero []FindOrdersByPriceRow + return zero, fmt.Errorf("query FindOrdersByPrice: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByPriceRow]) @@ -43,7 +44,8 @@ func (q *DBQuerier) FindOrdersMRR(ctx context.Context) ([]FindOrdersMRRRow, erro ctx = context.WithValue(ctx, QueryName{}, "FindOrdersMRR") rows, err := q.conn.Query(ctx, findOrdersMRRSQL) if err != nil { - return nil, fmt.Errorf("query FindOrdersMRR: %w", err) + var zero []FindOrdersMRRRow + return zero, fmt.Errorf("query FindOrdersMRR: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersMRRRow]) diff --git a/example/function/query.sql.go b/example/function/query.sql.go index eb07b9c1..36659102 100644 --- a/example/function/query.sql.go +++ b/example/function/query.sql.go @@ -98,7 +98,8 @@ func (q *DBQuerier) OutParams(ctx context.Context) ([]OutParamsRow, error) { ctx = context.WithValue(ctx, QueryName{}, "OutParams") rows, err := q.conn.Query(ctx, outParamsSQL) if err != nil { - return nil, fmt.Errorf("query OutParams: %w", err) + var zero []OutParamsRow + return zero, fmt.Errorf("query OutParams: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[OutParamsRow]) diff --git a/example/go_pointer_types/query.sql.go b/example/go_pointer_types/query.sql.go index 49378119..db8f6132 100644 --- a/example/go_pointer_types/query.sql.go +++ b/example/go_pointer_types/query.sql.go @@ -87,7 +87,8 @@ func (q *DBQuerier) GenSeries1(ctx context.Context) (*int, error) { ctx = context.WithValue(ctx, QueryName{}, "GenSeries1") rows, err := q.conn.Query(ctx, genSeries1SQL) if err != nil { - return nil, fmt.Errorf("query GenSeries1: %w", err) + var zero *int + return zero, fmt.Errorf("query GenSeries1: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) @@ -101,7 +102,8 @@ func (q *DBQuerier) GenSeries(ctx context.Context) ([]*int, error) { ctx = context.WithValue(ctx, QueryName{}, "GenSeries") rows, err := q.conn.Query(ctx, genSeriesSQL) if err != nil { - return nil, fmt.Errorf("query GenSeries: %w", err) + var zero []*int + return zero, fmt.Errorf("query GenSeries: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[*int]) @@ -115,7 +117,8 @@ func (q *DBQuerier) GenSeriesArr1(ctx context.Context) ([]int, error) { ctx = context.WithValue(ctx, QueryName{}, "GenSeriesArr1") rows, err := q.conn.Query(ctx, genSeriesArr1SQL) if err != nil { - return nil, fmt.Errorf("query GenSeriesArr1: %w", err) + var zero []int + return zero, fmt.Errorf("query GenSeriesArr1: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]int]) @@ -129,7 +132,8 @@ func (q *DBQuerier) GenSeriesArr(ctx context.Context) ([][]int, error) { ctx = context.WithValue(ctx, QueryName{}, "GenSeriesArr") rows, err := q.conn.Query(ctx, genSeriesArrSQL) if err != nil { - return nil, fmt.Errorf("query GenSeriesArr: %w", err) + var zero [][]int + return zero, fmt.Errorf("query GenSeriesArr: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[[]int]) @@ -144,7 +148,8 @@ func (q *DBQuerier) GenSeriesStr1(ctx context.Context) (*string, error) { ctx = context.WithValue(ctx, QueryName{}, "GenSeriesStr1") rows, err := q.conn.Query(ctx, genSeriesStr1SQL) if err != nil { - return nil, fmt.Errorf("query GenSeriesStr1: %w", err) + var zero *string + return zero, fmt.Errorf("query GenSeriesStr1: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*string]) @@ -158,7 +163,8 @@ func (q *DBQuerier) GenSeriesStr(ctx context.Context) ([]*string, error) { ctx = context.WithValue(ctx, QueryName{}, "GenSeriesStr") rows, err := q.conn.Query(ctx, genSeriesStrSQL) if err != nil { - return nil, fmt.Errorf("query GenSeriesStr: %w", err) + var zero []*string + return zero, fmt.Errorf("query GenSeriesStr: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[*string]) diff --git a/example/inline_param_count/inline0/query.sql.go b/example/inline_param_count/inline0/query.sql.go index 0a7aa26f..e69deb7c 100644 --- a/example/inline_param_count/inline0/query.sql.go +++ b/example/inline_param_count/inline0/query.sql.go @@ -85,7 +85,8 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { ctx = context.WithValue(ctx, QueryName{}, "CountAuthors") rows, err := q.conn.Query(ctx, countAuthorsSQL) if err != nil { - return nil, fmt.Errorf("query CountAuthors: %w", err) + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) @@ -109,7 +110,8 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, params FindAuthorByIDPar ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") rows, err := q.conn.Query(ctx, findAuthorByIDSQL, params.AuthorID) if err != nil { - return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) @@ -129,7 +131,8 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, params InsertAuthorParams) ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") rows, err := q.conn.Query(ctx, insertAuthorSQL, params.FirstName, params.LastName) if err != nil { - return 0, fmt.Errorf("query InsertAuthor: %w", err) + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) diff --git a/example/inline_param_count/inline1/query.sql.go b/example/inline_param_count/inline1/query.sql.go index 4a09e23f..a7b486bb 100644 --- a/example/inline_param_count/inline1/query.sql.go +++ b/example/inline_param_count/inline1/query.sql.go @@ -85,7 +85,8 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { ctx = context.WithValue(ctx, QueryName{}, "CountAuthors") rows, err := q.conn.Query(ctx, countAuthorsSQL) if err != nil { - return nil, fmt.Errorf("query CountAuthors: %w", err) + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) @@ -105,7 +106,8 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) if err != nil { - return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) @@ -125,7 +127,8 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, params InsertAuthorParams) ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") rows, err := q.conn.Query(ctx, insertAuthorSQL, params.FirstName, params.LastName) if err != nil { - return 0, fmt.Errorf("query InsertAuthor: %w", err) + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) diff --git a/example/inline_param_count/inline2/query.sql.go b/example/inline_param_count/inline2/query.sql.go index 3408f117..909ce1c3 100644 --- a/example/inline_param_count/inline2/query.sql.go +++ b/example/inline_param_count/inline2/query.sql.go @@ -85,7 +85,8 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { ctx = context.WithValue(ctx, QueryName{}, "CountAuthors") rows, err := q.conn.Query(ctx, countAuthorsSQL) if err != nil { - return nil, fmt.Errorf("query CountAuthors: %w", err) + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) @@ -105,7 +106,8 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) if err != nil { - return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) @@ -120,7 +122,8 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") rows, err := q.conn.Query(ctx, insertAuthorSQL, firstName, lastName) if err != nil { - return 0, fmt.Errorf("query InsertAuthor: %w", err) + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) diff --git a/example/inline_param_count/inline3/query.sql.go b/example/inline_param_count/inline3/query.sql.go index c19d9f29..d903de87 100644 --- a/example/inline_param_count/inline3/query.sql.go +++ b/example/inline_param_count/inline3/query.sql.go @@ -85,7 +85,8 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { ctx = context.WithValue(ctx, QueryName{}, "CountAuthors") rows, err := q.conn.Query(ctx, countAuthorsSQL) if err != nil { - return nil, fmt.Errorf("query CountAuthors: %w", err) + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) @@ -105,7 +106,8 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut ctx = context.WithValue(ctx, QueryName{}, "FindAuthorByID") rows, err := q.conn.Query(ctx, findAuthorByIDSQL, authorID) if err != nil { - return FindAuthorByIDRow{}, fmt.Errorf("query FindAuthorByID: %w", err) + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) @@ -120,7 +122,8 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName ctx = context.WithValue(ctx, QueryName{}, "InsertAuthor") rows, err := q.conn.Query(ctx, insertAuthorSQL, firstName, lastName) if err != nil { - return 0, fmt.Errorf("query InsertAuthor: %w", err) + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) diff --git a/example/ltree/query.sql.go b/example/ltree/query.sql.go index d771c53d..747f0d2d 100644 --- a/example/ltree/query.sql.go +++ b/example/ltree/query.sql.go @@ -84,7 +84,8 @@ func (q *DBQuerier) FindTopScienceChildren(ctx context.Context) ([]pgtype.Text, ctx = context.WithValue(ctx, QueryName{}, "FindTopScienceChildren") rows, err := q.conn.Query(ctx, findTopScienceChildrenSQL) if err != nil { - return nil, fmt.Errorf("query FindTopScienceChildren: %w", err) + var zero []pgtype.Text + return zero, fmt.Errorf("query FindTopScienceChildren: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[pgtype.Text]) @@ -99,7 +100,8 @@ func (q *DBQuerier) FindTopScienceChildrenAgg(ctx context.Context) (pgtype.Array ctx = context.WithValue(ctx, QueryName{}, "FindTopScienceChildrenAgg") rows, err := q.conn.Query(ctx, findTopScienceChildrenAggSQL) if err != nil { - return pgtype.Array[pgtype.Text]{}, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) + var zero pgtype.Array[pgtype.Text] + return zero, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[pgtype.Array[pgtype.Text]]) @@ -151,7 +153,8 @@ func (q *DBQuerier) FindLtreeInput(ctx context.Context, inLtree pgtype.Text, inL ctx = context.WithValue(ctx, QueryName{}, "FindLtreeInput") rows, err := q.conn.Query(ctx, findLtreeInputSQL, inLtree, inLtreeArray) if err != nil { - return FindLtreeInputRow{}, fmt.Errorf("query FindLtreeInput: %w", err) + var zero FindLtreeInputRow + return zero, fmt.Errorf("query FindLtreeInput: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindLtreeInputRow]) diff --git a/example/nested/query.sql.go b/example/nested/query.sql.go index a355faf8..1a110bf0 100644 --- a/example/nested/query.sql.go +++ b/example/nested/query.sql.go @@ -108,7 +108,8 @@ func (q *DBQuerier) ArrayNested2(ctx context.Context) ([]ProductImageType, error ctx = context.WithValue(ctx, QueryName{}, "ArrayNested2") rows, err := q.conn.Query(ctx, arrayNested2SQL) if err != nil { - return nil, fmt.Errorf("query ArrayNested2: %w", err) + var zero []ProductImageType + return zero, fmt.Errorf("query ArrayNested2: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]ProductImageType]) @@ -129,7 +130,8 @@ func (q *DBQuerier) Nested3(ctx context.Context) ([]ProductImageSetType, error) ctx = context.WithValue(ctx, QueryName{}, "Nested3") rows, err := q.conn.Query(ctx, nested3SQL) if err != nil { - return nil, fmt.Errorf("query Nested3: %w", err) + var zero []ProductImageSetType + return zero, fmt.Errorf("query Nested3: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[ProductImageSetType]) diff --git a/example/pgcrypto/query.sql.go b/example/pgcrypto/query.sql.go index 3eb98c33..9ba5af04 100644 --- a/example/pgcrypto/query.sql.go +++ b/example/pgcrypto/query.sql.go @@ -96,7 +96,8 @@ func (q *DBQuerier) FindUser(ctx context.Context, email string) (FindUserRow, er ctx = context.WithValue(ctx, QueryName{}, "FindUser") rows, err := q.conn.Query(ctx, findUserSQL, email) if err != nil { - return FindUserRow{}, fmt.Errorf("query FindUser: %w", err) + var zero FindUserRow + return zero, fmt.Errorf("query FindUser: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindUserRow]) diff --git a/example/separate_out_dir/out/alpha_query.sql.0.go b/example/separate_out_dir/out/alpha_query.sql.0.go index a23c0727..5c185fb8 100644 --- a/example/separate_out_dir/out/alpha_query.sql.0.go +++ b/example/separate_out_dir/out/alpha_query.sql.0.go @@ -90,7 +90,8 @@ func (q *DBQuerier) AlphaNested(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "AlphaNested") rows, err := q.conn.Query(ctx, alphaNestedSQL) if err != nil { - return "", fmt.Errorf("query AlphaNested: %w", err) + var zero string + return zero, fmt.Errorf("query AlphaNested: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) @@ -103,7 +104,8 @@ func (q *DBQuerier) AlphaCompositeArray(ctx context.Context) ([]Alpha, error) { ctx = context.WithValue(ctx, QueryName{}, "AlphaCompositeArray") rows, err := q.conn.Query(ctx, alphaCompositeArraySQL) if err != nil { - return nil, fmt.Errorf("query AlphaCompositeArray: %w", err) + var zero []Alpha + return zero, fmt.Errorf("query AlphaCompositeArray: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]Alpha]) diff --git a/example/separate_out_dir/out/alpha_query.sql.1.go b/example/separate_out_dir/out/alpha_query.sql.1.go index ae6dcfed..eef44b67 100644 --- a/example/separate_out_dir/out/alpha_query.sql.1.go +++ b/example/separate_out_dir/out/alpha_query.sql.1.go @@ -15,7 +15,8 @@ func (q *DBQuerier) Alpha(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "Alpha") rows, err := q.conn.Query(ctx, alphaSQL) if err != nil { - return "", fmt.Errorf("query Alpha: %w", err) + var zero string + return zero, fmt.Errorf("query Alpha: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) diff --git a/example/separate_out_dir/out/bravo_query.sql.go b/example/separate_out_dir/out/bravo_query.sql.go index 876f6e9e..18968ef0 100644 --- a/example/separate_out_dir/out/bravo_query.sql.go +++ b/example/separate_out_dir/out/bravo_query.sql.go @@ -15,7 +15,8 @@ func (q *DBQuerier) Bravo(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "Bravo") rows, err := q.conn.Query(ctx, bravoSQL) if err != nil { - return "", fmt.Errorf("query Bravo: %w", err) + var zero string + return zero, fmt.Errorf("query Bravo: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) diff --git a/example/slices/query.sql.go b/example/slices/query.sql.go index 2fb73c70..628324b5 100644 --- a/example/slices/query.sql.go +++ b/example/slices/query.sql.go @@ -82,7 +82,8 @@ func (q *DBQuerier) GetBools(ctx context.Context, data []bool) ([]bool, error) { ctx = context.WithValue(ctx, QueryName{}, "GetBools") rows, err := q.conn.Query(ctx, getBoolsSQL, data) if err != nil { - return nil, fmt.Errorf("query GetBools: %w", err) + var zero []bool + return zero, fmt.Errorf("query GetBools: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]bool]) @@ -95,7 +96,8 @@ func (q *DBQuerier) GetOneTimestamp(ctx context.Context, data *time.Time) (*time ctx = context.WithValue(ctx, QueryName{}, "GetOneTimestamp") rows, err := q.conn.Query(ctx, getOneTimestampSQL, data) if err != nil { - return nil, fmt.Errorf("query GetOneTimestamp: %w", err) + var zero *time.Time + return zero, fmt.Errorf("query GetOneTimestamp: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*time.Time]) @@ -109,7 +111,8 @@ func (q *DBQuerier) GetManyTimestamptzs(ctx context.Context, data []time.Time) ( ctx = context.WithValue(ctx, QueryName{}, "GetManyTimestamptzs") rows, err := q.conn.Query(ctx, getManyTimestamptzsSQL, data) if err != nil { - return nil, fmt.Errorf("query GetManyTimestamptzs: %w", err) + var zero []*time.Time + return zero, fmt.Errorf("query GetManyTimestamptzs: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[*time.Time]) @@ -123,7 +126,8 @@ func (q *DBQuerier) GetManyTimestamps(ctx context.Context, data []*time.Time) ([ ctx = context.WithValue(ctx, QueryName{}, "GetManyTimestamps") rows, err := q.conn.Query(ctx, getManyTimestampsSQL, data) if err != nil { - return nil, fmt.Errorf("query GetManyTimestamps: %w", err) + var zero []*time.Time + return zero, fmt.Errorf("query GetManyTimestamps: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[*time.Time]) diff --git a/example/syntax/query.sql.go b/example/syntax/query.sql.go index f085f89f..d160e0b6 100644 --- a/example/syntax/query.sql.go +++ b/example/syntax/query.sql.go @@ -113,7 +113,8 @@ func (q *DBQuerier) Backtick(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "Backtick") rows, err := q.conn.Query(ctx, backtickSQL) if err != nil { - return "", fmt.Errorf("query Backtick: %w", err) + var zero string + return zero, fmt.Errorf("query Backtick: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) @@ -126,7 +127,8 @@ func (q *DBQuerier) BacktickQuoteBacktick(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "BacktickQuoteBacktick") rows, err := q.conn.Query(ctx, backtickQuoteBacktickSQL) if err != nil { - return "", fmt.Errorf("query BacktickQuoteBacktick: %w", err) + var zero string + return zero, fmt.Errorf("query BacktickQuoteBacktick: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) @@ -139,7 +141,8 @@ func (q *DBQuerier) BacktickNewline(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "BacktickNewline") rows, err := q.conn.Query(ctx, backtickNewlineSQL) if err != nil { - return "", fmt.Errorf("query BacktickNewline: %w", err) + var zero string + return zero, fmt.Errorf("query BacktickNewline: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) @@ -152,7 +155,8 @@ func (q *DBQuerier) BacktickDoubleQuote(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "BacktickDoubleQuote") rows, err := q.conn.Query(ctx, backtickDoubleQuoteSQL) if err != nil { - return "", fmt.Errorf("query BacktickDoubleQuote: %w", err) + var zero string + return zero, fmt.Errorf("query BacktickDoubleQuote: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) @@ -165,7 +169,8 @@ func (q *DBQuerier) BacktickBackslashN(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "BacktickBackslashN") rows, err := q.conn.Query(ctx, backtickBackslashNSQL) if err != nil { - return "", fmt.Errorf("query BacktickBackslashN: %w", err) + var zero string + return zero, fmt.Errorf("query BacktickBackslashN: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) @@ -183,7 +188,8 @@ func (q *DBQuerier) IllegalNameSymbols(ctx context.Context, helloWorld string) ( ctx = context.WithValue(ctx, QueryName{}, "IllegalNameSymbols") rows, err := q.conn.Query(ctx, illegalNameSymbolsSQL, helloWorld) if err != nil { - return IllegalNameSymbolsRow{}, fmt.Errorf("query IllegalNameSymbols: %w", err) + var zero IllegalNameSymbolsRow + return zero, fmt.Errorf("query IllegalNameSymbols: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[IllegalNameSymbolsRow]) @@ -196,7 +202,8 @@ func (q *DBQuerier) SpaceAfter(ctx context.Context, space string) (string, error ctx = context.WithValue(ctx, QueryName{}, "SpaceAfter") rows, err := q.conn.Query(ctx, spaceAfterSQL, space) if err != nil { - return "", fmt.Errorf("query SpaceAfter: %w", err) + var zero string + return zero, fmt.Errorf("query SpaceAfter: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) @@ -209,7 +216,8 @@ func (q *DBQuerier) BadEnumName(ctx context.Context) (UnnamedEnum123, error) { ctx = context.WithValue(ctx, QueryName{}, "BadEnumName") rows, err := q.conn.Query(ctx, badEnumNameSQL) if err != nil { - return "", fmt.Errorf("query BadEnumName: %w", err) + var zero UnnamedEnum123 + return zero, fmt.Errorf("query BadEnumName: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[UnnamedEnum123]) @@ -222,7 +230,8 @@ func (q *DBQuerier) GoKeyword(ctx context.Context, go_ string) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "GoKeyword") rows, err := q.conn.Query(ctx, goKeywordSQL, go_) if err != nil { - return "", fmt.Errorf("query GoKeyword: %w", err) + var zero string + return zero, fmt.Errorf("query GoKeyword: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) diff --git a/example/void/query.sql.go b/example/void/query.sql.go index 94790852..140cc346 100644 --- a/example/void/query.sql.go +++ b/example/void/query.sql.go @@ -107,7 +107,8 @@ func (q *DBQuerier) VoidTwo(ctx context.Context) (string, error) { ctx = context.WithValue(ctx, QueryName{}, "VoidTwo") rows, err := q.conn.Query(ctx, voidTwoSQL) if err != nil { - return "", fmt.Errorf("query VoidTwo: %w", err) + var zero string + return zero, fmt.Errorf("query VoidTwo: %w", err) } return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (string, error) { @@ -131,7 +132,8 @@ func (q *DBQuerier) VoidThree(ctx context.Context) (VoidThreeRow, error) { ctx = context.WithValue(ctx, QueryName{}, "VoidThree") rows, err := q.conn.Query(ctx, voidThreeSQL) if err != nil { - return VoidThreeRow{}, fmt.Errorf("query VoidThree: %w", err) + var zero VoidThreeRow + return zero, fmt.Errorf("query VoidThree: %w", err) } return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (VoidThreeRow, error) { @@ -150,7 +152,8 @@ func (q *DBQuerier) VoidThree2(ctx context.Context) ([]string, error) { ctx = context.WithValue(ctx, QueryName{}, "VoidThree2") rows, err := q.conn.Query(ctx, voidThree2SQL) if err != nil { - return nil, fmt.Errorf("query VoidThree2: %w", err) + var zero []string + return zero, fmt.Errorf("query VoidThree2: %w", err) } return pgx.CollectRows(rows, func(row pgx.CollectableRow) (string, error) { diff --git a/internal/codegen/golang/query.gotemplate b/internal/codegen/golang/query.gotemplate index 4b8729e8..3091030d 100644 --- a/internal/codegen/golang/query.gotemplate +++ b/internal/codegen/golang/query.gotemplate @@ -65,7 +65,8 @@ func (q *DBQuerier) {{ $q.Name }}(ctx context.Context {{- $q.EmitParams }}) ({{ {{- else }} rows, err := q.conn.Query(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) if err != nil { - return {{ $q.EmitZeroResult }}, fmt.Errorf("query {{ $q.Name }}: %w", err) + {{ $q.EmitZeroResultInit "zero" }} + return zero, fmt.Errorf("query {{ $q.Name }}: %w", err) } return {{ $q.EmitCollectionFunc }}(rows, {{ $q.EmitRowMapper }}) diff --git a/internal/codegen/golang/templated_file.go b/internal/codegen/golang/templated_file.go index 674eadeb..2f8e4685 100644 --- a/internal/codegen/golang/templated_file.go +++ b/internal/codegen/golang/templated_file.go @@ -392,6 +392,15 @@ func (tq TemplatedQuery) EmitResultTypeInit(name string) (string, error) { } } +// EmitZeroResultInit returns a var declaration for the zero value of a result. +func (tq TemplatedQuery) EmitZeroResultInit(name string) (string, error) { + result, err := tq.EmitResultType() + if err != nil { + return "", fmt.Errorf("create result type for EmitZeroResultInit: %w", err) + } + return "var " + name + " " + result, nil +} + // EmitZeroResult returns the string representing the zero value of a result. func (tq TemplatedQuery) EmitZeroResult() (string, error) { switch tq.ResultKind { @@ -421,7 +430,7 @@ func (tq TemplatedQuery) EmitZeroResult() (string, error) { case "bool": return "false", nil default: - return tq.Outputs[0].QualType + "{}", nil // won't work for type Foo int + return "*new(" + tq.Outputs[0].QualType + ")", nil } default: return "", fmt.Errorf("unhandled EmitZeroResult kind: %s", tq.ResultKind) diff --git a/internal/pg/query.sql.go b/internal/pg/query.sql.go index e07077b7..2375efcf 100644 --- a/internal/pg/query.sql.go +++ b/internal/pg/query.sql.go @@ -144,7 +144,8 @@ func (q *DBQuerier) FindEnumTypes(ctx context.Context, oids []uint32) ([]FindEnu ctx = context.WithValue(ctx, QueryName{}, "FindEnumTypes") rows, err := q.conn.Query(ctx, findEnumTypesSQL, oids) if err != nil { - return nil, fmt.Errorf("query FindEnumTypes: %w", err) + var zero []FindEnumTypesRow + return zero, fmt.Errorf("query FindEnumTypes: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindEnumTypesRow]) @@ -191,7 +192,8 @@ func (q *DBQuerier) FindArrayTypes(ctx context.Context, oids []uint32) ([]FindAr ctx = context.WithValue(ctx, QueryName{}, "FindArrayTypes") rows, err := q.conn.Query(ctx, findArrayTypesSQL, oids) if err != nil { - return nil, fmt.Errorf("query FindArrayTypes: %w", err) + var zero []FindArrayTypesRow + return zero, fmt.Errorf("query FindArrayTypes: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindArrayTypesRow]) @@ -243,7 +245,8 @@ func (q *DBQuerier) FindCompositeTypes(ctx context.Context, oids []uint32) ([]Fi ctx = context.WithValue(ctx, QueryName{}, "FindCompositeTypes") rows, err := q.conn.Query(ctx, findCompositeTypesSQL, oids) if err != nil { - return nil, fmt.Errorf("query FindCompositeTypes: %w", err) + var zero []FindCompositeTypesRow + return zero, fmt.Errorf("query FindCompositeTypes: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindCompositeTypesRow]) @@ -282,7 +285,8 @@ func (q *DBQuerier) FindDescendantOIDs(ctx context.Context, oids []uint32) ([]ui ctx = context.WithValue(ctx, QueryName{}, "FindDescendantOIDs") rows, err := q.conn.Query(ctx, findDescendantOIDsSQL, oids) if err != nil { - return nil, fmt.Errorf("query FindDescendantOIDs: %w", err) + var zero []uint32 + return zero, fmt.Errorf("query FindDescendantOIDs: %w", err) } return pgx.CollectRows(rows, pgx.RowTo[uint32]) @@ -299,7 +303,8 @@ func (q *DBQuerier) FindOIDByName(ctx context.Context, name string) (uint32, err ctx = context.WithValue(ctx, QueryName{}, "FindOIDByName") rows, err := q.conn.Query(ctx, findOIDByNameSQL, name) if err != nil { - return 0, fmt.Errorf("query FindOIDByName: %w", err) + var zero uint32 + return zero, fmt.Errorf("query FindOIDByName: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[uint32]) @@ -314,7 +319,8 @@ func (q *DBQuerier) FindOIDName(ctx context.Context, oid uint32) (string, error) ctx = context.WithValue(ctx, QueryName{}, "FindOIDName") rows, err := q.conn.Query(ctx, findOIDNameSQL, oid) if err != nil { - return "", fmt.Errorf("query FindOIDName: %w", err) + var zero string + return zero, fmt.Errorf("query FindOIDName: %w", err) } return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) @@ -335,7 +341,8 @@ func (q *DBQuerier) FindOIDNames(ctx context.Context, oid []uint32) ([]FindOIDNa ctx = context.WithValue(ctx, QueryName{}, "FindOIDNames") rows, err := q.conn.Query(ctx, findOIDNamesSQL, oid) if err != nil { - return nil, fmt.Errorf("query FindOIDNames: %w", err) + var zero []FindOIDNamesRow + return zero, fmt.Errorf("query FindOIDNames: %w", err) } return pgx.CollectRows(rows, pgx.RowToStructByName[FindOIDNamesRow]) From 24c47e8eb95c973dd04dae0ecd17fe064c56688d Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 21:23:30 +0000 Subject: [PATCH 14/16] Use CollectOneRow for one queries --- example/author/query.sql | 4 +++ example/author/query.sql.go | 34 ++++++++++++++++--- example/author/query.sql_test.go | 18 ++++++++++ example/complex_params/query.sql.go | 10 +++--- example/composite/query.sql.go | 6 ++-- example/custom_types/query.sql.go | 6 ++-- example/device/query.sql.go | 4 +-- example/domain/query.sql.go | 2 +- example/enums/query.sql.go | 4 +-- example/erp/order/customer.sql.go | 6 ++-- example/go_pointer_types/query.sql.go | 6 ++-- .../inline_param_count/inline0/query.sql.go | 6 ++-- .../inline_param_count/inline1/query.sql.go | 6 ++-- .../inline_param_count/inline2/query.sql.go | 6 ++-- .../inline_param_count/inline3/query.sql.go | 6 ++-- example/ltree/query.sql.go | 4 +-- example/nested/query.sql.go | 2 +- example/pgcrypto/query.sql.go | 2 +- .../separate_out_dir/out/alpha_query.sql.0.go | 4 +-- .../separate_out_dir/out/alpha_query.sql.1.go | 2 +- .../separate_out_dir/out/bravo_query.sql.go | 2 +- example/slices/query.sql.go | 4 +-- example/syntax/query.sql.go | 18 +++++----- example/void/query.sql.go | 4 +-- internal/codegen/golang/templated_file.go | 2 +- internal/pg/query.sql.go | 4 +-- 26 files changed, 109 insertions(+), 63 deletions(-) diff --git a/example/author/query.sql b/example/author/query.sql index a312ae24..963cd109 100644 --- a/example/author/query.sql +++ b/example/author/query.sql @@ -14,6 +14,10 @@ SELECT first_name, last_name FROM author ORDER BY author_id = pggen.arg('AuthorI -- name: FindFirstNames :many SELECT first_name FROM author ORDER BY author_id = pggen.arg('AuthorID'); +-- FindFirstAuthor finds the first author by ID. +-- name: FindFirstAuthor :one +SELECT * FROM author ORDER BY author_id; + -- DeleteAuthors deletes authors with a first name of "joe". -- name: DeleteAuthors :exec DELETE FROM author WHERE first_name = 'joe'; diff --git a/example/author/query.sql.go b/example/author/query.sql.go index 45ed19d2..7e96c7b9 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -25,6 +25,9 @@ type Querier interface { // FindFirstNames finds one (or zero) authors by ID. FindFirstNames(ctx context.Context, authorID int32) ([]*string, error) + // FindFirstAuthor finds the first author by ID. + FindFirstAuthor(ctx context.Context) (FindFirstAuthorRow, error) + // DeleteAuthors deletes authors with a first name of "joe". DeleteAuthors(ctx context.Context) (pgconn.CommandTag, error) @@ -116,7 +119,7 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const findAuthorsSQL = `SELECT * FROM author WHERE first_name = $1;` @@ -173,6 +176,27 @@ func (q *DBQuerier) FindFirstNames(ctx context.Context, authorID int32) ([]*stri return pgx.CollectRows(rows, pgx.RowTo[*string]) } +const findFirstAuthorSQL = `SELECT * FROM author ORDER BY author_id;` + +type FindFirstAuthorRow struct { + AuthorID *int32 `json:"author_id" db:"author_id"` + FirstName *string `json:"first_name" db:"first_name"` + LastName *string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` +} + +// FindFirstAuthor implements Querier.FindFirstAuthor. +func (q *DBQuerier) FindFirstAuthor(ctx context.Context) (FindFirstAuthorRow, error) { + ctx = context.WithValue(ctx, QueryName{}, "FindFirstAuthor") + rows, err := q.conn.Query(ctx, findFirstAuthorSQL) + if err != nil { + var zero FindFirstAuthorRow + return zero, fmt.Errorf("query FindFirstAuthor: %w", err) + } + + return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindFirstAuthorRow]) +} + const deleteAuthorsSQL = `DELETE FROM author WHERE first_name = 'joe';` // DeleteAuthors implements Querier.DeleteAuthors. @@ -232,7 +256,7 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) + return pgx.CollectOneRow(rows, pgx.RowTo[int32]) } const insertAuthorSuffixSQL = `INSERT INTO author (first_name, last_name, suffix) @@ -261,7 +285,7 @@ func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorS return zero, fmt.Errorf("query InsertAuthorSuffix: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertAuthorSuffixRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertAuthorSuffixRow]) } const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS names FROM author WHERE author_id = $1;` @@ -275,7 +299,7 @@ func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*st return zero, fmt.Errorf("query StringAggFirstName: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*string]) + return pgx.CollectOneRow(rows, pgx.RowTo[*string]) } const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS names FROM author WHERE author_id = $1;` @@ -289,5 +313,5 @@ func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]st return zero, fmt.Errorf("query ArrayAggFirstName: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]string]) + return pgx.CollectOneRow(rows, pgx.RowTo[[]string]) } diff --git a/example/author/query.sql_test.go b/example/author/query.sql_test.go index 2e15de91..3c32f710 100644 --- a/example/author/query.sql_test.go +++ b/example/author/query.sql_test.go @@ -115,6 +115,24 @@ func TestNewQuerier_FindFirstNames(t *testing.T) { }) } +func TestNewQuerier_FindFirstAuthor(t *testing.T) { + conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) + defer cleanup() + + q := NewQuerier(conn) + adamsID := insertAuthor(t, q, "john", "adams") + insertAuthor(t, q, "george", "washington") + + author, err := q.FindFirstAuthor(t.Context()) + require.NoError(t, err) + assert.Equal(t, FindFirstAuthorRow{ + AuthorID: ptrs.Int32(adamsID), + FirstName: ptrs.String("john"), + LastName: ptrs.String("adams"), + Suffix: nil, + }, author) +} + func TestNewQuerier_InsertAuthorSuffix(t *testing.T) { conn, cleanup := pgtest.NewPostgresSchema(t, []string{"schema.sql"}) defer cleanup() diff --git a/example/complex_params/query.sql.go b/example/complex_params/query.sql.go index b4aae0f7..70fbcf65 100644 --- a/example/complex_params/query.sql.go +++ b/example/complex_params/query.sql.go @@ -114,7 +114,7 @@ func (q *DBQuerier) ParamArrayInt(ctx context.Context, ints []int) ([]int, error return zero, fmt.Errorf("query ParamArrayInt: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]int]) + return pgx.CollectOneRow(rows, pgx.RowTo[[]int]) } const paramNested1SQL = `SELECT $1::dimensions;` @@ -128,7 +128,7 @@ func (q *DBQuerier) ParamNested1(ctx context.Context, dimensions Dimensions) (Di return zero, fmt.Errorf("query ParamNested1: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Dimensions]) + return pgx.CollectOneRow(rows, pgx.RowTo[Dimensions]) } const paramNested2SQL = `SELECT $1::product_image_type;` @@ -142,7 +142,7 @@ func (q *DBQuerier) ParamNested2(ctx context.Context, image ProductImageType) (P return zero, fmt.Errorf("query ParamNested2: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[ProductImageType]) + return pgx.CollectOneRow(rows, pgx.RowTo[ProductImageType]) } const paramNested2ArraySQL = `SELECT $1::product_image_type[];` @@ -156,7 +156,7 @@ func (q *DBQuerier) ParamNested2Array(ctx context.Context, images []ProductImage return zero, fmt.Errorf("query ParamNested2Array: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]ProductImageType]) + return pgx.CollectOneRow(rows, pgx.RowTo[[]ProductImageType]) } const paramNested3SQL = `SELECT $1::product_image_set_type;` @@ -170,5 +170,5 @@ func (q *DBQuerier) ParamNested3(ctx context.Context, imageSet ProductImageSetTy return zero, fmt.Errorf("query ParamNested3: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[ProductImageSetType]) + return pgx.CollectOneRow(rows, pgx.RowTo[ProductImageSetType]) } diff --git a/example/composite/query.sql.go b/example/composite/query.sql.go index e13e59b8..c98c2556 100644 --- a/example/composite/query.sql.go +++ b/example/composite/query.sql.go @@ -190,7 +190,7 @@ func (q *DBQuerier) InsertScreenshotBlocks(ctx context.Context, screenshotID int return zero, fmt.Errorf("query InsertScreenshotBlocks: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertScreenshotBlocksRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertScreenshotBlocksRow]) } const arraysInputSQL = `SELECT $1::arrays;` @@ -204,7 +204,7 @@ func (q *DBQuerier) ArraysInput(ctx context.Context, arrays Arrays) (Arrays, err return zero, fmt.Errorf("query ArraysInput: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Arrays]) + return pgx.CollectOneRow(rows, pgx.RowTo[Arrays]) } const userEmailsSQL = `SELECT ('foo', 'bar@example.com')::user_email;` @@ -218,5 +218,5 @@ func (q *DBQuerier) UserEmails(ctx context.Context) (UserEmail, error) { return zero, fmt.Errorf("query UserEmails: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[UserEmail]) + return pgx.CollectOneRow(rows, pgx.RowTo[UserEmail]) } diff --git a/example/custom_types/query.sql.go b/example/custom_types/query.sql.go index 32c5ee4f..30d81d47 100644 --- a/example/custom_types/query.sql.go +++ b/example/custom_types/query.sql.go @@ -91,7 +91,7 @@ func (q *DBQuerier) CustomTypes(ctx context.Context) (CustomTypesRow, error) { return zero, fmt.Errorf("query CustomTypes: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CustomTypesRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[CustomTypesRow]) } const customStringSQL = `SELECT 'some_text'::text;` @@ -105,7 +105,7 @@ func (q *DBQuerier) CustomString(ctx context.Context) (mytype.String, error) { return zero, fmt.Errorf("query CustomString: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[mytype.String]) + return pgx.CollectOneRow(rows, pgx.RowTo[mytype.String]) } const customMyIntSQL = `SELECT '5'::my_int as int5;` @@ -119,7 +119,7 @@ func (q *DBQuerier) CustomMyInt(ctx context.Context) (int, error) { return zero, fmt.Errorf("query CustomMyInt: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]) + return pgx.CollectOneRow(rows, pgx.RowTo[int]) } const intArraySQL = `SELECT ARRAY ['5', '6', '7']::int[] as ints;` diff --git a/example/device/query.sql.go b/example/device/query.sql.go index 73c7cb3a..5f62a7c0 100644 --- a/example/device/query.sql.go +++ b/example/device/query.sql.go @@ -166,7 +166,7 @@ func (q *DBQuerier) CompositeUserOne(ctx context.Context) (User, error) { return zero, fmt.Errorf("query CompositeUserOne: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[User]) + return pgx.CollectOneRow(rows, pgx.RowTo[User]) } const compositeUserOneTwoColsSQL = `SELECT 1 AS num, ROW (15, 'qux')::"user" AS "user";` @@ -185,7 +185,7 @@ func (q *DBQuerier) CompositeUserOneTwoCols(ctx context.Context) (CompositeUserO return zero, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CompositeUserOneTwoColsRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[CompositeUserOneTwoColsRow]) } const compositeUserManySQL = `SELECT ROW (15, 'qux')::"user" AS "user";` diff --git a/example/domain/query.sql.go b/example/domain/query.sql.go index 333ca36c..d2122ecc 100644 --- a/example/domain/query.sql.go +++ b/example/domain/query.sql.go @@ -79,5 +79,5 @@ func (q *DBQuerier) DomainOne(ctx context.Context) (string, error) { return zero, fmt.Errorf("query DomainOne: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } diff --git a/example/enums/query.sql.go b/example/enums/query.sql.go index e7b266f6..bc1e910c 100644 --- a/example/enums/query.sql.go +++ b/example/enums/query.sql.go @@ -153,7 +153,7 @@ func (q *DBQuerier) FindOneDeviceArray(ctx context.Context) ([]DeviceType, error return zero, fmt.Errorf("query FindOneDeviceArray: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]DeviceType]) + return pgx.CollectOneRow(rows, pgx.RowTo[[]DeviceType]) } const findManyDeviceArraySQL = `SELECT enum_range('ipad'::device_type, 'iot'::device_type) AS device_types @@ -204,5 +204,5 @@ func (q *DBQuerier) EnumInsideComposite(ctx context.Context) (Device, error) { return zero, fmt.Errorf("query EnumInsideComposite: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[Device]) + return pgx.CollectOneRow(rows, pgx.RowTo[Device]) } diff --git a/example/erp/order/customer.sql.go b/example/erp/order/customer.sql.go index 259365f5..c7da2c60 100644 --- a/example/erp/order/customer.sql.go +++ b/example/erp/order/customer.sql.go @@ -100,7 +100,7 @@ func (q *DBQuerier) CreateTenant(ctx context.Context, key string, name string) ( return zero, fmt.Errorf("query CreateTenant: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[CreateTenantRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[CreateTenantRow]) } const findOrdersByCustomerSQL = `SELECT * @@ -176,7 +176,7 @@ func (q *DBQuerier) InsertCustomer(ctx context.Context, params InsertCustomerPar return zero, fmt.Errorf("query InsertCustomer: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertCustomerRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertCustomerRow]) } const insertOrderSQL = `INSERT INTO orders (order_date, order_total, customer_id) @@ -205,5 +205,5 @@ func (q *DBQuerier) InsertOrder(ctx context.Context, params InsertOrderParams) ( return zero, fmt.Errorf("query InsertOrder: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[InsertOrderRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertOrderRow]) } diff --git a/example/go_pointer_types/query.sql.go b/example/go_pointer_types/query.sql.go index db8f6132..75625a4c 100644 --- a/example/go_pointer_types/query.sql.go +++ b/example/go_pointer_types/query.sql.go @@ -91,7 +91,7 @@ func (q *DBQuerier) GenSeries1(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query GenSeries1: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) + return pgx.CollectOneRow(rows, pgx.RowTo[*int]) } const genSeriesSQL = `SELECT n @@ -121,7 +121,7 @@ func (q *DBQuerier) GenSeriesArr1(ctx context.Context) ([]int, error) { return zero, fmt.Errorf("query GenSeriesArr1: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]int]) + return pgx.CollectOneRow(rows, pgx.RowTo[[]int]) } const genSeriesArrSQL = `SELECT array_agg(n) @@ -152,7 +152,7 @@ func (q *DBQuerier) GenSeriesStr1(ctx context.Context) (*string, error) { return zero, fmt.Errorf("query GenSeriesStr1: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*string]) + return pgx.CollectOneRow(rows, pgx.RowTo[*string]) } const genSeriesStrSQL = `SELECT n::text diff --git a/example/inline_param_count/inline0/query.sql.go b/example/inline_param_count/inline0/query.sql.go index e69deb7c..76a90e47 100644 --- a/example/inline_param_count/inline0/query.sql.go +++ b/example/inline_param_count/inline0/query.sql.go @@ -89,7 +89,7 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query CountAuthors: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) + return pgx.CollectOneRow(rows, pgx.RowTo[*int]) } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -114,7 +114,7 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, params FindAuthorByIDPar return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -135,7 +135,7 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, params InsertAuthorParams) return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) + return pgx.CollectOneRow(rows, pgx.RowTo[int32]) } const deleteAuthorsByFullNameSQL = `DELETE diff --git a/example/inline_param_count/inline1/query.sql.go b/example/inline_param_count/inline1/query.sql.go index a7b486bb..77bfe4a4 100644 --- a/example/inline_param_count/inline1/query.sql.go +++ b/example/inline_param_count/inline1/query.sql.go @@ -89,7 +89,7 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query CountAuthors: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) + return pgx.CollectOneRow(rows, pgx.RowTo[*int]) } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -110,7 +110,7 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -131,7 +131,7 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, params InsertAuthorParams) return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) + return pgx.CollectOneRow(rows, pgx.RowTo[int32]) } const deleteAuthorsByFullNameSQL = `DELETE diff --git a/example/inline_param_count/inline2/query.sql.go b/example/inline_param_count/inline2/query.sql.go index 909ce1c3..b4d05f3b 100644 --- a/example/inline_param_count/inline2/query.sql.go +++ b/example/inline_param_count/inline2/query.sql.go @@ -89,7 +89,7 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query CountAuthors: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) + return pgx.CollectOneRow(rows, pgx.RowTo[*int]) } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -110,7 +110,7 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -126,7 +126,7 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) + return pgx.CollectOneRow(rows, pgx.RowTo[int32]) } const deleteAuthorsByFullNameSQL = `DELETE diff --git a/example/inline_param_count/inline3/query.sql.go b/example/inline_param_count/inline3/query.sql.go index d903de87..4dfb3550 100644 --- a/example/inline_param_count/inline3/query.sql.go +++ b/example/inline_param_count/inline3/query.sql.go @@ -89,7 +89,7 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query CountAuthors: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*int]) + return pgx.CollectOneRow(rows, pgx.RowTo[*int]) } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -110,7 +110,7 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -126,7 +126,7 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[int32]) + return pgx.CollectOneRow(rows, pgx.RowTo[int32]) } const deleteAuthorsByFullNameSQL = `DELETE diff --git a/example/ltree/query.sql.go b/example/ltree/query.sql.go index 747f0d2d..3e3fbc87 100644 --- a/example/ltree/query.sql.go +++ b/example/ltree/query.sql.go @@ -104,7 +104,7 @@ func (q *DBQuerier) FindTopScienceChildrenAgg(ctx context.Context) (pgtype.Array return zero, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[pgtype.Array[pgtype.Text]]) + return pgx.CollectOneRow(rows, pgx.RowTo[pgtype.Array[pgtype.Text]]) } const insertSampleDataSQL = `INSERT INTO test @@ -157,5 +157,5 @@ func (q *DBQuerier) FindLtreeInput(ctx context.Context, inLtree pgtype.Text, inL return zero, fmt.Errorf("query FindLtreeInput: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindLtreeInputRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindLtreeInputRow]) } diff --git a/example/nested/query.sql.go b/example/nested/query.sql.go index 1a110bf0..1452f753 100644 --- a/example/nested/query.sql.go +++ b/example/nested/query.sql.go @@ -112,7 +112,7 @@ func (q *DBQuerier) ArrayNested2(ctx context.Context) ([]ProductImageType, error return zero, fmt.Errorf("query ArrayNested2: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]ProductImageType]) + return pgx.CollectOneRow(rows, pgx.RowTo[[]ProductImageType]) } const nested3SQL = `SELECT diff --git a/example/pgcrypto/query.sql.go b/example/pgcrypto/query.sql.go index 9ba5af04..99696518 100644 --- a/example/pgcrypto/query.sql.go +++ b/example/pgcrypto/query.sql.go @@ -100,5 +100,5 @@ func (q *DBQuerier) FindUser(ctx context.Context, email string) (FindUserRow, er return zero, fmt.Errorf("query FindUser: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FindUserRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindUserRow]) } diff --git a/example/separate_out_dir/out/alpha_query.sql.0.go b/example/separate_out_dir/out/alpha_query.sql.0.go index 5c185fb8..aba2656e 100644 --- a/example/separate_out_dir/out/alpha_query.sql.0.go +++ b/example/separate_out_dir/out/alpha_query.sql.0.go @@ -94,7 +94,7 @@ func (q *DBQuerier) AlphaNested(ctx context.Context) (string, error) { return zero, fmt.Errorf("query AlphaNested: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } const alphaCompositeArraySQL = `SELECT ARRAY[ROW('key')]::alpha[];` @@ -108,5 +108,5 @@ func (q *DBQuerier) AlphaCompositeArray(ctx context.Context) ([]Alpha, error) { return zero, fmt.Errorf("query AlphaCompositeArray: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]Alpha]) + return pgx.CollectOneRow(rows, pgx.RowTo[[]Alpha]) } diff --git a/example/separate_out_dir/out/alpha_query.sql.1.go b/example/separate_out_dir/out/alpha_query.sql.1.go index eef44b67..d8e775b6 100644 --- a/example/separate_out_dir/out/alpha_query.sql.1.go +++ b/example/separate_out_dir/out/alpha_query.sql.1.go @@ -19,5 +19,5 @@ func (q *DBQuerier) Alpha(ctx context.Context) (string, error) { return zero, fmt.Errorf("query Alpha: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } diff --git a/example/separate_out_dir/out/bravo_query.sql.go b/example/separate_out_dir/out/bravo_query.sql.go index 18968ef0..5db88c2d 100644 --- a/example/separate_out_dir/out/bravo_query.sql.go +++ b/example/separate_out_dir/out/bravo_query.sql.go @@ -19,5 +19,5 @@ func (q *DBQuerier) Bravo(ctx context.Context) (string, error) { return zero, fmt.Errorf("query Bravo: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } diff --git a/example/slices/query.sql.go b/example/slices/query.sql.go index 628324b5..5234c926 100644 --- a/example/slices/query.sql.go +++ b/example/slices/query.sql.go @@ -86,7 +86,7 @@ func (q *DBQuerier) GetBools(ctx context.Context, data []bool) ([]bool, error) { return zero, fmt.Errorf("query GetBools: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[[]bool]) + return pgx.CollectOneRow(rows, pgx.RowTo[[]bool]) } const getOneTimestampSQL = `SELECT $1::timestamp;` @@ -100,7 +100,7 @@ func (q *DBQuerier) GetOneTimestamp(ctx context.Context, data *time.Time) (*time return zero, fmt.Errorf("query GetOneTimestamp: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[*time.Time]) + return pgx.CollectOneRow(rows, pgx.RowTo[*time.Time]) } const getManyTimestamptzsSQL = `SELECT * diff --git a/example/syntax/query.sql.go b/example/syntax/query.sql.go index d160e0b6..e961e293 100644 --- a/example/syntax/query.sql.go +++ b/example/syntax/query.sql.go @@ -117,7 +117,7 @@ func (q *DBQuerier) Backtick(ctx context.Context) (string, error) { return zero, fmt.Errorf("query Backtick: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } const backtickQuoteBacktickSQL = "SELECT '`\"`';" @@ -131,7 +131,7 @@ func (q *DBQuerier) BacktickQuoteBacktick(ctx context.Context) (string, error) { return zero, fmt.Errorf("query BacktickQuoteBacktick: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } const backtickNewlineSQL = "SELECT '`\n';" @@ -145,7 +145,7 @@ func (q *DBQuerier) BacktickNewline(ctx context.Context) (string, error) { return zero, fmt.Errorf("query BacktickNewline: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } const backtickDoubleQuoteSQL = "SELECT '`\"';" @@ -159,7 +159,7 @@ func (q *DBQuerier) BacktickDoubleQuote(ctx context.Context) (string, error) { return zero, fmt.Errorf("query BacktickDoubleQuote: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } const backtickBackslashNSQL = "SELECT '`\\n';" @@ -173,7 +173,7 @@ func (q *DBQuerier) BacktickBackslashN(ctx context.Context) (string, error) { return zero, fmt.Errorf("query BacktickBackslashN: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } const illegalNameSymbolsSQL = "SELECT '`\\n' as \"$\", $1 as \"foo.bar!@#$%&*()\"\"--+\";" @@ -192,7 +192,7 @@ func (q *DBQuerier) IllegalNameSymbols(ctx context.Context, helloWorld string) ( return zero, fmt.Errorf("query IllegalNameSymbols: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[IllegalNameSymbolsRow]) + return pgx.CollectOneRow(rows, pgx.RowToStructByName[IllegalNameSymbolsRow]) } const spaceAfterSQL = `SELECT $1;` @@ -206,7 +206,7 @@ func (q *DBQuerier) SpaceAfter(ctx context.Context, space string) (string, error return zero, fmt.Errorf("query SpaceAfter: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } const badEnumNameSQL = `SELECT 'inconvertible_enum_name'::"123";` @@ -220,7 +220,7 @@ func (q *DBQuerier) BadEnumName(ctx context.Context) (UnnamedEnum123, error) { return zero, fmt.Errorf("query BadEnumName: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[UnnamedEnum123]) + return pgx.CollectOneRow(rows, pgx.RowTo[UnnamedEnum123]) } const goKeywordSQL = `SELECT $1::text;` @@ -234,5 +234,5 @@ func (q *DBQuerier) GoKeyword(ctx context.Context, go_ string) (string, error) { return zero, fmt.Errorf("query GoKeyword: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } diff --git a/example/void/query.sql.go b/example/void/query.sql.go index 140cc346..bb73bb98 100644 --- a/example/void/query.sql.go +++ b/example/void/query.sql.go @@ -111,7 +111,7 @@ func (q *DBQuerier) VoidTwo(ctx context.Context) (string, error) { return zero, fmt.Errorf("query VoidTwo: %w", err) } - return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (string, error) { + return pgx.CollectOneRow(rows, func(row pgx.CollectableRow) (string, error) { var item string if err := row.Scan(nil, &item); err != nil { return item, err @@ -136,7 +136,7 @@ func (q *DBQuerier) VoidThree(ctx context.Context) (VoidThreeRow, error) { return zero, fmt.Errorf("query VoidThree: %w", err) } - return pgx.CollectExactlyOneRow(rows, func(row pgx.CollectableRow) (VoidThreeRow, error) { + return pgx.CollectOneRow(rows, func(row pgx.CollectableRow) (VoidThreeRow, error) { var item VoidThreeRow if err := row.Scan(nil, &item.Foo, &item.Bar); err != nil { return item, err diff --git a/internal/codegen/golang/templated_file.go b/internal/codegen/golang/templated_file.go index 2f8e4685..12efebfc 100644 --- a/internal/codegen/golang/templated_file.go +++ b/internal/codegen/golang/templated_file.go @@ -264,7 +264,7 @@ func (tq TemplatedQuery) EmitCollectionFunc() (string, error) { case ast.ResultKindMany: return "pgx.CollectRows", nil case ast.ResultKindOne: - return "pgx.CollectExactlyOneRow", nil + return "pgx.CollectOneRow", nil default: return "", fmt.Errorf("unhandled EmitCollectionFunc type: %s", tq.ResultKind) } diff --git a/internal/pg/query.sql.go b/internal/pg/query.sql.go index 2375efcf..efd521c6 100644 --- a/internal/pg/query.sql.go +++ b/internal/pg/query.sql.go @@ -307,7 +307,7 @@ func (q *DBQuerier) FindOIDByName(ctx context.Context, name string) (uint32, err return zero, fmt.Errorf("query FindOIDByName: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[uint32]) + return pgx.CollectOneRow(rows, pgx.RowTo[uint32]) } const findOIDNameSQL = `SELECT typname AS name @@ -323,7 +323,7 @@ func (q *DBQuerier) FindOIDName(ctx context.Context, oid uint32) (string, error) return zero, fmt.Errorf("query FindOIDName: %w", err) } - return pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) + return pgx.CollectOneRow(rows, pgx.RowTo[string]) } const findOIDNamesSQL = `SELECT oid, typname AS name, typtype AS kind From adb89a8cd471ee6da0e769e05eca493f83973709 Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 21:32:31 +0000 Subject: [PATCH 15/16] Wrap collection errors with query context --- example/author/query.sql.go | 63 ++++++++++++++++--- example/author/query.sql_test.go | 1 + example/complex_params/query.sql.go | 35 +++++++++-- example/composite/query.sql.go | 35 +++++++++-- example/custom_types/query.sql.go | 28 +++++++-- example/device/query.sql.go | 35 +++++++++-- example/domain/query.sql.go | 7 ++- example/enums/query.sql.go | 35 +++++++++-- example/erp/order/customer.sql.go | 35 +++++++++-- example/erp/order/price.sql.go | 14 ++++- example/function/query.sql.go | 7 ++- example/go_pointer_types/query.sql.go | 42 +++++++++++-- .../inline_param_count/inline0/query.sql.go | 21 ++++++- .../inline_param_count/inline1/query.sql.go | 21 ++++++- .../inline_param_count/inline2/query.sql.go | 21 ++++++- .../inline_param_count/inline3/query.sql.go | 21 ++++++- example/ltree/query.sql.go | 21 ++++++- example/nested/query.sql.go | 14 ++++- example/pgcrypto/query.sql.go | 7 ++- .../separate_out_dir/out/alpha_query.sql.0.go | 14 ++++- .../separate_out_dir/out/alpha_query.sql.1.go | 7 ++- .../separate_out_dir/out/bravo_query.sql.go | 7 ++- example/slices/query.sql.go | 28 +++++++-- example/syntax/query.sql.go | 63 ++++++++++++++++--- example/void/query.sql.go | 51 +++++++++------ internal/codegen/golang/query.gotemplate | 9 ++- internal/codegen/golang/templated_file.go | 25 ++++++-- internal/pg/query.sql.go | 49 ++++++++++++--- 28 files changed, 600 insertions(+), 116 deletions(-) diff --git a/example/author/query.sql.go b/example/author/query.sql.go index 7e96c7b9..8c19d2cf 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -119,7 +119,12 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + if err != nil { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) + } + return result, nil } const findAuthorsSQL = `SELECT * FROM author WHERE first_name = $1;` @@ -140,7 +145,12 @@ func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAu return zero, fmt.Errorf("query FindAuthors: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorsRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorsRow]) + if err != nil { + var zero []FindAuthorsRow + return zero, fmt.Errorf("scan FindAuthors row: %w", err) + } + return result, nil } const findAuthorNamesSQL = `SELECT first_name, last_name FROM author ORDER BY author_id = $1;` @@ -159,7 +169,12 @@ func (q *DBQuerier) FindAuthorNames(ctx context.Context, authorID int32) ([]Find return zero, fmt.Errorf("query FindAuthorNames: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorNamesRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorNamesRow]) + if err != nil { + var zero []FindAuthorNamesRow + return zero, fmt.Errorf("scan FindAuthorNames row: %w", err) + } + return result, nil } const findFirstNamesSQL = `SELECT first_name FROM author ORDER BY author_id = $1;` @@ -173,7 +188,12 @@ func (q *DBQuerier) FindFirstNames(ctx context.Context, authorID int32) ([]*stri return zero, fmt.Errorf("query FindFirstNames: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[*string]) + result, err := pgx.CollectRows(rows, pgx.RowTo[*string]) + if err != nil { + var zero []*string + return zero, fmt.Errorf("scan FindFirstNames row: %w", err) + } + return result, nil } const findFirstAuthorSQL = `SELECT * FROM author ORDER BY author_id;` @@ -194,7 +214,12 @@ func (q *DBQuerier) FindFirstAuthor(ctx context.Context) (FindFirstAuthorRow, er return zero, fmt.Errorf("query FindFirstAuthor: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindFirstAuthorRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindFirstAuthorRow]) + if err != nil { + var zero FindFirstAuthorRow + return zero, fmt.Errorf("query FindFirstAuthor: %w", err) + } + return result, nil } const deleteAuthorsSQL = `DELETE FROM author WHERE first_name = 'joe';` @@ -256,7 +281,12 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[int32]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[int32]) + if err != nil { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) + } + return result, nil } const insertAuthorSuffixSQL = `INSERT INTO author (first_name, last_name, suffix) @@ -285,7 +315,12 @@ func (q *DBQuerier) InsertAuthorSuffix(ctx context.Context, params InsertAuthorS return zero, fmt.Errorf("query InsertAuthorSuffix: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertAuthorSuffixRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertAuthorSuffixRow]) + if err != nil { + var zero InsertAuthorSuffixRow + return zero, fmt.Errorf("query InsertAuthorSuffix: %w", err) + } + return result, nil } const stringAggFirstNameSQL = `SELECT string_agg(first_name, ',') AS names FROM author WHERE author_id = $1;` @@ -299,7 +334,12 @@ func (q *DBQuerier) StringAggFirstName(ctx context.Context, authorID int32) (*st return zero, fmt.Errorf("query StringAggFirstName: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[*string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*string]) + if err != nil { + var zero *string + return zero, fmt.Errorf("query StringAggFirstName: %w", err) + } + return result, nil } const arrayAggFirstNameSQL = `SELECT array_agg(first_name) AS names FROM author WHERE author_id = $1;` @@ -313,5 +353,10 @@ func (q *DBQuerier) ArrayAggFirstName(ctx context.Context, authorID int32) ([]st return zero, fmt.Errorf("query ArrayAggFirstName: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[[]string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]string]) + if err != nil { + var zero []string + return zero, fmt.Errorf("query ArrayAggFirstName: %w", err) + } + return result, nil } diff --git a/example/author/query.sql_test.go b/example/author/query.sql_test.go index 3c32f710..8622b3e6 100644 --- a/example/author/query.sql_test.go +++ b/example/author/query.sql_test.go @@ -34,6 +34,7 @@ func TestNewQuerier_FindAuthorByID(t *testing.T) { t.Run("FindAuthorByID - none-exists", func(t *testing.T) { missingAuthorByID, err := q.FindAuthorByID(t.Context(), 888) require.Error(t, err, "expected error when finding author ID that doesn't match") + assert.ErrorContains(t, err, "query FindAuthorByID:") assert.Zero(t, missingAuthorByID, "expected zero value when error") if !errors.Is(err, pgx.ErrNoRows) { t.Fatalf("expected no rows error to wrap pgx.ErrNoRows; got %s", err) diff --git a/example/complex_params/query.sql.go b/example/complex_params/query.sql.go index 70fbcf65..c49bbb99 100644 --- a/example/complex_params/query.sql.go +++ b/example/complex_params/query.sql.go @@ -114,7 +114,12 @@ func (q *DBQuerier) ParamArrayInt(ctx context.Context, ints []int) ([]int, error return zero, fmt.Errorf("query ParamArrayInt: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[[]int]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]int]) + if err != nil { + var zero []int + return zero, fmt.Errorf("query ParamArrayInt: %w", err) + } + return result, nil } const paramNested1SQL = `SELECT $1::dimensions;` @@ -128,7 +133,12 @@ func (q *DBQuerier) ParamNested1(ctx context.Context, dimensions Dimensions) (Di return zero, fmt.Errorf("query ParamNested1: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[Dimensions]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[Dimensions]) + if err != nil { + var zero Dimensions + return zero, fmt.Errorf("query ParamNested1: %w", err) + } + return result, nil } const paramNested2SQL = `SELECT $1::product_image_type;` @@ -142,7 +152,12 @@ func (q *DBQuerier) ParamNested2(ctx context.Context, image ProductImageType) (P return zero, fmt.Errorf("query ParamNested2: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[ProductImageType]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[ProductImageType]) + if err != nil { + var zero ProductImageType + return zero, fmt.Errorf("query ParamNested2: %w", err) + } + return result, nil } const paramNested2ArraySQL = `SELECT $1::product_image_type[];` @@ -156,7 +171,12 @@ func (q *DBQuerier) ParamNested2Array(ctx context.Context, images []ProductImage return zero, fmt.Errorf("query ParamNested2Array: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[[]ProductImageType]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]ProductImageType]) + if err != nil { + var zero []ProductImageType + return zero, fmt.Errorf("query ParamNested2Array: %w", err) + } + return result, nil } const paramNested3SQL = `SELECT $1::product_image_set_type;` @@ -170,5 +190,10 @@ func (q *DBQuerier) ParamNested3(ctx context.Context, imageSet ProductImageSetTy return zero, fmt.Errorf("query ParamNested3: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[ProductImageSetType]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[ProductImageSetType]) + if err != nil { + var zero ProductImageSetType + return zero, fmt.Errorf("query ParamNested3: %w", err) + } + return result, nil } diff --git a/example/composite/query.sql.go b/example/composite/query.sql.go index c98c2556..4edda308 100644 --- a/example/composite/query.sql.go +++ b/example/composite/query.sql.go @@ -136,7 +136,12 @@ func (q *DBQuerier) SearchScreenshots(ctx context.Context, params SearchScreensh return zero, fmt.Errorf("query SearchScreenshots: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[SearchScreenshotsRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[SearchScreenshotsRow]) + if err != nil { + var zero []SearchScreenshotsRow + return zero, fmt.Errorf("scan SearchScreenshots row: %w", err) + } + return result, nil } const searchScreenshotsOneColSQL = `SELECT @@ -163,7 +168,12 @@ func (q *DBQuerier) SearchScreenshotsOneCol(ctx context.Context, params SearchSc return zero, fmt.Errorf("query SearchScreenshotsOneCol: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[[]Blocks]) + result, err := pgx.CollectRows(rows, pgx.RowTo[[]Blocks]) + if err != nil { + var zero [][]Blocks + return zero, fmt.Errorf("scan SearchScreenshotsOneCol row: %w", err) + } + return result, nil } const insertScreenshotBlocksSQL = `WITH screens AS ( @@ -190,7 +200,12 @@ func (q *DBQuerier) InsertScreenshotBlocks(ctx context.Context, screenshotID int return zero, fmt.Errorf("query InsertScreenshotBlocks: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertScreenshotBlocksRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertScreenshotBlocksRow]) + if err != nil { + var zero InsertScreenshotBlocksRow + return zero, fmt.Errorf("query InsertScreenshotBlocks: %w", err) + } + return result, nil } const arraysInputSQL = `SELECT $1::arrays;` @@ -204,7 +219,12 @@ func (q *DBQuerier) ArraysInput(ctx context.Context, arrays Arrays) (Arrays, err return zero, fmt.Errorf("query ArraysInput: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[Arrays]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[Arrays]) + if err != nil { + var zero Arrays + return zero, fmt.Errorf("query ArraysInput: %w", err) + } + return result, nil } const userEmailsSQL = `SELECT ('foo', 'bar@example.com')::user_email;` @@ -218,5 +238,10 @@ func (q *DBQuerier) UserEmails(ctx context.Context) (UserEmail, error) { return zero, fmt.Errorf("query UserEmails: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[UserEmail]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[UserEmail]) + if err != nil { + var zero UserEmail + return zero, fmt.Errorf("query UserEmails: %w", err) + } + return result, nil } diff --git a/example/custom_types/query.sql.go b/example/custom_types/query.sql.go index 30d81d47..7a1f4e15 100644 --- a/example/custom_types/query.sql.go +++ b/example/custom_types/query.sql.go @@ -91,7 +91,12 @@ func (q *DBQuerier) CustomTypes(ctx context.Context) (CustomTypesRow, error) { return zero, fmt.Errorf("query CustomTypes: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[CustomTypesRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[CustomTypesRow]) + if err != nil { + var zero CustomTypesRow + return zero, fmt.Errorf("query CustomTypes: %w", err) + } + return result, nil } const customStringSQL = `SELECT 'some_text'::text;` @@ -105,7 +110,12 @@ func (q *DBQuerier) CustomString(ctx context.Context) (mytype.String, error) { return zero, fmt.Errorf("query CustomString: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[mytype.String]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[mytype.String]) + if err != nil { + var zero mytype.String + return zero, fmt.Errorf("query CustomString: %w", err) + } + return result, nil } const customMyIntSQL = `SELECT '5'::my_int as int5;` @@ -119,7 +129,12 @@ func (q *DBQuerier) CustomMyInt(ctx context.Context) (int, error) { return zero, fmt.Errorf("query CustomMyInt: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[int]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[int]) + if err != nil { + var zero int + return zero, fmt.Errorf("query CustomMyInt: %w", err) + } + return result, nil } const intArraySQL = `SELECT ARRAY ['5', '6', '7']::int[] as ints;` @@ -133,5 +148,10 @@ func (q *DBQuerier) IntArray(ctx context.Context) ([][]int32, error) { return zero, fmt.Errorf("query IntArray: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[[]int32]) + result, err := pgx.CollectRows(rows, pgx.RowTo[[]int32]) + if err != nil { + var zero [][]int32 + return zero, fmt.Errorf("scan IntArray row: %w", err) + } + return result, nil } diff --git a/example/device/query.sql.go b/example/device/query.sql.go index 5f62a7c0..5fe9dd59 100644 --- a/example/device/query.sql.go +++ b/example/device/query.sql.go @@ -127,7 +127,12 @@ func (q *DBQuerier) FindDevicesByUser(ctx context.Context, id int) ([]FindDevice return zero, fmt.Errorf("query FindDevicesByUser: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindDevicesByUserRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindDevicesByUserRow]) + if err != nil { + var zero []FindDevicesByUserRow + return zero, fmt.Errorf("scan FindDevicesByUser row: %w", err) + } + return result, nil } const compositeUserSQL = `SELECT @@ -152,7 +157,12 @@ func (q *DBQuerier) CompositeUser(ctx context.Context) ([]CompositeUserRow, erro return zero, fmt.Errorf("query CompositeUser: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[CompositeUserRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[CompositeUserRow]) + if err != nil { + var zero []CompositeUserRow + return zero, fmt.Errorf("scan CompositeUser row: %w", err) + } + return result, nil } const compositeUserOneSQL = `SELECT ROW (15, 'qux')::"user" AS "user";` @@ -166,7 +176,12 @@ func (q *DBQuerier) CompositeUserOne(ctx context.Context) (User, error) { return zero, fmt.Errorf("query CompositeUserOne: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[User]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[User]) + if err != nil { + var zero User + return zero, fmt.Errorf("query CompositeUserOne: %w", err) + } + return result, nil } const compositeUserOneTwoColsSQL = `SELECT 1 AS num, ROW (15, 'qux')::"user" AS "user";` @@ -185,7 +200,12 @@ func (q *DBQuerier) CompositeUserOneTwoCols(ctx context.Context) (CompositeUserO return zero, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[CompositeUserOneTwoColsRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[CompositeUserOneTwoColsRow]) + if err != nil { + var zero CompositeUserOneTwoColsRow + return zero, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) + } + return result, nil } const compositeUserManySQL = `SELECT ROW (15, 'qux')::"user" AS "user";` @@ -199,7 +219,12 @@ func (q *DBQuerier) CompositeUserMany(ctx context.Context) ([]User, error) { return zero, fmt.Errorf("query CompositeUserMany: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[User]) + result, err := pgx.CollectRows(rows, pgx.RowTo[User]) + if err != nil { + var zero []User + return zero, fmt.Errorf("scan CompositeUserMany row: %w", err) + } + return result, nil } const insertUserSQL = `INSERT INTO "user" (id, name) diff --git a/example/domain/query.sql.go b/example/domain/query.sql.go index d2122ecc..ccdf872c 100644 --- a/example/domain/query.sql.go +++ b/example/domain/query.sql.go @@ -79,5 +79,10 @@ func (q *DBQuerier) DomainOne(ctx context.Context) (string, error) { return zero, fmt.Errorf("query DomainOne: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query DomainOne: %w", err) + } + return result, nil } diff --git a/example/enums/query.sql.go b/example/enums/query.sql.go index bc1e910c..6c23538b 100644 --- a/example/enums/query.sql.go +++ b/example/enums/query.sql.go @@ -126,7 +126,12 @@ func (q *DBQuerier) FindAllDevices(ctx context.Context) ([]FindAllDevicesRow, er return zero, fmt.Errorf("query FindAllDevices: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindAllDevicesRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindAllDevicesRow]) + if err != nil { + var zero []FindAllDevicesRow + return zero, fmt.Errorf("scan FindAllDevices row: %w", err) + } + return result, nil } const insertDeviceSQL = `INSERT INTO device (mac, type) @@ -153,7 +158,12 @@ func (q *DBQuerier) FindOneDeviceArray(ctx context.Context) ([]DeviceType, error return zero, fmt.Errorf("query FindOneDeviceArray: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[[]DeviceType]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]DeviceType]) + if err != nil { + var zero []DeviceType + return zero, fmt.Errorf("query FindOneDeviceArray: %w", err) + } + return result, nil } const findManyDeviceArraySQL = `SELECT enum_range('ipad'::device_type, 'iot'::device_type) AS device_types @@ -169,7 +179,12 @@ func (q *DBQuerier) FindManyDeviceArray(ctx context.Context) ([][]DeviceType, er return zero, fmt.Errorf("query FindManyDeviceArray: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[[]DeviceType]) + result, err := pgx.CollectRows(rows, pgx.RowTo[[]DeviceType]) + if err != nil { + var zero [][]DeviceType + return zero, fmt.Errorf("scan FindManyDeviceArray row: %w", err) + } + return result, nil } const findManyDeviceArrayWithNumSQL = `SELECT 1 AS num, enum_range('ipad'::device_type, 'iot'::device_type) AS device_types @@ -190,7 +205,12 @@ func (q *DBQuerier) FindManyDeviceArrayWithNum(ctx context.Context) ([]FindManyD return zero, fmt.Errorf("query FindManyDeviceArrayWithNum: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindManyDeviceArrayWithNumRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindManyDeviceArrayWithNumRow]) + if err != nil { + var zero []FindManyDeviceArrayWithNumRow + return zero, fmt.Errorf("scan FindManyDeviceArrayWithNum row: %w", err) + } + return result, nil } const enumInsideCompositeSQL = `SELECT ROW('08:00:2b:01:02:03'::macaddr, 'phone'::device_type) ::device;` @@ -204,5 +224,10 @@ func (q *DBQuerier) EnumInsideComposite(ctx context.Context) (Device, error) { return zero, fmt.Errorf("query EnumInsideComposite: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[Device]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[Device]) + if err != nil { + var zero Device + return zero, fmt.Errorf("query EnumInsideComposite: %w", err) + } + return result, nil } diff --git a/example/erp/order/customer.sql.go b/example/erp/order/customer.sql.go index c7da2c60..17a1a3f8 100644 --- a/example/erp/order/customer.sql.go +++ b/example/erp/order/customer.sql.go @@ -100,7 +100,12 @@ func (q *DBQuerier) CreateTenant(ctx context.Context, key string, name string) ( return zero, fmt.Errorf("query CreateTenant: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[CreateTenantRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[CreateTenantRow]) + if err != nil { + var zero CreateTenantRow + return zero, fmt.Errorf("query CreateTenant: %w", err) + } + return result, nil } const findOrdersByCustomerSQL = `SELECT * @@ -123,7 +128,12 @@ func (q *DBQuerier) FindOrdersByCustomer(ctx context.Context, customerID int32) return zero, fmt.Errorf("query FindOrdersByCustomer: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByCustomerRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByCustomerRow]) + if err != nil { + var zero []FindOrdersByCustomerRow + return zero, fmt.Errorf("scan FindOrdersByCustomer row: %w", err) + } + return result, nil } const findProductsInOrderSQL = `SELECT o.order_id, p.product_id, p.name @@ -147,7 +157,12 @@ func (q *DBQuerier) FindProductsInOrder(ctx context.Context, orderID int32) ([]F return zero, fmt.Errorf("query FindProductsInOrder: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindProductsInOrderRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindProductsInOrderRow]) + if err != nil { + var zero []FindProductsInOrderRow + return zero, fmt.Errorf("scan FindProductsInOrder row: %w", err) + } + return result, nil } const insertCustomerSQL = `INSERT INTO customer (first_name, last_name, email) @@ -176,7 +191,12 @@ func (q *DBQuerier) InsertCustomer(ctx context.Context, params InsertCustomerPar return zero, fmt.Errorf("query InsertCustomer: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertCustomerRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertCustomerRow]) + if err != nil { + var zero InsertCustomerRow + return zero, fmt.Errorf("query InsertCustomer: %w", err) + } + return result, nil } const insertOrderSQL = `INSERT INTO orders (order_date, order_total, customer_id) @@ -205,5 +225,10 @@ func (q *DBQuerier) InsertOrder(ctx context.Context, params InsertOrderParams) ( return zero, fmt.Errorf("query InsertOrder: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertOrderRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertOrderRow]) + if err != nil { + var zero InsertOrderRow + return zero, fmt.Errorf("query InsertOrder: %w", err) + } + return result, nil } diff --git a/example/erp/order/price.sql.go b/example/erp/order/price.sql.go index a021695a..cd0d7190 100644 --- a/example/erp/order/price.sql.go +++ b/example/erp/order/price.sql.go @@ -27,7 +27,12 @@ func (q *DBQuerier) FindOrdersByPrice(ctx context.Context, minTotal pgtype.Numer return zero, fmt.Errorf("query FindOrdersByPrice: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByPriceRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByPriceRow]) + if err != nil { + var zero []FindOrdersByPriceRow + return zero, fmt.Errorf("scan FindOrdersByPrice row: %w", err) + } + return result, nil } const findOrdersMRRSQL = `SELECT date_trunc('month', order_date) AS month, sum(order_total) AS order_mrr @@ -48,5 +53,10 @@ func (q *DBQuerier) FindOrdersMRR(ctx context.Context) ([]FindOrdersMRRRow, erro return zero, fmt.Errorf("query FindOrdersMRR: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersMRRRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersMRRRow]) + if err != nil { + var zero []FindOrdersMRRRow + return zero, fmt.Errorf("scan FindOrdersMRR row: %w", err) + } + return result, nil } diff --git a/example/function/query.sql.go b/example/function/query.sql.go index 36659102..4d3b3aee 100644 --- a/example/function/query.sql.go +++ b/example/function/query.sql.go @@ -102,5 +102,10 @@ func (q *DBQuerier) OutParams(ctx context.Context) ([]OutParamsRow, error) { return zero, fmt.Errorf("query OutParams: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[OutParamsRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[OutParamsRow]) + if err != nil { + var zero []OutParamsRow + return zero, fmt.Errorf("scan OutParams row: %w", err) + } + return result, nil } diff --git a/example/go_pointer_types/query.sql.go b/example/go_pointer_types/query.sql.go index 75625a4c..2bca0609 100644 --- a/example/go_pointer_types/query.sql.go +++ b/example/go_pointer_types/query.sql.go @@ -91,7 +91,12 @@ func (q *DBQuerier) GenSeries1(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query GenSeries1: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[*int]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*int]) + if err != nil { + var zero *int + return zero, fmt.Errorf("query GenSeries1: %w", err) + } + return result, nil } const genSeriesSQL = `SELECT n @@ -106,7 +111,12 @@ func (q *DBQuerier) GenSeries(ctx context.Context) ([]*int, error) { return zero, fmt.Errorf("query GenSeries: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[*int]) + result, err := pgx.CollectRows(rows, pgx.RowTo[*int]) + if err != nil { + var zero []*int + return zero, fmt.Errorf("scan GenSeries row: %w", err) + } + return result, nil } const genSeriesArr1SQL = `SELECT array_agg(n) @@ -121,7 +131,12 @@ func (q *DBQuerier) GenSeriesArr1(ctx context.Context) ([]int, error) { return zero, fmt.Errorf("query GenSeriesArr1: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[[]int]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]int]) + if err != nil { + var zero []int + return zero, fmt.Errorf("query GenSeriesArr1: %w", err) + } + return result, nil } const genSeriesArrSQL = `SELECT array_agg(n) @@ -136,7 +151,12 @@ func (q *DBQuerier) GenSeriesArr(ctx context.Context) ([][]int, error) { return zero, fmt.Errorf("query GenSeriesArr: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[[]int]) + result, err := pgx.CollectRows(rows, pgx.RowTo[[]int]) + if err != nil { + var zero [][]int + return zero, fmt.Errorf("scan GenSeriesArr row: %w", err) + } + return result, nil } const genSeriesStr1SQL = `SELECT n::text @@ -152,7 +172,12 @@ func (q *DBQuerier) GenSeriesStr1(ctx context.Context) (*string, error) { return zero, fmt.Errorf("query GenSeriesStr1: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[*string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*string]) + if err != nil { + var zero *string + return zero, fmt.Errorf("query GenSeriesStr1: %w", err) + } + return result, nil } const genSeriesStrSQL = `SELECT n::text @@ -167,5 +192,10 @@ func (q *DBQuerier) GenSeriesStr(ctx context.Context) ([]*string, error) { return zero, fmt.Errorf("query GenSeriesStr: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[*string]) + result, err := pgx.CollectRows(rows, pgx.RowTo[*string]) + if err != nil { + var zero []*string + return zero, fmt.Errorf("scan GenSeriesStr row: %w", err) + } + return result, nil } diff --git a/example/inline_param_count/inline0/query.sql.go b/example/inline_param_count/inline0/query.sql.go index 76a90e47..61529bf7 100644 --- a/example/inline_param_count/inline0/query.sql.go +++ b/example/inline_param_count/inline0/query.sql.go @@ -89,7 +89,12 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query CountAuthors: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[*int]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*int]) + if err != nil { + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) + } + return result, nil } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -114,7 +119,12 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, params FindAuthorByIDPar return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + if err != nil { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) + } + return result, nil } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -135,7 +145,12 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, params InsertAuthorParams) return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[int32]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[int32]) + if err != nil { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) + } + return result, nil } const deleteAuthorsByFullNameSQL = `DELETE diff --git a/example/inline_param_count/inline1/query.sql.go b/example/inline_param_count/inline1/query.sql.go index 77bfe4a4..134befbf 100644 --- a/example/inline_param_count/inline1/query.sql.go +++ b/example/inline_param_count/inline1/query.sql.go @@ -89,7 +89,12 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query CountAuthors: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[*int]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*int]) + if err != nil { + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) + } + return result, nil } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -110,7 +115,12 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + if err != nil { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) + } + return result, nil } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -131,7 +141,12 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, params InsertAuthorParams) return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[int32]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[int32]) + if err != nil { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) + } + return result, nil } const deleteAuthorsByFullNameSQL = `DELETE diff --git a/example/inline_param_count/inline2/query.sql.go b/example/inline_param_count/inline2/query.sql.go index b4d05f3b..96dd4320 100644 --- a/example/inline_param_count/inline2/query.sql.go +++ b/example/inline_param_count/inline2/query.sql.go @@ -89,7 +89,12 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query CountAuthors: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[*int]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*int]) + if err != nil { + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) + } + return result, nil } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -110,7 +115,12 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + if err != nil { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) + } + return result, nil } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -126,7 +136,12 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[int32]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[int32]) + if err != nil { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) + } + return result, nil } const deleteAuthorsByFullNameSQL = `DELETE diff --git a/example/inline_param_count/inline3/query.sql.go b/example/inline_param_count/inline3/query.sql.go index 4dfb3550..8ef8b669 100644 --- a/example/inline_param_count/inline3/query.sql.go +++ b/example/inline_param_count/inline3/query.sql.go @@ -89,7 +89,12 @@ func (q *DBQuerier) CountAuthors(ctx context.Context) (*int, error) { return zero, fmt.Errorf("query CountAuthors: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[*int]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*int]) + if err != nil { + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) + } + return result, nil } const findAuthorByIDSQL = `SELECT * FROM author WHERE author_id = $1;` @@ -110,7 +115,12 @@ func (q *DBQuerier) FindAuthorByID(ctx context.Context, authorID int32) (FindAut return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + if err != nil { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) + } + return result, nil } const insertAuthorSQL = `INSERT INTO author (first_name, last_name) @@ -126,7 +136,12 @@ func (q *DBQuerier) InsertAuthor(ctx context.Context, firstName string, lastName return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[int32]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[int32]) + if err != nil { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) + } + return result, nil } const deleteAuthorsByFullNameSQL = `DELETE diff --git a/example/ltree/query.sql.go b/example/ltree/query.sql.go index 3e3fbc87..323cab16 100644 --- a/example/ltree/query.sql.go +++ b/example/ltree/query.sql.go @@ -88,7 +88,12 @@ func (q *DBQuerier) FindTopScienceChildren(ctx context.Context) ([]pgtype.Text, return zero, fmt.Errorf("query FindTopScienceChildren: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[pgtype.Text]) + result, err := pgx.CollectRows(rows, pgx.RowTo[pgtype.Text]) + if err != nil { + var zero []pgtype.Text + return zero, fmt.Errorf("scan FindTopScienceChildren row: %w", err) + } + return result, nil } const findTopScienceChildrenAggSQL = `SELECT array_agg(path) @@ -104,7 +109,12 @@ func (q *DBQuerier) FindTopScienceChildrenAgg(ctx context.Context) (pgtype.Array return zero, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[pgtype.Array[pgtype.Text]]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[pgtype.Array[pgtype.Text]]) + if err != nil { + var zero pgtype.Array[pgtype.Text] + return zero, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) + } + return result, nil } const insertSampleDataSQL = `INSERT INTO test @@ -157,5 +167,10 @@ func (q *DBQuerier) FindLtreeInput(ctx context.Context, inLtree pgtype.Text, inL return zero, fmt.Errorf("query FindLtreeInput: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindLtreeInputRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindLtreeInputRow]) + if err != nil { + var zero FindLtreeInputRow + return zero, fmt.Errorf("query FindLtreeInput: %w", err) + } + return result, nil } diff --git a/example/nested/query.sql.go b/example/nested/query.sql.go index 1452f753..da84e485 100644 --- a/example/nested/query.sql.go +++ b/example/nested/query.sql.go @@ -112,7 +112,12 @@ func (q *DBQuerier) ArrayNested2(ctx context.Context) ([]ProductImageType, error return zero, fmt.Errorf("query ArrayNested2: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[[]ProductImageType]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]ProductImageType]) + if err != nil { + var zero []ProductImageType + return zero, fmt.Errorf("query ArrayNested2: %w", err) + } + return result, nil } const nested3SQL = `SELECT @@ -134,5 +139,10 @@ func (q *DBQuerier) Nested3(ctx context.Context) ([]ProductImageSetType, error) return zero, fmt.Errorf("query Nested3: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[ProductImageSetType]) + result, err := pgx.CollectRows(rows, pgx.RowTo[ProductImageSetType]) + if err != nil { + var zero []ProductImageSetType + return zero, fmt.Errorf("scan Nested3 row: %w", err) + } + return result, nil } diff --git a/example/pgcrypto/query.sql.go b/example/pgcrypto/query.sql.go index 99696518..8432a28f 100644 --- a/example/pgcrypto/query.sql.go +++ b/example/pgcrypto/query.sql.go @@ -100,5 +100,10 @@ func (q *DBQuerier) FindUser(ctx context.Context, email string) (FindUserRow, er return zero, fmt.Errorf("query FindUser: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[FindUserRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindUserRow]) + if err != nil { + var zero FindUserRow + return zero, fmt.Errorf("query FindUser: %w", err) + } + return result, nil } diff --git a/example/separate_out_dir/out/alpha_query.sql.0.go b/example/separate_out_dir/out/alpha_query.sql.0.go index aba2656e..66d2b38d 100644 --- a/example/separate_out_dir/out/alpha_query.sql.0.go +++ b/example/separate_out_dir/out/alpha_query.sql.0.go @@ -94,7 +94,12 @@ func (q *DBQuerier) AlphaNested(ctx context.Context) (string, error) { return zero, fmt.Errorf("query AlphaNested: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query AlphaNested: %w", err) + } + return result, nil } const alphaCompositeArraySQL = `SELECT ARRAY[ROW('key')]::alpha[];` @@ -108,5 +113,10 @@ func (q *DBQuerier) AlphaCompositeArray(ctx context.Context) ([]Alpha, error) { return zero, fmt.Errorf("query AlphaCompositeArray: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[[]Alpha]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]Alpha]) + if err != nil { + var zero []Alpha + return zero, fmt.Errorf("query AlphaCompositeArray: %w", err) + } + return result, nil } diff --git a/example/separate_out_dir/out/alpha_query.sql.1.go b/example/separate_out_dir/out/alpha_query.sql.1.go index d8e775b6..1de74a95 100644 --- a/example/separate_out_dir/out/alpha_query.sql.1.go +++ b/example/separate_out_dir/out/alpha_query.sql.1.go @@ -19,5 +19,10 @@ func (q *DBQuerier) Alpha(ctx context.Context) (string, error) { return zero, fmt.Errorf("query Alpha: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query Alpha: %w", err) + } + return result, nil } diff --git a/example/separate_out_dir/out/bravo_query.sql.go b/example/separate_out_dir/out/bravo_query.sql.go index 5db88c2d..455610a6 100644 --- a/example/separate_out_dir/out/bravo_query.sql.go +++ b/example/separate_out_dir/out/bravo_query.sql.go @@ -19,5 +19,10 @@ func (q *DBQuerier) Bravo(ctx context.Context) (string, error) { return zero, fmt.Errorf("query Bravo: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query Bravo: %w", err) + } + return result, nil } diff --git a/example/slices/query.sql.go b/example/slices/query.sql.go index 5234c926..83573803 100644 --- a/example/slices/query.sql.go +++ b/example/slices/query.sql.go @@ -86,7 +86,12 @@ func (q *DBQuerier) GetBools(ctx context.Context, data []bool) ([]bool, error) { return zero, fmt.Errorf("query GetBools: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[[]bool]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]bool]) + if err != nil { + var zero []bool + return zero, fmt.Errorf("query GetBools: %w", err) + } + return result, nil } const getOneTimestampSQL = `SELECT $1::timestamp;` @@ -100,7 +105,12 @@ func (q *DBQuerier) GetOneTimestamp(ctx context.Context, data *time.Time) (*time return zero, fmt.Errorf("query GetOneTimestamp: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[*time.Time]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*time.Time]) + if err != nil { + var zero *time.Time + return zero, fmt.Errorf("query GetOneTimestamp: %w", err) + } + return result, nil } const getManyTimestamptzsSQL = `SELECT * @@ -115,7 +125,12 @@ func (q *DBQuerier) GetManyTimestamptzs(ctx context.Context, data []time.Time) ( return zero, fmt.Errorf("query GetManyTimestamptzs: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[*time.Time]) + result, err := pgx.CollectRows(rows, pgx.RowTo[*time.Time]) + if err != nil { + var zero []*time.Time + return zero, fmt.Errorf("scan GetManyTimestamptzs row: %w", err) + } + return result, nil } const getManyTimestampsSQL = `SELECT * @@ -130,5 +145,10 @@ func (q *DBQuerier) GetManyTimestamps(ctx context.Context, data []*time.Time) ([ return zero, fmt.Errorf("query GetManyTimestamps: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[*time.Time]) + result, err := pgx.CollectRows(rows, pgx.RowTo[*time.Time]) + if err != nil { + var zero []*time.Time + return zero, fmt.Errorf("scan GetManyTimestamps row: %w", err) + } + return result, nil } diff --git a/example/syntax/query.sql.go b/example/syntax/query.sql.go index e961e293..18702757 100644 --- a/example/syntax/query.sql.go +++ b/example/syntax/query.sql.go @@ -117,7 +117,12 @@ func (q *DBQuerier) Backtick(ctx context.Context) (string, error) { return zero, fmt.Errorf("query Backtick: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query Backtick: %w", err) + } + return result, nil } const backtickQuoteBacktickSQL = "SELECT '`\"`';" @@ -131,7 +136,12 @@ func (q *DBQuerier) BacktickQuoteBacktick(ctx context.Context) (string, error) { return zero, fmt.Errorf("query BacktickQuoteBacktick: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query BacktickQuoteBacktick: %w", err) + } + return result, nil } const backtickNewlineSQL = "SELECT '`\n';" @@ -145,7 +155,12 @@ func (q *DBQuerier) BacktickNewline(ctx context.Context) (string, error) { return zero, fmt.Errorf("query BacktickNewline: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query BacktickNewline: %w", err) + } + return result, nil } const backtickDoubleQuoteSQL = "SELECT '`\"';" @@ -159,7 +174,12 @@ func (q *DBQuerier) BacktickDoubleQuote(ctx context.Context) (string, error) { return zero, fmt.Errorf("query BacktickDoubleQuote: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query BacktickDoubleQuote: %w", err) + } + return result, nil } const backtickBackslashNSQL = "SELECT '`\\n';" @@ -173,7 +193,12 @@ func (q *DBQuerier) BacktickBackslashN(ctx context.Context) (string, error) { return zero, fmt.Errorf("query BacktickBackslashN: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query BacktickBackslashN: %w", err) + } + return result, nil } const illegalNameSymbolsSQL = "SELECT '`\\n' as \"$\", $1 as \"foo.bar!@#$%&*()\"\"--+\";" @@ -192,7 +217,12 @@ func (q *DBQuerier) IllegalNameSymbols(ctx context.Context, helloWorld string) ( return zero, fmt.Errorf("query IllegalNameSymbols: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowToStructByName[IllegalNameSymbolsRow]) + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[IllegalNameSymbolsRow]) + if err != nil { + var zero IllegalNameSymbolsRow + return zero, fmt.Errorf("query IllegalNameSymbols: %w", err) + } + return result, nil } const spaceAfterSQL = `SELECT $1;` @@ -206,7 +236,12 @@ func (q *DBQuerier) SpaceAfter(ctx context.Context, space string) (string, error return zero, fmt.Errorf("query SpaceAfter: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query SpaceAfter: %w", err) + } + return result, nil } const badEnumNameSQL = `SELECT 'inconvertible_enum_name'::"123";` @@ -220,7 +255,12 @@ func (q *DBQuerier) BadEnumName(ctx context.Context) (UnnamedEnum123, error) { return zero, fmt.Errorf("query BadEnumName: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[UnnamedEnum123]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[UnnamedEnum123]) + if err != nil { + var zero UnnamedEnum123 + return zero, fmt.Errorf("query BadEnumName: %w", err) + } + return result, nil } const goKeywordSQL = `SELECT $1::text;` @@ -234,5 +274,10 @@ func (q *DBQuerier) GoKeyword(ctx context.Context, go_ string) (string, error) { return zero, fmt.Errorf("query GoKeyword: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query GoKeyword: %w", err) + } + return result, nil } diff --git a/example/void/query.sql.go b/example/void/query.sql.go index bb73bb98..05939016 100644 --- a/example/void/query.sql.go +++ b/example/void/query.sql.go @@ -111,13 +111,18 @@ func (q *DBQuerier) VoidTwo(ctx context.Context) (string, error) { return zero, fmt.Errorf("query VoidTwo: %w", err) } - return pgx.CollectOneRow(rows, func(row pgx.CollectableRow) (string, error) { - var item string - if err := row.Scan(nil, &item); err != nil { - return item, err + result, err := pgx.CollectOneRow(rows, func(row pgx.CollectableRow) (string, error) { + var item string + if err := row.Scan(nil, &item); err != nil { + return item, err + } + return item, nil + }) + if err != nil { + var zero string + return zero, fmt.Errorf("query VoidTwo: %w", err) } - return item, nil -}) + return result, nil } const voidThreeSQL = `SELECT void_fn(), 'foo' as foo, 'bar' as bar;` @@ -136,13 +141,18 @@ func (q *DBQuerier) VoidThree(ctx context.Context) (VoidThreeRow, error) { return zero, fmt.Errorf("query VoidThree: %w", err) } - return pgx.CollectOneRow(rows, func(row pgx.CollectableRow) (VoidThreeRow, error) { - var item VoidThreeRow - if err := row.Scan(nil, &item.Foo, &item.Bar); err != nil { - return item, err + result, err := pgx.CollectOneRow(rows, func(row pgx.CollectableRow) (VoidThreeRow, error) { + var item VoidThreeRow + if err := row.Scan(nil, &item.Foo, &item.Bar); err != nil { + return item, err + } + return item, nil + }) + if err != nil { + var zero VoidThreeRow + return zero, fmt.Errorf("query VoidThree: %w", err) } - return item, nil -}) + return result, nil } const voidThree2SQL = `SELECT 'foo' as foo, void_fn(), void_fn();` @@ -156,11 +166,16 @@ func (q *DBQuerier) VoidThree2(ctx context.Context) ([]string, error) { return zero, fmt.Errorf("query VoidThree2: %w", err) } - return pgx.CollectRows(rows, func(row pgx.CollectableRow) (string, error) { - var item string - if err := row.Scan(&item, nil, nil); err != nil { - return item, err + result, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (string, error) { + var item string + if err := row.Scan(&item, nil, nil); err != nil { + return item, err + } + return item, nil + }) + if err != nil { + var zero []string + return zero, fmt.Errorf("scan VoidThree2 row: %w", err) } - return item, nil -}) + return result, nil } diff --git a/internal/codegen/golang/query.gotemplate b/internal/codegen/golang/query.gotemplate index 3091030d..0617b8c2 100644 --- a/internal/codegen/golang/query.gotemplate +++ b/internal/codegen/golang/query.gotemplate @@ -69,8 +69,13 @@ func (q *DBQuerier) {{ $q.Name }}(ctx context.Context {{- $q.EmitParams }}) ({{ return zero, fmt.Errorf("query {{ $q.Name }}: %w", err) } - return {{ $q.EmitCollectionFunc }}(rows, {{ $q.EmitRowMapper }}) -{{- end }} + result, err := {{ $q.EmitCollectionFunc }}(rows, {{ $q.EmitRowMapper }}) + if err != nil { + {{ $q.EmitZeroResultInit "zero" }} + return zero, fmt.Errorf("{{ $q.EmitCollectionErrContext }}: %w", err) + } + return result, nil + {{- end }} } {{- end -}} {{- "\n" -}} diff --git a/internal/codegen/golang/templated_file.go b/internal/codegen/golang/templated_file.go index 12efebfc..1ea064c4 100644 --- a/internal/codegen/golang/templated_file.go +++ b/internal/codegen/golang/templated_file.go @@ -270,6 +270,19 @@ func (tq TemplatedQuery) EmitCollectionFunc() (string, error) { } } +func (tq TemplatedQuery) EmitCollectionErrContext() (string, error) { + switch tq.ResultKind { + case ast.ResultKindExec: + return "", fmt.Errorf("cannot EmitCollectionErrContext for :exec query %s", tq.Name) + case ast.ResultKindMany: + return "scan " + tq.Name + " row", nil + case ast.ResultKindOne: + return "query " + tq.Name, nil + default: + return "", fmt.Errorf("unhandled EmitCollectionErrContext type: %s", tq.ResultKind) + } +} + func (tq TemplatedQuery) EmitRowToFunc() (string, error) { switch tq.ResultKind { case ast.ResultKindExec: @@ -298,12 +311,12 @@ func (tq TemplatedQuery) EmitRowMapper() (string, error) { return "", err } return fmt.Sprintf(`func(row pgx.CollectableRow) (%s, error) { - var item %s - if err := row.Scan(%s); err != nil { - return item, err - } - return item, nil -}`, resultType, resultType, scanTargets), nil + var item %s + if err := row.Scan(%s); err != nil { + return item, err + } + return item, nil + }`, resultType, resultType, scanTargets), nil } func (tq TemplatedQuery) EmitScanTargets() (string, error) { diff --git a/internal/pg/query.sql.go b/internal/pg/query.sql.go index efd521c6..8eaa9f19 100644 --- a/internal/pg/query.sql.go +++ b/internal/pg/query.sql.go @@ -148,7 +148,12 @@ func (q *DBQuerier) FindEnumTypes(ctx context.Context, oids []uint32) ([]FindEnu return zero, fmt.Errorf("query FindEnumTypes: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindEnumTypesRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindEnumTypesRow]) + if err != nil { + var zero []FindEnumTypesRow + return zero, fmt.Errorf("scan FindEnumTypes row: %w", err) + } + return result, nil } const findArrayTypesSQL = `SELECT @@ -196,7 +201,12 @@ func (q *DBQuerier) FindArrayTypes(ctx context.Context, oids []uint32) ([]FindAr return zero, fmt.Errorf("query FindArrayTypes: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindArrayTypesRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindArrayTypesRow]) + if err != nil { + var zero []FindArrayTypesRow + return zero, fmt.Errorf("scan FindArrayTypes row: %w", err) + } + return result, nil } const findCompositeTypesSQL = `WITH table_cols AS ( @@ -249,7 +259,12 @@ func (q *DBQuerier) FindCompositeTypes(ctx context.Context, oids []uint32) ([]Fi return zero, fmt.Errorf("query FindCompositeTypes: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindCompositeTypesRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindCompositeTypesRow]) + if err != nil { + var zero []FindCompositeTypesRow + return zero, fmt.Errorf("scan FindCompositeTypes row: %w", err) + } + return result, nil } const findDescendantOIDsSQL = `WITH RECURSIVE oid_descs(oid) AS ( @@ -289,7 +304,12 @@ func (q *DBQuerier) FindDescendantOIDs(ctx context.Context, oids []uint32) ([]ui return zero, fmt.Errorf("query FindDescendantOIDs: %w", err) } - return pgx.CollectRows(rows, pgx.RowTo[uint32]) + result, err := pgx.CollectRows(rows, pgx.RowTo[uint32]) + if err != nil { + var zero []uint32 + return zero, fmt.Errorf("scan FindDescendantOIDs row: %w", err) + } + return result, nil } const findOIDByNameSQL = `SELECT oid @@ -307,7 +327,12 @@ func (q *DBQuerier) FindOIDByName(ctx context.Context, name string) (uint32, err return zero, fmt.Errorf("query FindOIDByName: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[uint32]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[uint32]) + if err != nil { + var zero uint32 + return zero, fmt.Errorf("query FindOIDByName: %w", err) + } + return result, nil } const findOIDNameSQL = `SELECT typname AS name @@ -323,7 +348,12 @@ func (q *DBQuerier) FindOIDName(ctx context.Context, oid uint32) (string, error) return zero, fmt.Errorf("query FindOIDName: %w", err) } - return pgx.CollectOneRow(rows, pgx.RowTo[string]) + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query FindOIDName: %w", err) + } + return result, nil } const findOIDNamesSQL = `SELECT oid, typname AS name, typtype AS kind @@ -345,5 +375,10 @@ func (q *DBQuerier) FindOIDNames(ctx context.Context, oid []uint32) ([]FindOIDNa return zero, fmt.Errorf("query FindOIDNames: %w", err) } - return pgx.CollectRows(rows, pgx.RowToStructByName[FindOIDNamesRow]) + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindOIDNamesRow]) + if err != nil { + var zero []FindOIDNamesRow + return zero, fmt.Errorf("scan FindOIDNames row: %w", err) + } + return result, nil } From a6e9a749132d9fea68f873819ef9b009c39328f5 Mon Sep 17 00:00:00 2001 From: meoyawn Date: Wed, 6 May 2026 21:42:44 +0000 Subject: [PATCH 16/16] Update README for pgx v5 --- README.md | 165 +++++++++++++++++++++++++++--------------------------- 1 file changed, 81 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 705cc4a9..bb952c34 100644 --- a/README.md +++ b/README.md @@ -105,12 +105,9 @@ Why should you use `pggen` instead of the [myriad] of Go SQL bindings? - pggen uses [pgx], a faster replacement for [lib/pq], the original Go Postgres library that's now in maintenance mode. -- pggen provides a batch (aka query pipelining) interface for each generated - query with [`pgx.Batch`]. Query pipelining is the reason Postgres sits atop - the [TechEmpower benchmarks]. Using a batch enables sending multiple queries - in a single network round-trip instead of one network round-trip per query. +- pggen-generated queriers work with pgx connection transports like + [`*pgx.Conn`], [`pgx.Tx`], and [`*pgxpool.Pool`]. -[TechEmpower benchmarks]: https://www.techempower.com/benchmarks/#section=data-r20&hw=ph&test=query [pgx]: https://github.com/jackc/pgx [lib/pq]: https://github.com/lib/pq @@ -368,31 +365,53 @@ Examples embedded in the repo: --go-type '_text=[]*github.com/jschaf/pggen/mytype.String' ``` - pgx must be able to decode the Postgres type using the given Go type. That - means the Go type must fulfill at least one of following: + pggen only changes the generated Go signatures and scan destinations. pgx + must still be able to decode the Postgres type using the given Go type. + That means the Go type must fulfill at least one of the following: - The Go type is a wrapper around primitive type, like `type AuthorID int`. pgx will use decode methods on the underlying primitive type. - - The Go type implements both [`pgtype.BinaryDecoder`] and - [`pgtype.TextDecoder`]. pgx will use the correct decoder based on the wire - format. See the [pgtype repo] for many example types. - - - The pgx connection executing the query must have registered a data type - using the Go type with [`ConnInfo.RegisterDataType`]. See the + - The Go type implements the scanner interfaces supported by the + [`pgtype.Codec`] for that Postgres type. See the [pgtype package] for + built-in codecs and interfaces. + + - The Go type implements [`sql.Scanner`]. Query parameters can also + implement [`driver.Valuer`]. + + - The pgx connection executing the query has registered the Postgres type + in its pgx v5 type map. For enum, composite, and array types that + pgx can inspect, pggen generates `RegisterTypes(ctx, *pgx.Conn)`, which + calls [`pgx.Conn.LoadType`] and [`pgtype.Map.RegisterType`]. + + ```go + conn, err := pgx.Connect(ctx, url) + if err != nil { + return err + } + if err := RegisterTypes(ctx, conn); err != nil { + return err + } + + q := NewQuerier(conn) + ``` + + If you use [`*pgxpool.Pool`], call `RegisterTypes` from + [`pgxpool.Config.AfterConnect`] so every pooled connection has the same + type map. + + - The pgx connection has a custom [`pgtype.Type`] registered with a + [`pgtype.Codec`]. This is useful for user-defined base types and extension + types that need application-provided codecs. See the [example/custom_types test] for an example. - + ```go - ci := conn.ConnInfo() - - ci.RegisterDataType(pgtype.DataType{ - Value: new(pgtype.Int2), - Name: "my_int", - OID: myIntOID, + conn.TypeMap().RegisterType(&pgtype.Type{ + Name: "my_int", + OID: myIntOID, + Codec: pgtype.Int2Codec{}, }) ``` - - - The Go type implements [`sql.Scanner`]. - pgx is able to use reflection to build an object to write fields into. @@ -416,11 +435,13 @@ Examples embedded in the repo: func (q *DBQuerier) FindCompositeUser(ctx context.Context) (User, error) {} ``` -[pgtype repo]: https://github.com/jackc/pgtype -[`pgtype.BinaryDecoder`]: https://pkg.go.dev/github.com/jackc/pgtype#BinaryDecoder -[`pgtype.TextDecoder`]: https://pkg.go.dev/github.com/jackc/pgtype#TextDecoder -[`ConnInfo.RegisterDataType`]: https://pkg.go.dev/github.com/jackc/pgtype#ConnInfo.RegisterDataType +[pgtype package]: https://pkg.go.dev/github.com/jackc/pgx/v5/pgtype +[`pgtype.Codec`]: https://pkg.go.dev/github.com/jackc/pgx/v5/pgtype#Codec +[`pgtype.Type`]: https://pkg.go.dev/github.com/jackc/pgx/v5/pgtype#Type +[`pgtype.Map.RegisterType`]: https://pkg.go.dev/github.com/jackc/pgx/v5/pgtype#Map.RegisterType +[`pgx.Conn.LoadType`]: https://pkg.go.dev/github.com/jackc/pgx/v5#Conn.LoadType [`sql.Scanner`]: https://golang.org/pkg/database/sql/#Scanner +[`driver.Valuer`]: https://pkg.go.dev/database/sql/driver#Valuer [composite types]: https://www.postgresql.org/docs/current/rowtypes.html [example/custom_types test]: ./example/custom_types/query.sql_test.go @@ -472,42 +493,15 @@ pggen gen go \ We'll walk through the generated file `author/query.sql.go`: -- The `Querier` interface defines the interface with methods for each SQL - query. Each SQL query compiles into three methods, one method for to run - the query by itself, and two methods to support batching a query with - [`pgx.Batch`]. +- The `Querier` interface defines one method for each SQL query. ```go // Querier is a typesafe Go interface backed by SQL queries. - // - // Methods ending with Batch enqueue a query to run later in a pgx.Batch. After - // calling SendBatch on pgx.Conn, pgxpool.Pool, or pgx.Tx, use the Scan methods - // to parse the results. type Querier interface { // FindAuthors finds authors by first name. FindAuthors(ctx context.Context, firstName string) ([]FindAuthorsRow, error) - // FindAuthorsBatch enqueues a FindAuthors query into batch to be executed - // later by the batch. - FindAuthorsBatch(batch *pgx.Batch, firstName string) - // FindAuthorsScan scans the result of an executed FindAuthorsBatch query. - FindAuthorsScan(results pgx.BatchResults) ([]FindAuthorsRow, error) } ``` - - To use the batch interface, create a `*pgx.Batch`, call the - `Batch` methods, send the batch, and finally get the results - with the `Scan` methods. See [example/author/query.sql_test.go] - for complete example. - - ```sql - q := NewQuerier(conn) - batch := &pgx.Batch{} - q.FindAuthorsBatch(batch, "alice") - q.FindAuthorsBatch(batch, "bob") - results := conn.SendBatch(context.Background(), batch) - aliceAuthors, err := q.FindAuthorsScan(results) - bobAuthors, err := q.FindAuthorsScan(results) - ``` - The `DBQuerier` struct implements the `Querier` interface with concrete implementations of each query method. @@ -532,6 +526,15 @@ We'll walk through the generated file `author/query.sql.go`: } } ``` + +- For custom PostgreSQL types, pggen generates `RegisterTypes`. Call it once + for each `*pgx.Conn` before running queries that use generated enum, + composite, or array types. + + ```go + // RegisterTypes loads custom PostgreSQL types into conn's pgx type map. + func RegisterTypes(ctx context.Context, conn *pgx.Conn) error {} + ``` - pggen embeds the SQL query formatted for a Postgres `PREPARE` statement with parameters indicated by `$1`, `$2`, etc. instead of @@ -549,10 +552,10 @@ We'll walk through the generated file `author/query.sql.go`: ```sql type FindAuthorsRow struct { - AuthorID int32 `json:"author_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Suffix pgtype.Text `json:"suffix"` + AuthorID int32 `json:"author_id" db:"author_id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Suffix *string `json:"suffix" db:"suffix"` } ``` @@ -571,11 +574,12 @@ We'll walk through the generated file `author/query.sql.go`: that column can have both `text` and `null` values. So, the Postgres `text` represented in Go can be either a `string` or `nil`. [`pgtype`] provides nullable types for all built-in Postgres types. pggen tries to infer if a - column is nullable or non-nullable. If a column is nullable, pggen uses a - `pgtype` Go type like `pgtype.Text`. If a column is non-nullable, pggen uses - a more ergonomic type like `string`. pggen's nullability inference - implemented in [internal/pginfer/nullability.go] is rudimentary; a proper - approach requires a full explain-plan with some control flow analysis. + column is nullable or non-nullable. If a column is nullable, pggen uses a + nullable Go type like `*string` or a `pgtype` type. If a column is + non-nullable, pggen uses a more ergonomic type like `string`. pggen's + nullability inference implemented in [internal/pginfer/nullability.go] is + rudimentary; a proper approach requires a full explain-plan with some + control flow analysis. - Lastly, pggen generates the implementation for each query. @@ -587,36 +591,29 @@ We'll walk through the generated file `author/query.sql.go`: ```sql // FindAuthors implements Querier.FindAuthors. func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAuthorsRow, error) { + ctx = context.WithValue(ctx, QueryName{}, "FindAuthors") rows, err := q.conn.Query(ctx, findAuthorsSQL, firstName) - if rows != nil { - defer rows.Close() - } if err != nil { - return nil, fmt.Errorf("query FindAuthors: %w", err) - } - items := []FindAuthorsRow{} - for rows.Next() { - var item FindAuthorsRow - if err := rows.Scan(&item.AuthorID, &item.FirstName, &item.LastName, &item.Suffix); err != nil { - return nil, fmt.Errorf("scan FindAuthors row: %w", err) - } - items = append(items, item) + var zero []FindAuthorsRow + return zero, fmt.Errorf("query FindAuthors: %w", err) } - if err := rows.Err(); err != nil { - return nil, err + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorsRow]) + if err != nil { + var zero []FindAuthorsRow + return zero, fmt.Errorf("scan FindAuthors row: %w", err) } - return items, err + return result, nil } ``` -[example/author/query.sql_test.go]: ./example/author/query.sql_test.go -[`pgx.Batch`]: https://pkg.go.dev/github.com/jackc/pgx#Batch -[`*pgx.Conn`]: https://pkg.go.dev/github.com/jackc/pgx#Conn -[`pgx.Tx`]: https://pkg.go.dev/github.com/jackc/pgx#Tx -[`*pgxpool.Pool`]: https://pkg.go.dev/github.com/jackc/pgx/v4/pgxpool#Pool +[`*pgx.Conn`]: https://pkg.go.dev/github.com/jackc/pgx/v5#Conn +[`pgx.Tx`]: https://pkg.go.dev/github.com/jackc/pgx/v5#Tx +[`*pgxpool.Pool`]: https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Pool +[`pgxpool.Config.AfterConnect`]: https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Config [internal/casing/casing.go]: ./internal/casing/casing.go [internal/codegen/golang/gotype/types.go]: ./internal/codegen/golang/gotype/types.go -[`pgtype`]: https://pkg.go.dev/github.com/jackc/pgtype +[`pgtype`]: https://pkg.go.dev/github.com/jackc/pgx/v5/pgtype [internal/pginfer/nullability.go]: ./internal/pginfer/nullability.go # Contributing