diff --git a/Makefile b/Makefile index 823c3480..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 @@ -29,7 +32,7 @@ psql: .PHONY: test test: - go test ./... + go run ./internal/pgdocker/cmd/pgdocker -- go test ./... .PHONY: acceptance-test acceptance-test: 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 diff --git a/example/acceptance_test.go b/example/acceptance_test.go index c1e231a9..cfe30a7e 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.Array[pgtype.Text]", }, }, { 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 e654ea13..8c19d2cf 100644 --- a/example/author/query.sql.go +++ b/example/author/query.sql.go @@ -5,11 +5,12 @@ 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" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { // FindAuthorById finds one (or zero) authors by ID. @@ -24,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) @@ -48,8 +52,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. @@ -61,150 +64,172 @@ 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 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 { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindAuthorByIDRow]) + if err != nil { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + return result, nil } 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) + var zero []FindAuthorsRow + return zero, fmt.Errorf("query FindAuthors: %w", err) } - defer rows.Close() - 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) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindAuthors rows: %w", 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 } 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) + var zero []FindAuthorNamesRow + return zero, fmt.Errorf("query FindAuthorNames: %w", err) } - defer rows.Close() - items := []FindAuthorNamesRow{} - for rows.Next() { - var item FindAuthorNamesRow - if err := rows.Scan(&item.FirstName, &item.LastName); err != nil { - return nil, fmt.Errorf("scan FindAuthorNames row: %w", err) - } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindAuthorNames rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindAuthorNamesRow]) + if err != nil { + var zero []FindAuthorNamesRow + return zero, fmt.Errorf("scan FindAuthorNames row: %w", err) } - return items, err + return result, nil } 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) + var zero []*string + return zero, fmt.Errorf("query FindFirstNames: %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 FindFirstNames row: %w", err) - } - items = append(items, &item) + + 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;` + +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) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindFirstNames rows: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[FindFirstAuthorRow]) + if err != nil { + var zero FindFirstAuthorRow + return zero, fmt.Errorf("query FindFirstAuthor: %w", err) } - return items, err + return result, nil } 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 cmdTag, fmt.Errorf("exec query DeleteAuthors: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthors: %w", err) } return cmdTag, err } @@ -213,10 +238,10 @@ 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 cmdTag, fmt.Errorf("exec query DeleteAuthorsByFirstName: %w", err) + return pgconn.CommandTag{}, fmt.Errorf("exec query DeleteAuthorsByFirstName: %w", err) } return cmdTag, err } @@ -235,10 +260,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 } @@ -249,13 +274,19 @@ 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 { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + 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) @@ -269,70 +300,63 @@ 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") - 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 { - return item, fmt.Errorf("query InsertAuthorSuffix: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "InsertAuthorSuffix") + rows, err := q.conn.Query(ctx, insertAuthorSuffixSQL, params.FirstName, params.LastName, params.Suffix) + if err != nil { + var zero InsertAuthorSuffixRow + return zero, fmt.Errorf("query InsertAuthorSuffix: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertAuthorSuffixRow]) + if err != nil { + var zero InsertAuthorSuffixRow + return zero, fmt.Errorf("query InsertAuthorSuffix: %w", err) } - return item, nil + return result, nil } 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") - 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) + ctx = context.WithValue(ctx, QueryName{}, "StringAggFirstName") + rows, err := q.conn.Query(ctx, stringAggFirstNameSQL, authorID) + if err != nil { + var zero *string + return zero, fmt.Errorf("query StringAggFirstName: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*string]) + if err != nil { + var zero *string + return zero, fmt.Errorf("query StringAggFirstName: %w", err) } - return item, nil + return result, nil } 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") - row := q.conn.QueryRow(ctx, arrayAggFirstNameSQL, authorID) - item := []string{} - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query ArrayAggFirstName: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "ArrayAggFirstName") + rows, err := q.conn.Query(ctx, arrayAggFirstNameSQL, authorID) + if err != nil { + var zero []string + return zero, fmt.Errorf("query ArrayAggFirstName: %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 + 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 } - -// 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/author/query.sql_test.go b/example/author/query.sql_test.go index 991ed5c3..8622b3e6 100644 --- a/example/author/query.sql_test.go +++ b/example/author/query.sql_test.go @@ -7,7 +7,7 @@ import ( "github.com/jschaf/pggen/internal/ptrs" "github.com/stretchr/testify/require" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" ) @@ -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) @@ -63,6 +64,26 @@ 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(t.Context(), InsertAuthorSuffixParams{ + FirstName: "bill", + LastName: "clinton", + Suffix: "jr", + }) + require.NoError(t, err) + authors, err := q.FindAuthors(t.Context(), "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(t.Context(), "george") require.NoError(t, err) @@ -95,6 +116,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 4315bdc5..c49bbb99 100644 --- a/example/complex_params/query.sql.go +++ b/example/complex_params/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,161 @@ 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.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 -} - -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 + 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++ } - 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, + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } + return nil } -// 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()}, - ) -} +var typesToRegister = []string{} -// 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)) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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), - } -} +var _ = addTypeToRegister("\"dimensions\"") -// 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_set_type\"") -// 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\"") -// 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 -} +var _ = addTypeToRegister("\"_product_image_type\"") 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 { + var zero []int + return zero, fmt.Errorf("query ParamArrayInt: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]int]) + if err != nil { + var zero []int + return zero, fmt.Errorf("query ParamArrayInt: %w", err) } - return item, nil + return result, nil } 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 { + var zero Dimensions + return zero, fmt.Errorf("query ParamNested1: %w", err) } - if err := dimensionsRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ParamNested1 row: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[Dimensions]) + if err != nil { + var zero Dimensions + return zero, fmt.Errorf("query ParamNested1: %w", err) } - return item, nil + return result, nil } 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 { + var zero ProductImageType + return zero, fmt.Errorf("query ParamNested2: %w", err) } - if err := productImageTypeRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ParamNested2 row: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[ProductImageType]) + if err != nil { + var zero ProductImageType + return zero, fmt.Errorf("query ParamNested2: %w", err) } - return item, nil + return result, nil } 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 { + var zero []ProductImageType + return zero, fmt.Errorf("query ParamNested2Array: %w", err) } - if err := productImageTypeArray.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ParamNested2Array row: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]ProductImageType]) + if err != nil { + var zero []ProductImageType + return zero, fmt.Errorf("query ParamNested2Array: %w", err) } - return item, nil + return result, nil } 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) + ctx = context.WithValue(ctx, QueryName{}, "ParamNested3") + rows, err := q.conn.Query(ctx, paramNested3SQL, imageSet) + if err != nil { + var zero ProductImageSetType + return zero, fmt.Errorf("query ParamNested3: %w", err) } - if err := productImageSetTypeRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ParamNested3 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 + 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 } - -// 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/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/codegen_test.go b/example/composite/codegen_test.go index a412ccb2..39613ff4 100644 --- a/example/composite/codegen_test.go +++ b/example/composite/codegen_test.go @@ -29,7 +29,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..4edda308 100644 --- a/example/composite/query.sql.go +++ b/example/composite/query.sql.go @@ -5,11 +5,13 @@ 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" ) +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,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} } // 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"` -} - -// 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 -} - -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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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{}}, - ) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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)) -} +var _ = addTypeToRegister("\"arrays\"") -// 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{}}, - ) -} - -// 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 -} +var _ = addTypeToRegister("\"_blocks\"") const searchScreenshotsSQL = `SELECT ss.id, @@ -226,34 +123,25 @@ 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) + var zero []SearchScreenshotsRow + return zero, fmt.Errorf("query SearchScreenshots: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close SearchScreenshots rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[SearchScreenshotsRow]) + if err != nil { + var zero []SearchScreenshotsRow + return zero, fmt.Errorf("scan SearchScreenshots row: %w", err) } - return items, err + return result, nil } const searchScreenshotsOneColSQL = `SELECT @@ -273,28 +161,19 @@ 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) + var zero [][]Blocks + return zero, 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) + + result, err := pgx.CollectRows(rows, pgx.RowTo[[]Blocks]) + if err != nil { + var zero [][]Blocks + return zero, fmt.Errorf("scan SearchScreenshotsOneCol row: %w", err) } - return items, err + return result, nil } const insertScreenshotBlocksSQL = `WITH screens AS ( @@ -307,77 +186,62 @@ 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 { + var zero InsertScreenshotBlocksRow + return zero, fmt.Errorf("query InsertScreenshotBlocks: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[InsertScreenshotBlocksRow]) + if err != nil { + var zero InsertScreenshotBlocksRow + return zero, fmt.Errorf("query InsertScreenshotBlocks: %w", err) } - return item, nil + return result, nil } 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) + ctx = context.WithValue(ctx, QueryName{}, "ArraysInput") + rows, err := q.conn.Query(ctx, arraysInputSQL, arrays) + if err != nil { + var zero Arrays + return zero, fmt.Errorf("query ArraysInput: %w", err) } - if err := arraysRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ArraysInput row: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[Arrays]) + if err != nil { + var zero Arrays + return zero, fmt.Errorf("query ArraysInput: %w", err) } - return item, nil + return result, nil } 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 { + var zero UserEmail + return zero, 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 -} - -// 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 + 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 } - -// 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 4c68fdd4..1da4ea88 100644 --- a/example/composite/query.sql_test.go +++ b/example/composite/query.sql_test.go @@ -3,7 +3,8 @@ package composite import ( "testing" - "github.com/jackc/pgtype" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" "github.com/jschaf/pggen/internal/difftest" "github.com/jschaf/pggen/internal/pgtest" "github.com/jschaf/pggen/internal/ptrs" @@ -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) @@ -87,7 +94,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) } @@ -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/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 133c424c..7a1f4e15 100644 --- a/example/custom_types/query.sql.go +++ b/example/custom_types/query.sql.go @@ -5,16 +5,19 @@ 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/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) + CustomString(ctx context.Context) (mytype.String, error) + CustomMyInt(ctx context.Context) (int, error) IntArray(ctx context.Context) ([][]int32, error) @@ -23,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. @@ -36,114 +38,120 @@ 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 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 { + var zero CustomTypesRow + return zero, fmt.Errorf("query CustomTypes: %w", err) + } + + 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;` + +// 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 item, nil + + 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;` // 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 { + var zero int + return zero, fmt.Errorf("query CustomMyInt: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[int]) + if err != nil { + var zero int + return zero, fmt.Errorf("query CustomMyInt: %w", err) } - return item, nil + return result, nil } 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) + var zero [][]int32 + return zero, fmt.Errorf("query IntArray: %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 + 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 } - -// 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 3888956a..77d8d422 100644 --- a/example/custom_types/query.sql_test.go +++ b/example/custom_types/query.sql_test.go @@ -3,7 +3,8 @@ package custom_types import ( "testing" - "github.com/jackc/pgtype" + "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() @@ -38,15 +49,15 @@ func TestQuerier_CustomMyInt(t *testing.T) { AND pn.nspname = current_schema() LIMIT 1; `)) - oidVal := pgtype.OIDValue{} - 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 d2b6fcd8..5fe9dd59 100644 --- a/example/device/query.sql.go +++ b/example/device/query.sql.go @@ -5,11 +5,13 @@ 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" + "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,43 @@ 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 -} +var _ = addTypeToRegister("\"device_type\"") -// 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("\"user\"") const findDevicesByUserSQL = `SELECT id, @@ -164,31 +113,26 @@ 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) + var zero []FindDevicesByUserRow + return zero, fmt.Errorf("query FindDevicesByUser: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindDevicesByUser rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindDevicesByUserRow]) + if err != nil { + var zero []FindDevicesByUserRow + return zero, fmt.Errorf("scan FindDevicesByUser row: %w", err) } - return items, err + return result, nil } const compositeUserSQL = `SELECT @@ -199,102 +143,88 @@ 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) + var zero []CompositeUserRow + return zero, 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) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[CompositeUserRow]) + if err != nil { + var zero []CompositeUserRow + return zero, fmt.Errorf("scan CompositeUser row: %w", err) } - return items, err + return result, nil } 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) + ctx = context.WithValue(ctx, QueryName{}, "CompositeUserOne") + rows, err := q.conn.Query(ctx, compositeUserOneSQL) + if err != nil { + var zero User + return zero, fmt.Errorf("query CompositeUserOne: %w", err) } - if err := userRow.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign CompositeUserOne row: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[User]) + if err != nil { + var zero User + return zero, fmt.Errorf("query CompositeUserOne: %w", err) } - return item, nil + return result, nil } 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) + ctx = context.WithValue(ctx, QueryName{}, "CompositeUserOneTwoCols") + rows, err := q.conn.Query(ctx, compositeUserOneTwoColsSQL) + if err != nil { + var zero CompositeUserOneTwoColsRow + return zero, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) } - if err := userRow.AssignTo(&item.User); err != nil { - return item, fmt.Errorf("assign CompositeUserOneTwoCols row: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[CompositeUserOneTwoColsRow]) + if err != nil { + var zero CompositeUserOneTwoColsRow + return zero, fmt.Errorf("query CompositeUserOneTwoCols: %w", err) } - return item, nil + return result, nil } 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) + var zero []User + return zero, fmt.Errorf("query CompositeUserMany: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close CompositeUserMany rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowTo[User]) + if err != nil { + var zero []User + return zero, fmt.Errorf("scan CompositeUserMany row: %w", err) } - return items, err + return result, nil } const insertUserSQL = `INSERT INTO "user" (id, name) @@ -302,10 +232,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 +244,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 2c4f3223..571ac386 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/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -13,13 +12,14 @@ 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 _, 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 +27,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) @@ -46,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() @@ -56,9 +50,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 +60,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}, }, @@ -83,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/domain/query.sql.go b/example/domain/query.sql.go index 01130f2c..ccdf872c 100644 --- a/example/domain/query.sql.go +++ b/example/domain/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,58 @@ 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 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 { + var zero string + return zero, 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} -} - -func (t textPreferrer) TypeName() string { - return t.typeName + 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 } - -// 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 cbaea2ed..6c23538b 100644 --- a/example/enums/query.sql.go +++ b/example/enums/query.sql.go @@ -5,16 +5,18 @@ 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" + "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,80 @@ 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 - } - 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)) +// 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 vt + return nil } -type compositeField struct { - name string // name of the field - typeName string // Postgres type name - defaultVal pgtype.ValueTranscoder // default value to use -} +var typesToRegister = []string{} -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 addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -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("\"device_type\"") -// 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\"") -// 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_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) + var zero []FindAllDevicesRow + return zero, fmt.Errorf("query FindAllDevices: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindAllDevices rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindAllDevicesRow]) + if err != nil { + var zero []FindAllDevicesRow + return zero, fmt.Errorf("scan FindAllDevices row: %w", err) } - return items, err + return result, nil } 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 +151,19 @@ 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) + ctx = context.WithValue(ctx, QueryName{}, "FindOneDeviceArray") + rows, err := q.conn.Query(ctx, findOneDeviceArraySQL) + if err != nil { + var zero []DeviceType + return zero, fmt.Errorf("query FindOneDeviceArray: %w", err) } - if err := deviceTypesArray.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign FindOneDeviceArray row: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]DeviceType]) + if err != nil { + var zero []DeviceType + return zero, fmt.Errorf("query FindOneDeviceArray: %w", err) } - return item, nil + return result, nil } const findManyDeviceArraySQL = `SELECT enum_range('ipad'::device_type, 'iot'::device_type) AS device_types @@ -246,28 +172,19 @@ 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) + var zero [][]DeviceType + return zero, 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) + + result, err := pgx.CollectRows(rows, pgx.RowTo[[]DeviceType]) + if err != nil { + var zero [][]DeviceType + return zero, fmt.Errorf("scan FindManyDeviceArray row: %w", err) } - return items, err + return result, nil } const findManyDeviceArrayWithNumSQL = `SELECT 1 AS num, enum_range('ipad'::device_type, 'iot'::device_type) AS device_types @@ -275,74 +192,42 @@ 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) + var zero []FindManyDeviceArrayWithNumRow + return zero, fmt.Errorf("query FindManyDeviceArrayWithNum: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindManyDeviceArrayWithNum rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindManyDeviceArrayWithNumRow]) + if err != nil { + var zero []FindManyDeviceArrayWithNumRow + return zero, fmt.Errorf("scan FindManyDeviceArrayWithNum row: %w", err) } - return items, err + return result, nil } 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 { + var zero Device + return zero, 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 -} -// 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 + 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 } - -// 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 520c4bb8..ae39ba95 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/pgtype" "github.com/jschaf/pggen/internal/pgtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,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") @@ -28,7 +28,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, ) @@ -50,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) @@ -65,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) @@ -80,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) @@ -99,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") @@ -107,7 +111,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 +120,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 ca372b20..17a1a3f8 100644 --- a/example/erp/order/customer.sql.go +++ b/example/erp/order/customer.sql.go @@ -5,11 +5,13 @@ 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" ) +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,26 @@ 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 { + var zero CreateTenantRow + return zero, fmt.Errorf("query CreateTenant: %w", err) } - return item, nil + + 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 * @@ -101,32 +113,27 @@ 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) + var zero []FindOrdersByCustomerRow + return zero, fmt.Errorf("query FindOrdersByCustomer: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindOrdersByCustomer rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByCustomerRow]) + if err != nil { + var zero []FindOrdersByCustomerRow + return zero, fmt.Errorf("scan FindOrdersByCustomer row: %w", err) } - return items, err + return result, nil } const findProductsInOrderSQL = `SELECT o.order_id, p.product_id, p.name @@ -136,31 +143,26 @@ 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) + var zero []FindProductsInOrderRow + return zero, 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) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindProductsInOrderRow]) + if err != nil { + var zero []FindProductsInOrderRow + return zero, fmt.Errorf("scan FindProductsInOrder row: %w", err) } - return items, err + return result, nil } const insertCustomerSQL = `INSERT INTO customer (first_name, last_name, email) @@ -174,21 +176,27 @@ 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 { + var zero InsertCustomerRow + return zero, fmt.Errorf("query InsertCustomer: %w", err) } - return item, nil + + 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) @@ -202,44 +210,25 @@ 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 { + var zero InsertOrderRow + return zero, 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 + 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 } - -// 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 c5889f88..6f757c79 100644 --- a/example/erp/order/customer.sql_test.go +++ b/example/erp/order/customer.sql_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "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" @@ -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 2395a2ca..cd0d7190 100644 --- a/example/erp/order/price.sql.go +++ b/example/erp/order/price.sql.go @@ -5,38 +5,34 @@ package order import ( "context" "fmt" - "github.com/jackc/pgtype" + "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) + var zero []FindOrdersByPriceRow + return zero, 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) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersByPriceRow]) + if err != nil { + var zero []FindOrdersByPriceRow + return zero, fmt.Errorf("scan FindOrdersByPrice row: %w", err) } - return items, err + return result, nil } const findOrdersMRRSQL = `SELECT date_trunc('month', order_date) AS month, sum(order_total) AS order_mrr @@ -44,28 +40,23 @@ 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) + var zero []FindOrdersMRRRow + return zero, 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) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindOrdersMRRRow]) + if err != nil { + var zero []FindOrdersMRRRow + return zero, fmt.Errorf("scan FindOrdersMRR row: %w", err) } - return items, err + return result, nil } diff --git a/example/function/query.sql.go b/example/function/query.sql.go index ebfd5e7c..4d3b3aee 100644 --- a/example/function/query.sql.go +++ b/example/function/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,81 @@ 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 - } - 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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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{}}, - ) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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_item\"") -// 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_stats\"") + +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) + var zero []OutParamsRow + return zero, fmt.Errorf("query OutParams: %w", err) } - 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 -} - -// 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 + 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 } - -// 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/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/go_pointer_types/query.sql.go b/example/go_pointer_types/query.sql.go index fce3f65d..2bca0609 100644 --- a/example/go_pointer_types/query.sql.go +++ b/example/go_pointer_types/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,19 @@ 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 { + var zero *int + return zero, fmt.Errorf("query GenSeries1: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[*int]) + if err != nil { + var zero *int + return zero, fmt.Errorf("query GenSeries1: %w", err) } - return item, nil + return result, nil } const genSeriesSQL = `SELECT n @@ -93,24 +104,19 @@ 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) + var zero []*int + return zero, fmt.Errorf("query GenSeries: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GenSeries rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowTo[*int]) + if err != nil { + var zero []*int + return zero, fmt.Errorf("scan GenSeries row: %w", err) } - return items, err + return result, nil } const genSeriesArr1SQL = `SELECT array_agg(n) @@ -118,13 +124,19 @@ 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 { + var zero []int + return zero, fmt.Errorf("query GenSeriesArr1: %w", err) } - return item, nil + + 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) @@ -132,24 +144,19 @@ 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) + var zero [][]int + return zero, fmt.Errorf("query GenSeriesArr: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GenSeriesArr rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowTo[[]int]) + if err != nil { + var zero [][]int + return zero, fmt.Errorf("scan GenSeriesArr row: %w", err) } - return items, err + return result, nil } const genSeriesStr1SQL = `SELECT n::text @@ -158,13 +165,19 @@ 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 { + var zero *string + return zero, fmt.Errorf("query GenSeriesStr1: %w", err) } - return item, nil + + 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 @@ -172,47 +185,17 @@ 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) + var zero []*string + return zero, fmt.Errorf("query GenSeriesStr: %w", err) } - 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 + 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 } - -// 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 4b4bbace..61529bf7 100644 --- a/example/inline_param_count/inline0/query.sql.go +++ b/example/inline_param_count/inline0/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,60 @@ 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 { + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) } - return item, nil + + 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;` @@ -93,21 +104,27 @@ 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 { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + + 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) @@ -121,13 +138,19 @@ 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 { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + 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 @@ -144,35 +167,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/inline0/query.sql_test.go b/example/inline_param_count/inline0/query.sql_test.go index b51099a1..58edb1cf 100644 --- a/example/inline_param_count/inline0/query.sql_test.go +++ b/example/inline_param_count/inline0/query.sql_test.go @@ -4,9 +4,9 @@ import ( "errors" "testing" + "github.com/jackc/pgx/v5" "github.com/stretchr/testify/require" - "github.com/jackc/pgx/v4" "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..134befbf 100644 --- a/example/inline_param_count/inline1/query.sql.go +++ b/example/inline_param_count/inline1/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,86 @@ 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 { + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) } - return item, nil + + 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;` 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 { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + + 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) @@ -117,13 +134,19 @@ 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 { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + 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 @@ -140,35 +163,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_test.go b/example/inline_param_count/inline1/query.sql_test.go index 73d8d16c..3e8e447d 100644 --- a/example/inline_param_count/inline1/query.sql_test.go +++ b/example/inline_param_count/inline1/query.sql_test.go @@ -4,9 +4,9 @@ import ( "errors" "testing" + "github.com/jackc/pgx/v5" "github.com/stretchr/testify/require" - "github.com/jackc/pgx/v4" "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..96dd4320 100644 --- a/example/inline_param_count/inline2/query.sql.go +++ b/example/inline_param_count/inline2/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,86 @@ 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 { + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) } - return item, nil + + 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;` 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 { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + + 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) @@ -112,13 +129,19 @@ 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 { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + 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 @@ -135,35 +158,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_test.go b/example/inline_param_count/inline2/query.sql_test.go index 3edcd1a7..1bf7b35d 100644 --- a/example/inline_param_count/inline2/query.sql_test.go +++ b/example/inline_param_count/inline2/query.sql_test.go @@ -4,9 +4,9 @@ import ( "errors" "testing" + "github.com/jackc/pgx/v5" "github.com/stretchr/testify/require" - "github.com/jackc/pgx/v4" "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..8ef8b669 100644 --- a/example/inline_param_count/inline3/query.sql.go +++ b/example/inline_param_count/inline3/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,86 @@ 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 { + var zero *int + return zero, fmt.Errorf("query CountAuthors: %w", err) } - return item, nil + + 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;` 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 { + var zero FindAuthorByIDRow + return zero, fmt.Errorf("query FindAuthorByID: %w", err) } - return item, nil + + 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) @@ -112,13 +129,19 @@ 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 { + var zero int32 + return zero, fmt.Errorf("query InsertAuthor: %w", err) } - return item, nil + + 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 @@ -129,35 +152,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/inline_param_count/inline3/query.sql_test.go b/example/inline_param_count/inline3/query.sql_test.go index bbf18d32..99b41fef 100644 --- a/example/inline_param_count/inline3/query.sql_test.go +++ b/example/inline_param_count/inline3/query.sql_test.go @@ -4,9 +4,9 @@ import ( "errors" "testing" + "github.com/jackc/pgx/v5" "github.com/stretchr/testify/require" - "github.com/jackc/pgx/v4" "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 ff77e4bf..bc62aadf 100644 --- a/example/ltree/codegen_test.go +++ b/example/ltree/codegen_test.go @@ -24,8 +24,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.Array[pgtype.Text]", }, }) if err != nil { diff --git a/example/ltree/query.sql.go b/example/ltree/query.sql.go index aee3db7f..323cab16 100644 --- a/example/ltree/query.sql.go +++ b/example/ltree/query.sql.go @@ -5,16 +5,18 @@ 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" ) +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) (pgtype.Array[pgtype.Text], 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,19 @@ 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) + var zero []pgtype.Text + return zero, 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) + + 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 items, err + return result, nil } const findTopScienceChildrenAggSQL = `SELECT array_agg(path) @@ -100,14 +101,20 @@ 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) (pgtype.Array[pgtype.Text], error) { + ctx = context.WithValue(ctx, QueryName{}, "FindTopScienceChildrenAgg") + rows, err := q.conn.Query(ctx, findTopScienceChildrenAggSQL) + if err != nil { + var zero pgtype.Array[pgtype.Text] + return zero, fmt.Errorf("query FindTopScienceChildrenAgg: %w", err) + } + + 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 item, nil + return result, nil } const insertSampleDataSQL = `INSERT INTO test @@ -127,10 +134,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 +154,23 @@ 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 pgtype.Array[pgtype.Text] `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 { + var zero FindLtreeInputRow + return zero, 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 -} - -// 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 + 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 } - -// 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 eb73110a..14041b45 100644 --- a/example/ltree/query.sql_test.go +++ b/example/ltree/query.sql_test.go @@ -3,7 +3,7 @@ package ltree import ( "testing" - "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" @@ -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,34 @@ func TestQuerier(t *testing.T) { { rows, err := q.FindTopScienceChildrenAgg(ctx) require.NoError(t, err) - want := pgtype.TextArray{ + want := pgtype.Array[pgtype.Text]{ 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}, + {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}, }, - Status: pgtype.Present, - Dimensions: []pgtype.ArrayDimension{{Length: 4, LowerBound: 1}}, + Dims: []pgtype.ArrayDimension{{Length: 4, LowerBound: 1}}, + Valid: true, } 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, + 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) } } - -// 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 1ffb4074..da84e485 100644 --- a/example/nested/query.sql.go +++ b/example/nested/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,144 +33,69 @@ 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.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 -} - -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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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{}}, - ) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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()}, - ) -} +var _ = addTypeToRegister("\"dimensions\"") -// 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_set_type\"") -// 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) -} +var _ = addTypeToRegister("\"product_image_type\"") + +var _ = addTypeToRegister("\"_product_image_type\"") const arrayNested2SQL = `SELECT ARRAY [ @@ -180,17 +105,19 @@ 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) + ctx = context.WithValue(ctx, QueryName{}, "ArrayNested2") + rows, err := q.conn.Query(ctx, arrayNested2SQL) + if err != nil { + var zero []ProductImageType + return zero, fmt.Errorf("query ArrayNested2: %w", err) } - if err := imagesArray.AssignTo(&item); err != nil { - return item, fmt.Errorf("assign ArrayNested2 row: %w", err) + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]ProductImageType]) + if err != nil { + var zero []ProductImageType + return zero, fmt.Errorf("query ArrayNested2: %w", err) } - return item, nil + return result, nil } const nested3SQL = `SELECT @@ -205,51 +132,17 @@ 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) + var zero []ProductImageSetType + return zero, fmt.Errorf("query Nested3: %w", err) } - 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 -} - -// 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 + 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 } - -// 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/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/pgcrypto/query.sql.go b/example/pgcrypto/query.sql.go index 90be999c..8432a28f 100644 --- a/example/pgcrypto/query.sql.go +++ b/example/pgcrypto/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,23 @@ 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 { + var zero FindUserRow + return zero, 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} -} - -func (t textPreferrer) TypeName() string { - return t.typeName + 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 } - -// 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 bfebba47..66d2b38d 100644 --- a/example/separate_out_dir/out/alpha_query.sql.0.go +++ b/example/separate_out_dir/out/alpha_query.sql.0.go @@ -5,11 +5,12 @@ 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" ) +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,86 @@ 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 - } - 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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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{}}, - ) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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\"") + +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 { + var zero string + return zero, fmt.Errorf("query AlphaNested: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query AlphaNested: %w", err) } - return item, nil + return result, nil } 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 { + var zero []Alpha + return zero, 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 + 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 } - -// 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..1de74a95 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,24 @@ 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 { + var zero string + return zero, fmt.Errorf("query Alpha: %w", err) } - return item, nil + + 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 d3869f04..455610a6 100644 --- a/example/separate_out_dir/out/bravo_query.sql.go +++ b/example/separate_out_dir/out/bravo_query.sql.go @@ -5,17 +5,24 @@ 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 { + var zero string + return zero, fmt.Errorf("query Bravo: %w", err) } - return item, nil + + 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/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/slices/query.sql.go b/example/slices/query.sql.go index 8ef9062c..83573803 100644 --- a/example/slices/query.sql.go +++ b/example/slices/query.sql.go @@ -5,12 +5,13 @@ 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" "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,79 @@ 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 -} - -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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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 +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } 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 { + var zero []bool + return zero, fmt.Errorf("query GetBools: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[[]bool]) + if err != nil { + var zero []bool + return zero, fmt.Errorf("query GetBools: %w", err) } - return item, nil + return result, nil } 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 { + var zero *time.Time + return zero, fmt.Errorf("query GetOneTimestamp: %w", err) + } + + 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 item, nil + return result, nil } const getManyTimestamptzsSQL = `SELECT * @@ -162,24 +118,19 @@ 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) + var zero []*time.Time + return zero, fmt.Errorf("query GetManyTimestamptzs: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close GetManyTimestamptzs rows: %w", err) + + 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 items, err + return result, nil } const getManyTimestampsSQL = `SELECT * @@ -187,47 +138,17 @@ 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) + var zero []*time.Time + return zero, 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} -} - -func (t textPreferrer) TypeName() string { - return t.typeName + 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 } - -// 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 b221822c..18702757 100644 --- a/example/syntax/query.sql.go +++ b/example/syntax/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,214 @@ 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 { + var zero string + return zero, fmt.Errorf("query Backtick: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query Backtick: %w", err) } - return item, nil + return result, nil } 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 { + var zero string + return zero, fmt.Errorf("query BacktickQuoteBacktick: %w", err) } - return item, nil + + 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';" // 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 { + var zero string + return zero, fmt.Errorf("query BacktickNewline: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query BacktickNewline: %w", err) } - return item, nil + return result, nil } 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 { + var zero string + return zero, fmt.Errorf("query BacktickDoubleQuote: %w", err) } - return item, nil + + 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';" // 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 { + var zero string + return zero, fmt.Errorf("query BacktickBackslashN: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query BacktickBackslashN: %w", err) } - return item, nil + return result, nil } 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 { + var zero IllegalNameSymbolsRow + return zero, fmt.Errorf("query IllegalNameSymbols: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[IllegalNameSymbolsRow]) + if err != nil { + var zero IllegalNameSymbolsRow + return zero, fmt.Errorf("query IllegalNameSymbols: %w", err) } - return item, nil + return result, nil } 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 { + var zero string + return zero, fmt.Errorf("query SpaceAfter: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + if err != nil { + var zero string + return zero, fmt.Errorf("query SpaceAfter: %w", err) } - return item, nil + return result, nil } 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 { + var zero UnnamedEnum123 + return zero, fmt.Errorf("query BadEnumName: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[UnnamedEnum123]) + if err != nil { + var zero UnnamedEnum123 + return zero, fmt.Errorf("query BadEnumName: %w", err) } - return item, nil + return result, nil } 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 { + var zero string + return zero, 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} -} - -func (t textPreferrer) TypeName() string { - return t.typeName + 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 } - -// 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_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/example/void/query.sql.go b/example/void/query.sql.go index 62fb382e..05939016 100644 --- a/example/void/query.sql.go +++ b/example/void/query.sql.go @@ -5,11 +5,12 @@ 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" ) +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,78 @@ 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) - var item string - if err := row.Scan(nil, &item); err != nil { - return item, fmt.Errorf("query VoidTwo: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "VoidTwo") + rows, err := q.conn.Query(ctx, voidTwoSQL) + if err != nil { + var zero string + return zero, fmt.Errorf("query VoidTwo: %w", 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;` 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) - var item VoidThreeRow - if err := row.Scan(nil, &item.Foo, &item.Bar); err != nil { - return item, fmt.Errorf("query VoidThree: %w", err) + ctx = context.WithValue(ctx, QueryName{}, "VoidThree") + rows, err := q.conn.Query(ctx, voidThreeSQL) + if err != nil { + var zero VoidThreeRow + return zero, fmt.Errorf("query VoidThree: %w", 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();` // 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) + var zero []string + return zero, fmt.Errorf("query VoidThree2: %w", err) } - defer rows.Close() - items := []string{} - for rows.Next() { + + result, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (string, error) { var item string - if err := rows.Scan(&item, nil, nil); err != nil { - return nil, fmt.Errorf("scan VoidThree2 row: %w", err) + if err := row.Scan(&item, nil, nil); err != nil { + return item, err } - items = append(items, item) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close VoidThree2 rows: %w", err) + return item, nil + }) + if err != nil { + var zero []string + return zero, fmt.Errorf("scan VoidThree2 row: %w", err) } - return items, err + return result, 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 -} - -// 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/generate.go b/generate.go index 7d4190d6..0ffcecba 100644 --- a/generate.go +++ b/generate.go @@ -10,7 +10,7 @@ import ( "path/filepath" "time" - "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 3d2e1501..c0745071 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,7 @@ require ( github.com/docker/docker v28.0.1+incompatible github.com/docker/go-connections v0.5.0 github.com/google/go-cmp v0.7.0 - github.com/jackc/pgconn v1.14.3 - github.com/jackc/pgproto3/v2 v2.3.3 - github.com/jackc/pgtype v1.14.4 - github.com/jackc/pgx/v4 v4.18.3 + github.com/jackc/pgx/v5 v5.5.1 github.com/peterbourgon/ff/v3 v3.4.0 github.com/stretchr/testify v1.10.0 golang.org/x/mod v0.24.0 @@ -26,8 +23,6 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // 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-20240606120523-5a60cdf6a761 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -37,7 +32,6 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect diff --git a/go.sum b/go.sum index a8568f0f..4e40167b 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,13 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -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.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -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/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -28,98 +21,33 @@ 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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= -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.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= -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.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/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/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -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.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -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/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +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/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.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -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/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= @@ -132,45 +60,21 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -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.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= @@ -191,112 +95,41 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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-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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -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= @@ -310,15 +143,10 @@ google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -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= 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.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/internal/codegen/golang/declarer.go b/internal/codegen/golang/declarer.go index aa1347a1..61ca79fe 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,25 @@ 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, `"`, `""`) + `"` +} + // 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 +90,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 +123,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 +142,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 +154,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 +174,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 +213,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 ded7df64..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/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/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/gotype/known_types.go b/internal/codegen/golang/gotype/known_types.go index 4784c7b5..3322a6ba 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 @@ -93,73 +93,73 @@ var ( // //nolint:gochecknoglobals 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("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) + 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("*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("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("string", pg.Unknown) + PgCircle = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Circle", pg.Circle) + 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.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.Timestamptz", pg.TimestamptzArray) + PgInterval = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.Interval", pg.Interval) + 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/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("any", pg.Record) + PgUUID = MustParseKnownType("github.com/jackc/pgx/v5/pgtype.UUID", pg.UUID) + 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 @@ -180,7 +179,7 @@ var ( type knownGoType struct{ pgNative, nullable, nonNullable Type } //nolint:gochecknoglobals -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 26b659b8..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,8 +240,27 @@ 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/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/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 { 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 720befbb..0617b8c2 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,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. @@ -40,10 +41,10 @@ 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 -}} +{{- with emitDeclarers .Declarers $.PkgPath }}{{- "\n\n" -}}{{ . }}{{- end -}} {{- end -}} {{- range $i, $q := .Queries -}} @@ -54,72 +55,28 @@ 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 }}") -{{- 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) + 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 { + return {{ $q.EmitZeroResult }}, fmt.Errorf("exec query {{ $q.Name }}: %w", err) } - {{- $q.EmitResultAssigns "item" }} - return {{ $q.EmitResultExpr "item" }}, nil -{{- else if eq $q.ResultKind ":many" }} + return cmdTag, err +{{- else }} rows, 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" }}) + {{ $q.EmitZeroResultInit "zero" }} + return zero, fmt.Errorf("query {{ $q.Name }}: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close {{ $q.Name }} rows: %w", err) - } - return items, err -{{- else if eq $q.ResultKind ":exec" }} - cmdTag, err := q.conn.Exec(ctx, {{ $q.SQLVarName }} {{- $q.EmitParamNames }}) + + result, err := {{ $q.EmitCollectionFunc }}(rows, {{ $q.EmitRowMapper }}) if err != nil { - return cmdTag, fmt.Errorf("exec query {{ $q.Name }}: %w", err) + {{ $q.EmitZeroResultInit "zero" }} + return zero, fmt.Errorf("{{ $q.EmitCollectionErrContext }}: %w", err) } - return cmdTag, err -{{- 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} + return result, nil + {{- end }} } - -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/templated_file.go b/internal/codegen/golang/templated_file.go index cc6f9522..1ea064c4 100644 --- a/internal/codegen/golang/templated_file.go +++ b/internal/codegen/golang/templated_file.go @@ -41,7 +41,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 } @@ -152,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() } @@ -201,6 +176,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("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) + 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}") + 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) { @@ -213,7 +213,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 { @@ -224,7 +224,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.") @@ -237,7 +237,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.") @@ -257,31 +257,110 @@ func (tq TemplatedQuery) EmitRowScanArgs() (string, error) { return sb.String(), nil } +func (tq TemplatedQuery) EmitCollectionFunc() (string, error) { + switch tq.ResultKind { + case ast.ResultKindExec: + return "", fmt.Errorf("cannot EmitCollectionFunc for :exec query %s", tq.Name) + case ast.ResultKindMany: + return "pgx.CollectRows", nil + case ast.ResultKindOne: + return "pgx.CollectOneRow", nil + default: + return "", fmt.Errorf("unhandled EmitCollectionFunc type: %s", tq.ResultKind) + } +} + +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: + 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. +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) { - outs := removeVoidColumns(tq.Outputs) switch tq.ResultKind { case ast.ResultKindExec: return "pgconn.CommandTag", nil 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 "[]" + tq.EmitSingularResultType(), 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 tq.EmitSingularResultType(), nil default: return "", fmt.Errorf("unhandled EmitResultType kind: %s", tq.ResultKind) } @@ -326,108 +405,49 @@ 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 - } +// 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 sb.String(), nil + return "var " + name + " " + result, 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("}") - } +// 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 } + 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 + } + if _, ok := gotype.UnwrapNestedType(tq.Outputs[0].Type).(*gotype.EnumType); ok { + return `""`, nil + } + switch typ { + case "int", "int32", "int64", "float32", "float64", "uint", "uint32", "uint64": + return "0", nil + case "string": + return `""`, nil + case "bool": + return "false", nil + default: + return "*new(" + tq.Outputs[0].QualType + ")", nil + } + 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 @@ -489,16 +509,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) @@ -509,6 +528,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') } @@ -518,17 +539,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 3b9375fa..7cef961e 100644 --- a/internal/codegen/golang/templater.go +++ b/internal/codegen/golang/templater.go @@ -6,6 +6,7 @@ import ( "strings" "unicode" + "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" @@ -74,7 +75,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 } @@ -114,10 +115,9 @@ 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") } pkgPath := "" @@ -184,14 +184,23 @@ 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 + } + 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", - ResultKind: query.ResultKind, + ResultKind: resultKind, Doc: docs.String(), PreparedSQL: query.PreparedSQL, Inputs: inputs, - Outputs: outputs, + Outputs: nonVoidCols, + ScanCols: outputs, InlineParamCount: tm.inlineParamCount, }) } @@ -233,3 +242,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/codegen/golang/testdata/declarer_composite.input.golden b/internal/codegen/golang/testdata/declarer_composite.input.golden index 8cfddeb6..2b86b954 100644 --- a/internal/codegen/golang/testdata/declarer_composite.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite.input.golden @@ -1,110 +1,41 @@ // 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 - } - 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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -// 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 typesToRegister = []string{} -// 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)) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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 +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 8c697bf3..2b86b954 100644 --- a/internal/codegen/golang/testdata/declarer_composite.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite.output.golden @@ -1,95 +1,41 @@ // 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 - } - 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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} + +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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..2ca012c7 100644 --- a/internal/codegen/golang/testdata/declarer_composite_array.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite_array.input.golden @@ -1,130 +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 - } - 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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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{}}, - ) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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) -} +var _ = addTypeToRegister("\"some_table\"") -// 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 +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 9947597c..2ca012c7 100644 --- a/internal/codegen/golang/testdata/declarer_composite_array.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite_array.output.golden @@ -1,101 +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 - } - 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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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{}}, - ) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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_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 567b2626..f5430b1b 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,40 @@ 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 - } - 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 +// 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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()}, - ) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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)) -} +var _ = addTypeToRegister("\"device_type\"") -// 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 +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 a0daad58..f5430b1b 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,40 @@ 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 -} +var _ = addTypeToRegister("\"device_type\"") -// 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("\"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..4fc1b1d3 100644 --- a/internal/codegen/golang/testdata/declarer_composite_nested.input.golden +++ b/internal/codegen/golang/testdata/declarer_composite_nested.input.golden @@ -1,132 +1,48 @@ // 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 - } - 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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -// 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 typesToRegister = []string{} -// 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, - } +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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{}}, - ) -} +var _ = addTypeToRegister("\"foo_type\"") -// 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 +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 12719532..4fc1b1d3 100644 --- a/internal/codegen/golang/testdata/declarer_composite_nested.output.golden +++ b/internal/codegen/golang/testdata/declarer_composite_nested.output.golden @@ -1,109 +1,48 @@ // 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 - } - 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 + 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++ } - 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} + if loaded == 0 { + return fmt.Errorf("load PostgreSQL type %q: %w", lastType, lastErr) + } + pending = remaining } - return typ + return nil } -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 typesToRegister = []string{} -// 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{}}, - ) +func addTypeToRegister(typ string) struct{} { + typesToRegister = append(typesToRegister, typ) + return struct{}{} } -// 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("\"foo_type\"") + +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/codegen/golang/type_resolver_test.go b/internal/codegen/golang/type_resolver_test.go index e3ae8d1d..aadb6815 100644 --- a/internal/codegen/golang/type_resolver_test.go +++ b/internal/codegen/golang/type_resolver_test.go @@ -4,7 +4,7 @@ import ( "testing" "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" @@ -89,7 +89,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", @@ -101,7 +101,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 bf569cee..365c50b4 100644 --- a/internal/pg/column.go +++ b/internal/pg/column.go @@ -8,26 +8,25 @@ import ( "sync" "time" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" "github.com/jschaf/pggen/internal/texts" ) // 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 dde9a1da..9f789c55 100644 --- a/internal/pg/column_test.go +++ b/internal/pg/column_test.go @@ -7,8 +7,7 @@ import ( "time" "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" @@ -76,7 +75,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 @@ -87,7 +86,7 @@ func findTableOID(t *testing.T, conn *pgx.Conn, table string) pgtype.OID { ctx, cancel := context.WithTimeout(t.Context(), 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 && !errors.Is(err, pgx.ErrNoRows) { t.Fatal(err) } diff --git a/internal/pg/known_types.go b/internal/pg/known_types.go index b5122917..4a13ac06 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" ) @@ -84,7 +84,7 @@ var ( // All known Postgres types by OID. // //nolint:gochecknoglobals -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..8eaa9f19 100644 --- a/internal/pg/query.sql.go +++ b/internal/pg/query.sql.go @@ -5,11 +5,12 @@ 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" ) +type QueryName struct{} + // Querier is a typesafe Go interface backed by SQL queries. type Querier interface { FindEnumTypes(ctx context.Context, oids []uint32) ([]FindEnumTypesRow, error) @@ -23,11 +24,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 +36,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. @@ -48,36 +48,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 + 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 findEnumTypesSQL = `WITH enums AS ( @@ -125,35 +130,30 @@ 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" 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) + var zero []FindEnumTypesRow + return zero, 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) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindEnumTypesRow]) + if err != nil { + var zero []FindEnumTypesRow + return zero, fmt.Errorf("scan FindEnumTypes row: %w", err) } - return items, err + return result, nil } const findArrayTypesSQL = `SELECT @@ -186,32 +186,27 @@ 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" 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) + var zero []FindArrayTypesRow + return zero, fmt.Errorf("query FindArrayTypes: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindArrayTypes rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindArrayTypesRow]) + if err != nil { + var zero []FindArrayTypesRow + return zero, fmt.Errorf("scan FindArrayTypes row: %w", err) } - return items, err + return result, nil } const findCompositeTypesSQL = `WITH table_cols AS ( @@ -245,36 +240,31 @@ 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" 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) + var zero []FindCompositeTypesRow + return zero, 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) + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[FindCompositeTypesRow]) + if err != nil { + var zero []FindCompositeTypesRow + return zero, fmt.Errorf("scan FindCompositeTypes row: %w", err) } - return items, err + return result, nil } const findDescendantOIDsSQL = `WITH RECURSIVE oid_descs(oid) AS ( @@ -306,25 +296,20 @@ SELECT oid FROM oid_descs;` // FindDescendantOIDs implements Querier.FindDescendantOIDs. -func (q *DBQuerier) FindDescendantOIDs(ctx context.Context, oids []uint32) ([]pgtype.OID, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindDescendantOIDs") +func (q *DBQuerier) FindDescendantOIDs(ctx context.Context, oids []uint32) ([]uint32, error) { + 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 := []pgtype.OID{} - for rows.Next() { - var item pgtype.OID - if err := rows.Scan(&item); err != nil { - return nil, fmt.Errorf("scan FindDescendantOIDs row: %w", err) - } - items = append(items, item) + var zero []uint32 + return zero, fmt.Errorf("query FindDescendantOIDs: %w", err) } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("close FindDescendantOIDs rows: %w", err) + + result, err := pgx.CollectRows(rows, pgx.RowTo[uint32]) + if err != nil { + var zero []uint32 + return zero, fmt.Errorf("scan FindDescendantOIDs row: %w", err) } - return items, err + return result, nil } const findOIDByNameSQL = `SELECT oid @@ -334,14 +319,20 @@ ORDER BY oid DESC LIMIT 1;` // FindOIDByName implements Querier.FindOIDByName. -func (q *DBQuerier) FindOIDByName(ctx context.Context, name string) (pgtype.OID, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOIDByName") - row := q.conn.QueryRow(ctx, findOIDByNameSQL, name) - var item pgtype.OID - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query FindOIDByName: %w", err) +func (q *DBQuerier) FindOIDByName(ctx context.Context, name string) (uint32, error) { + ctx = context.WithValue(ctx, QueryName{}, "FindOIDByName") + rows, err := q.conn.Query(ctx, findOIDByNameSQL, name) + if err != nil { + var zero uint32 + return zero, fmt.Errorf("query FindOIDByName: %w", err) + } + + result, err := pgx.CollectOneRow(rows, pgx.RowTo[uint32]) + if err != nil { + var zero uint32 + return zero, fmt.Errorf("query FindOIDByName: %w", err) } - return item, nil + return result, nil } const findOIDNameSQL = `SELECT typname AS name @@ -349,14 +340,20 @@ FROM pg_type WHERE oid = $1;` // FindOIDName implements Querier.FindOIDName. -func (q *DBQuerier) FindOIDName(ctx context.Context, oid pgtype.OID) (pgtype.Name, error) { - ctx = context.WithValue(ctx, "pggen_query_name", "FindOIDName") - row := q.conn.QueryRow(ctx, findOIDNameSQL, oid) - var item pgtype.Name - if err := row.Scan(&item); err != nil { - return item, fmt.Errorf("query FindOIDName: %w", err) +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 { + var zero string + return zero, fmt.Errorf("query FindOIDName: %w", err) } - return item, nil + + 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 @@ -364,54 +361,24 @@ 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" 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) + var zero []FindOIDNamesRow + return zero, fmt.Errorf("query FindOIDNames: %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 + 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 } - -// 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 5d19493f..c3561dc6 100644 --- a/internal/pg/type_cache.go +++ b/internal/pg/type_cache.go @@ -1,19 +1,15 @@ package pg -import ( - "sync" - - "github.com/jackc/pgtype" -) +import "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 } @@ -24,16 +20,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[oid]; ok { + cachedTypes[oid] = t } else { - uncachedTypes[pgtype.OID(oid)] = struct{}{} + uncachedTypes[oid] = struct{}{} } } return cachedTypes, uncachedTypes @@ -41,7 +37,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[oid] tc.mu.Unlock() return typ, ok } diff --git a/internal/pg/type_fetcher.go b/internal/pg/type_fetcher.go index b5bf621b..00cb0ab5 100644 --- a/internal/pg/type_fetcher.go +++ b/internal/pg/type_fetcher.go @@ -5,8 +5,7 @@ import ( "fmt" "time" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" ) // TypeFetcher fetches Postgres types by the OID. @@ -25,7 +24,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 } @@ -41,9 +40,7 @@ func (tf *TypeFetcher) FindTypesByOIDs(oids ...uint32) (map[pgtype.OID]Type, err 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) @@ -97,7 +94,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 { @@ -105,9 +102,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, @@ -120,14 +117,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{}{} } @@ -149,13 +146,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 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, } @@ -165,7 +162,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 { @@ -175,14 +172,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 { @@ -190,7 +187,7 @@ func (tf *TypeFetcher) findArrayTypes(ctx context.Context, uncached map[pgtype.O } 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) } @@ -205,7 +202,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) { @@ -247,10 +244,10 @@ 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)) + oids = append(oids, oid) } return oids } diff --git a/internal/pg/type_fetcher_test.go b/internal/pg/type_fetcher_test.go index 16f2b39b..5f1900b4 100644 --- a/internal/pg/type_fetcher_test.go +++ b/internal/pg/type_fetcher_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -196,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) } @@ -249,7 +248,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(t.Context(), rawOID) @@ -257,10 +256,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 0bc8c12b..c5d36bb5 100644 --- a/internal/pg/types.go +++ b/internal/pg/types.go @@ -3,14 +3,13 @@ package pg import ( "strconv" - "github.com/jackc/pgtype" "github.com/jschaf/pggen/internal/pg/pgoid" ) // 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 } @@ -52,8 +51,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 @@ -63,7 +62,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 @@ -72,7 +71,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. @@ -85,26 +84,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 @@ -112,8 +111,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 } @@ -123,38 +122,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/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/pgdocker/pgdocker.go b/internal/pgdocker/pgdocker.go index 59c47a14..962edb01 100644 --- a/internal/pgdocker/pgdocker.go +++ b/internal/pgdocker/pgdocker.go @@ -21,7 +21,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" ) diff --git a/internal/pginfer/pginfer.go b/internal/pginfer/pginfer.go index 315a2b4e..35f08758 100644 --- a/internal/pginfer/pginfer.go +++ b/internal/pginfer/pginfer.go @@ -7,11 +7,8 @@ import ( "strings" "time" - "github.com/jackc/pgconn" - - "github.com/jackc/pgproto3/v2" - "github.com/jackc/pgtype" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "github.com/jschaf/pggen/internal/ast" "github.com/jschaf/pggen/internal/pg" ) @@ -160,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[pgtype.OID(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) } @@ -190,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[pgtype.OID(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], }) @@ -205,7 +202,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 } @@ -218,7 +215,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: desc.TableOID, Number: desc.TableAttributeNumber, } } diff --git a/internal/pgplan/pgplan.go b/internal/pgplan/pgplan.go index 9f95e25f..7bbecbf5 100644 --- a/internal/pgplan/pgplan.go +++ b/internal/pgplan/pgplan.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" ) // ExplainQuery executes an explain query and parses the plan. diff --git a/internal/pgtest/pg_test_db.go b/internal/pgtest/pg_test_db.go index 1ee8388c..08703035 100644 --- a/internal/pgtest/pg_test_db.go +++ b/internal/pgtest/pg_test_db.go @@ -3,13 +3,14 @@ package pgtest import ( "context" "math/rand/v2" + "net/url" "os" "strconv" "strings" "testing" "time" - "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v5" ) // CleanupFunc deletes the schema and all database objects. @@ -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) @@ -52,7 +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) } - cleanup := func() { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() @@ -69,6 +69,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) {