Giter Club home page Giter Club logo

pggen's Introduction

Test Lint GoReportCard

pggen - generate type safe Go methods from Postgres SQL queries

pggen generates Go code to provide a typesafe wrapper to run Postgres queries. If Postgres can run the query, pggen can generate code for it. The generated code is strongly-typed with rich mappings between Postgres types and Go types without relying on interface{}. pggen uses prepared queries, so you don't have to worry about SQL injection attacks.

How to use pggen in three steps:

  1. Write arbitrarily complex SQL queries with a name and a :one, :many, or :exec annotation. Declare inputs with pggen.arg('input_name').

    -- name: SearchScreenshots :many
    SELECT ss.id, array_agg(bl) AS blocks
    FROM screenshots ss
      JOIN blocks bl ON bl.screenshot_id = ss.id
    WHERE bl.body LIKE pggen.arg('body') || '%'
    GROUP BY ss.id
    ORDER BY ss.id
    LIMIT pggen.arg('limit') OFFSET pggen.arg('offset');
  2. Run pggen to generate Go code to create type-safe methods for each query.

    pggen gen go \
        --schema-glob schema.sql \
        --query-glob 'screenshots/*.sql' \
        --go-type 'int8=int' \
        --go-type 'text=string'

    That command generates methods and type definitions like below. The full example is in ./example/composite/query.sql.go.

    type SearchScreenshotsParams struct {
        Body   string
        Limit  int
        Offset int
    }
    
    type SearchScreenshotsRow struct {
        ID     int      `json:"id"`
        Blocks []Blocks `json:"blocks"`
    }
    
    // Blocks represents the Postgres composite type "blocks".
    type Blocks struct {
        ID           int    `json:"id"`
        ScreenshotID int    `json:"screenshot_id"`
        Body         string `json:"body"`
    }
    
    func (q *DBQuerier) SearchScreenshots(
        ctx context.Context,
        params SearchScreenshotsParams,
    ) ([]SearchScreenshotsRow, error) {
        /* omitted */
    }
  3. Use the generated code.

    var conn *pgx.Conn
    q := NewQuerier(conn)
    rows, err := q.SearchScreenshots(ctx, SearchScreenshotsParams{
        Body:   "some_prefix",
        Limit:  50,
        Offset: 200,
    })

Pitch

Why should you use pggen instead of the myriad of Go SQL bindings?

  • pggen generates code by introspecting the database system catalogs, so you can use any database extensions or custom methods, and it will just work. For database types that pggen doesn't recognize, you can provide your own type mappings.

  • pggen scales to Postgres databases of any size and supports incremental adoption. pggen is narrowly tailored to only generate code for queries you write in SQL. pggen will not create a model for every database object. Instead, pggen only generates structs necessary to run the queries you specify.

  • pggen works with any Postgres database with any extensions. Under the hood, pggen runs each query and uses the Postgres catalog tables, pg_type, pg_class, and pg_attribute, to get perfect type information for both the query parameters and result columns.

  • pggen works with all Postgres queries. If Postgres can run the query, pggen can generate Go code for the query.

  • 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.

Anti-pitch

I'd like to try to convince you why you shouldn't use pggen. Often, this is far more revealing than the pitch.

  • You want auto-generated models for every table in your database. pggen only generates code for each query in a query file. pggen requires custom SQL for even the simplest CRUD queries. Use gorm or any of alternatives listed at awesome Go ORMs.

  • You use a database other than Postgres. pggen only supports Postgres. sqlc, a similar tool which inspired pggen, has early support for MySQL.

  • You want an active-record pattern where models have methods like find, create, update, and delete. pggen only generates code for queries you write. Use gorm.

  • You prefer building queries in a Go dialect instead of SQL. I'd recommend investing in really learning SQL; it will payoff. Otherwise, use squirrel, goqu, or go-sqlbuilder

  • You don't want to add a Postgres or Docker dependency to your build phase. Use sqlc, though you might still need Docker. sqlc generates code by parsing the schema file and queries in Go without using Postgres.

Install

Download precompiled binaries

Precompiled binaries from the latest release. Change ~/bin if you want to install to a different directory. All assets are listed on the releases page.

  • MacOS Apple Silicon (arm64)

    mkdir -p ~/bin \
      && curl --silent --show-error --location --fail 'https://github.com/jschaf/pggen/releases/latest/download/pggen-darwin-arm64.tar.xz' \
      | tar -xJf - -C ~/bin/    
  • MacOS Intel (amd64)

    mkdir -p ~/bin \
      && curl --silent --show-error --location --fail 'https://github.com/jschaf/pggen/releases/latest/download/pggen-darwin-amd64.tar.xz' \
      | tar -xJf - -C ~/bin/    
  • Linux (amd64)

    mkdir -p ~/bin \
      && curl --silent --show-error --location --fail 'https://github.com/jschaf/pggen/releases/latest/download/pggen-linux-amd64.tar.xz' \
      | tar -xJf - -C ~/bin/    
  • Windows (amd64)

    mkdir -p ~/bin \
      && curl --silent --show-error --location --fail 'https://github.com/jschaf/pggen/releases/latest/download/pggen-windows-amd64.tar.xz' \
      | tar -xJf - -C ~/bin/    

Make sure pggen works:

pggen gen go --help

Install from source

Requires Go 1.16 because pggen uses go:embed. Installs to $GOPATH/bin.

go install github.com/jschaf/pggen/cmd/pggen@latest

Make sure pggen works:

pggen gen go --help

Usage

Generate code using Docker to create the Postgres database from a schema file:

# --schema-glob runs all matching files on Dockerized Postgres during database 
# creation.
pggen gen go \
    --schema-glob author/schema.sql \
    --query-glob author/query.sql

# Output: author/query.go.sql

# Or with multiple schema files. The schema files run on Postgres
# in the order they appear on the command line.
pggen gen go \
    --schema-glob author/schema.sql \
    --schema-glob book/schema.sql \
    --schema-glob publisher/schema.sql \
    --query-glob author/query.sql

# Output: author/query.sql.go

Generate code using an existing Postgres database (useful for custom setups):

pggen gen go \
    --query-glob author/query.sql \
    --postgres-connection "user=postgres port=5555 dbname=pggen"

# Output: author/query.sql.go

Generate code for multiple query files. All the query files must reside in the same directory. If query files reside in different directories, you can use --output-dir to set a single output directory:

pggen gen go \
    --schema-glob schema.sql \
    --query-glob author/fiction.sql \
    --query-glob author/nonfiction.sql \
    --query-glob author/bestselling.sql

# Output: author/fiction.sql.go
#         author/nonfiction.sql.go
#         author/bestselling.sql.go

# Or, using a glob. Notice quotes around glob pattern to prevent shell 
# expansion.
pggen gen go \
    --schema-glob schema.sql \
    --query-glob 'author/*.sql'

Examples

Examples embedded in the repo:

Features

  • JSON struct tags: All <query_name>Row structs include JSON struct tags using the Postgres column name. To change the struct tag, use an SQL column alias.

    -- name: FindAuthors :many
    SELECT first_name, last_name as family_name FROM author;

    Generates:

    type FindAuthorsRow struct {
        FirstName   string `json:"first_name"`
        FamilyName  string `json:"family_name"`
    }
  • Acronyms: Custom acronym support so that author_id renders as AuthorID instead of AuthorId. Supports two formats:

    1. Long form: --acronym <word>=<relacement>: replaces <word> with <replacement> literally. Useful for plural acronyms like author_ids which should render as AuthorIDs, not AuthorIds. For the IDs example, use --acronym ids=IDs.

    2. Short form: --acronym <word>: replaces <word> with uppercase <WORD>. Equivalent to --acronym <word>=<WORD>

    By default, pggen includes --acronym id to render id as ID.

  • Enums: Postgres enums map to Go string constant enums. The Postgres type:

    CREATE TYPE device_type AS ENUM ('undefined', 'phone', 'ipad');

    pggen generates the following Go code when used in a query:

    // DeviceType represents the Postgres enum device_type.
    type DeviceType string
    
    const (
        DeviceTypeUndefined DeviceType = "undefined"
        DeviceTypePhone     DeviceType = "phone"
        DeviceTypeIpad      DeviceType = "ipad"
    )
    
    func (d DeviceType) String() string { return string(d) }
  • Custom types: Use a custom Go type to represent a Postgres type with the --go-type flag. The format is <pg_type>=<qualified_go_type>. For example:

    pggen gen go \
        --schema-glob example/custom_types/schema.sql \
        --query-glob example/custom_types/query.sql \
        --go-type 'int8=*int' \
        --go-type 'int4=int' \
        --go-type '_int4=[]int' \
        --go-type 'text=*github.com/jschaf/pggen/mytype.String' \
        --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:

    • 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 example/custom_types test for an example.

      ci := conn.ConnInfo()
      
      ci.RegisterDataType(pgtype.DataType{
      	Value: new(pgtype.Int2),
      	Name:  "my_int",
      	OID:   myIntOID,
      })
    • The Go type implements sql.Scanner.

    • pgx is able to use reflection to build an object to write fields into.

  • Nested structs (composite types): pggen creates child structs to represent Postgres composite types that appear in output columns.

    -- name: FindCompositeUser :one
    SELECT ROW (15, 'qux')::"user" AS "user";

    pggen generates the following Go code:

    // User represents the Postgres composite type "user".
    type User struct {
        ID   pgtype.Int8
        Name pgtype.Text
    }
    
    func (q *DBQuerier) FindCompositeUser(ctx context.Context) (User, error) {}

IDE integration

If your IDE provides SQL autocomplete, you may want to get rid of its warnings by declaring the following DDL schema.

-- Exists solely so editors don't underline every pggen.arg() expression in
-- squiggly red.
CREATE SCHEMA pggen;

-- pggen.arg defines a named parameter that's eventually compiled into a
-- placeholder for a prepared query: $1, $2, etc.
CREATE FUNCTION pggen.arg(param TEXT) RETURNS text AS $$SELECT null$$ LANGUAGE sql;

Tutorial

Let's say we have a database with the following schema in author/schema.sql:

CREATE TABLE author (
  author_id  serial PRIMARY KEY,
  first_name text NOT NULL,
  last_name  text NOT NULL,
  suffix     text NULL
)

First, write a query in the file author/query.sql. The query name is FindAuthors and the query returns :many rows. A query can return :many rows, :one row, or :exec for update, insert, and delete queries.

-- FindAuthors finds authors by first name.
-- name: FindAuthors :many
SELECT * FROM author WHERE first_name = pggen.arg('first_name');

Second, use pggen to generate Go code to author/query.sql.go:

pggen gen go \
    --schema-glob author/schema.sql \
    --query-glob author/query.sql

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.

    // 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 <query_name>Batch methods, send the batch, and finally get the results with the <query_name>Scan methods. See example/author/query.sql_test.go for complete example.

    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.

    type DBQuerier struct {
        conn genericConn
    }
  • Create DBQuerier with NewQuerier. The genericConn parameter is an interface over the different pgx connection transports so that DBQuerier doesn't force you to use a specific connection transport. *pgx.Conn, pgx.Tx, and *pgxpool.Pool all implement genericConn.

    // NewQuerier creates a DBQuerier that implements Querier. conn is typically
    // *pgx.Conn, pgx.Tx, or *pgxpool.Pool.
    func NewQuerier(conn genericConn) *DBQuerier {
        return &DBQuerier{
            conn: conn,
        }
    }
  • pggen embeds the SQL query formatted for a Postgres PREPARE statement with parameters indicated by $1, $2, etc. instead of pggen.arg('first_name').

    const findAuthorsSQL = `SELECT * FROM author WHERE first_name = $1;`
  • pggen generates a row struct for each query named <query_name>Row. pggen transforms the output column names into struct field names from lower_snake_case to UpperCamelCase in internal/casing/casing.go. pggen derives JSON struct tags from the Postgres column names. To change the JSON struct name, change the column name in the query.

    type FindAuthorsRow struct {
        AuthorID  int32       `json:"author_id"`
        FirstName string      `json:"first_name"`
        LastName  string      `json:"last_name"`
        Suffix    pgtype.Text `json:"suffix"`
    }

    As a convenience, if a query only generates a single column, pggen skips creating the <query_name>Row struct and returns the type directly. For example, the generated query for SELECT author_id from author returns int32, not a <query_name>Row struct.

    pggen infers struct field types by preparing the query. When Postgres prepares a query, Postgres returns the parameter and column types as OIDs. pggen finds the type name from the returned OIDs in internal/codegen/golang/gotype/types.go.

    Choosing an appropriate type is more difficult than might seem at first glance due to null. When Postgres reports that a column has a type text, 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.

  • Lastly, pggen generates the implementation for each query.

    As a convenience, if a there are only one or two query parameters, pggen inlines the parameters into the method definition, as with firstName below. If there are three or more parameters, pggen creates a struct named <query_name>Params to pass the parameters to the query method.

    // FindAuthors implements Querier.FindAuthors.
    func (q *DBQuerier) FindAuthors(ctx context.Context, firstName string) ([]FindAuthorsRow, error) {
        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)
        }
        if err := rows.Err(); err != nil {
            return nil, err
        }
        return items, err
    }

Contributing

See CONTRIBUTING.md and ARCHITECTURE.md.

Acknowledgments

pggen was directly inspired by sqlc. The primary difference between pggen and sqlc is how each tool infers the type and nullability of the input parameters and output columns for SQL queries.

sqlc parses the queries in Go code, using Cgo to call the Postgres parser.c library. After parsing, sqlc infers the types of the query parameters and result columns using custom logic in Go. In contrast, pggen gets the same type information by running the queries on Postgres and then fetching the type information for Postgres catalog tables.

Use sqlc if you don't wish to run Postgres to generate code or if you need better nullability analysis than pggen provides.

Use pggen if you can run Postgres for code generation, and you use complex queries that sqlc is unable to parse. Additionally, use pggen if you have a custom database setup that's difficult to replicate in a schema file. pggen supports running on any database with any extensions.

pggen's People

Contributors

0xjac avatar amari avatar benjamin-thomas avatar bweston92 avatar darkxanter avatar farazfazli avatar jschaf avatar ngld avatar td0m avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pggen's Issues

confusing installation procedure.

First of all thank your for this super interesting project. I am using pgx on a project of mine and i found pggen to be a good match for generating go codes.
Now I suppose using the command given on documentation is supposed to install pggen binary on GOPATH or am i missing something? On my end the command does not install pggen binary on GOPATH thus make in totally inaccessible.
My GO version is 1.16

Optional arguments in insert query

Given the following schema

CREATE TABLE actor  (
	id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    type TEXT NOT NULL,
    name TEXT
);

and the following insert query

-- name: Insert :exec
INSERT INTO actor (id, tenant_id, created, type, name)
    VALUES (pggen.arg('id'), pggen.arg('tenant_id'), NOW(), pggen.arg('type'), pggen.arg('name'));

The insert parms struct that is generated looks like this

type InsertParams struct {
	ID          pgtype.UUID
	TenantID    pgtype.UUID
	Type        string
	Name        string
}

I would however expect it to look like this

type InsertParams struct {
	ID          pgtype.UUID
	TenantID    pgtype.UUID
	Type          string
	Name        *string
}

This is because in the schema name is nullable and thus when I am inserting if I don't pass a pointer null should be supplied to the query.

Is it possible to overcome this issue? I couldn't see anything in the docs so sorry if I missed something.

Proposal: Custom go type mapping per query argument

Based on the side note in #43, detailed in a separate issue here as requested per @jschaf

Currently, pggen allows to map an SQL type to a Go type using the --go-type <sql-type>=<go-type> argument (repeated for each SQL type which we want to map). This implies that an SQL type is only ever mapped to one specific Go type for all arguments of all queries.

However, it might be useful to map a SQL type to different Go types based on context. This can apply either to parameters across queries or within queries, as well as the return value. A strong argument in favor of different mappings is the fact that PostgreSQL does not distinguish between a type which does not contain NULLs and a NULL-able type.

As a simple example, consider a schema to track flights, we are interested in the flight number, departure time, ETA, and arrival time.
The SQL table for the data could look like:

CREATE TABLE flights (
    flight_number INTEGER PRIMARY KEY,
    departure TIMESTAMP WITH TIME ZONE NOT NULL,
    eta TIMESTAMP WITH TIME ZONE,
    arrival TIMESTAMP WITH TIME ZONE
)

We see the issue here, the departure/arrival times and ETA are all of type timestamptz. However, while the departure time is never NULL, the ETA and arrival time may be NULL. (This makes sense since the departure time is scheduled and known in advance, while the arrival time will only be known upon landing and the ETA is an estimation which may not be present, for example if the flight is delayed or canceled.)

Let's consider some queries for flights:

-- name: InsertFlight :exec
INSERT INTO flights (flight_number, departure, eta)
    VALUES (pggen.arg('flightNumber'), pggen.arg('departure'), pggen.arg('eta'));

-- GetTodaysFlights :many
SELECT f.flight_number, f.departure, f.arrival, f.eta
FROM flights f
WHERE CURRENT_DATE <= f.departure AND f.departure < CURRENT_DATE + 1
ORDER BY f.departure DESC;

-- GetTodaysDepartures :many
SELECT f.departure
FROM flights f
WHERE CURRENT_DATE <= f.departure AND f.departure < CURRENT_DATE + 1
ORDER BY f.departure DESC;

For those queries, all timezone arguments can be mapped to *time.Time but not to time.Time as this would fail for GetTodaysFlights for flights without an arrival time and/or an ETA. Having *time.Time implies that the departure parameter in InsertFlight will be a pointer which is not needed, and will fail at runtime if a nil pointer is given.

It would be much better to individually map parameters and return values to the desired go types. This mapping would take precedence over the global mapping (via --go-type) and of course if not specifed, the then global mapping would be used.

This individual mapping could be specified in the query comment, reusing the existing syntax used to specify the function name and return cardinality. Assuming the following global mapping: --go-type "timestamptz=*time.Time" --go-type "integer=int", the queries can be defined as:

-- name: InsertFlight :exec
-- arg: flightNumber int
-- arg: departure time.Time
INSERT INTO flights (flight_number, departure, eta)
    VALUES (pggen.arg('flightNumber'), pggen.arg('departure'), pggen.arg('eta'));

-- GetTodaysFlights :many
-- return: departure time.Time
SELECT f.flight_number, f.departure, f.arrival, f.eta
FROM flights f
WHERE CURRENT_DATE <= f.departure AND f.departure < CURRENT_DATE + 1
ORDER BY f.departure DESC;

-- GetTodaysDepartures :many
-- return: departure time.Time
SELECT f.departure
FROM flights f
WHERE CURRENT_DATE <= f.departure AND f.departure < CURRENT_DATE + 1
ORDER BY f.departure DESC;

The format here is:

  • argument: --arg: <arg-name> <go-type>
  • return column --return: <column-name> <go-type>

Note that:

  1. The value of <go-type> should be identical to that in the global mapping: --go-type <sql-type>=<go-type>.
  2. Even if the return is single column, the column name is still specified, but the return value does not need to be a struct with a single column, it can jut be a slice (with :many) or an instance (with :one) of the column type. The custom type specified is always for the column (of a single row), not for many rows, in this case it should be a slice of the type.

So the Go functions should look like:

func InsertFlight(flightNumber int, departure time.Time, eta *time.Time) error

type GetTodaysFlightsRow struct {
   FlightNumber int    `json:"flight_number"`
   Departure time.Time `json:"departure"`
   ETA *time.Time      `json:"eta"`
   Arrival *time.Time  `json:"arrival"`
}

func GetTodaysFlights() ([]GetTodaysDeparturesRow, error)

func GetTodaysDepartures() ([]time.Time, error)

Hopefully it should be straight forward enough to implement and avoid any corner cases I can think of.

void return value not recognized

ERROR: generate go code: template all: template query file xxx no go type found for Postgres type void oid=2278

I try to call a function without a return value

-- name: Something :exec
select * something(pggen.arg('arg1'), pggen.arg('arg2'), pggen.arg('arg3'));

-- or

-- name: Something :exec
select something(pggen.arg('arg1'), pggen.arg('arg2'), pggen.arg('arg3'));

workaround:

-- name: Something :exec
select true from something(pggen.arg('arg1'), pggen.arg('arg2'), pggen.arg('arg3'));

Support for enums(?)

Schema:

CREATE TYPE device_type AS ENUM ('undefined', 'phone', 'laptop', 'ipad', 'desktop', 'iot');

CREATE TABLE IF NOT EXISTS device (
  mac MACADDR PRIMARY KEY,
  type device_type NOT NULL DEFAULT 'undefined'
);

Queries:

-- name: List :many
SELECT *
FROM device;

Error:

infer typed named query List: infer output types for query: fetch oid types: did not find all OIDs; missing OID 16393

Will pggen support enums? If possible, it might be nice to generate go enums too, here is how sqlc does it.

cannot unmarshal number into Go value of type map[string]interface {}

I am just trying to get through a basic tutorial. I installed through go install:
go install github.com/jschaf/pggen/cmd/pggen@latest

Then tried to run the following command against my postgres database:
pggen gen go \ --schema-glob author/schema.sql \ --query-glob author/query.sql

it creates the author schema in the database correctly, but I get the following error while trying to generate code for query.sql:
ERROR: parse template query file "C:\\code\\api-brand-allocation\\author\\query.sql": infer typed named query FindAuthorByID: infer output types for query: infer output type nullability: explain prepared query: can't scan into dest[0]: json: cannot unmarshal number into Go value of type map[string]interface {}
I pulled my query.sql and schema.sql from the following location:
https://github.com/jschaf/pggen/tree/main/example/author

Can't figure out what I missed, hopefully something simple. Thanks!

proposal: Make generation of Batch and Scan optional

I think generation of Batch and Scan for every query is unnecessary. For most cases they are unused and untested codes since we don't need those in our business logic. I propose turning off Batch and Scan by default and enable them for certain queries using a flag on sql files. For example:

the following query with this flags should not generate Batch and Scan

    -- name: InsertAuthor :one
    INSERT INTO author (first_name, last_name)
    VALUES (pggen.arg('FirstName'), pggen.arg('LastName'))
    RETURNING author_id;

the following query with this flags should generate Batch and Scan

    -- name: InsertAuthor :one --include-batch
    INSERT INTO author (first_name, last_name)
    VALUES (pggen.arg('FirstName'), pggen.arg('LastName'))
    RETURNING author_id;

Flag for package name missing

Is there a way to configure the package in the generated file?

Looking here the mapping from flags to pggen.GenerateOptions it seems we are missing a flag for GoPackage? I was expecting to be able to use a --go-package flag to set a custom package name.

array_agg(table) returning a []Table?

hi thanks for the cool tool!

given a schema

create table if not exists screenshots (
    id bigint primary key               
);                                      

create table if not exists blocks (                            
    id serial primary key,                                     
    screenshot_id bigint not null references screenshots (id), 
    body text not null                                         
);                                                             

and query

-- name: SearchScreenshots :many                        
select                                                  
    screenshots.*,                                      
    array_agg(blocks) blocks                  
from                                                    
    screenshots                                         
    join blocks on blocks.screenshot_id = screenshots.id
where                                                   
    pggen.arg('Body') % blocks.body                    
group by                                                
    screenshots.id                                      
limit pggen.arg('Limit')
offset pggen.arg('Offset');  

('str' % column being a pg_trgm thing)

pggen generates

type SearchScreenshotsParams struct {                                                                            
    Body   string                                                                            
    Limit  int                                                                            
    Offset int                                                                            
}                                                                            
                                                                            
type SearchScreenshotsRow struct {                                                                            
    ID             pgtype.Int8      `json:"id"`                                                                                                                        
    Blocks         []Blocks         `json:"blocks"`                                                                            
}                                                                            
                                                                            
// SearchScreenshots implements Querier.SearchScreenshots.                                                                      
func (q *DBQuerier) SearchScreenshots(ctx context.Context, params SearchScreenshotsParams) ([]SearchScreenshotsRow, error) {    
    // some stuff
    return items, err
}                                                              

however the Blocks in SearchScreenshotsRow's []Blocks is undefined

so my question is,
is it somehow possible, or in the scope of this project, that pggen could generate something like

 Blocks         []Block         `json:"blocks"`

where Block is a struct with fields matching the blocks table?

also postgres shows the type of the array_agg(blocks) column as block[]
image

any help is much appreciated, thanks!

How to enable statement caching?

This is not a issue but more of a question, how to utilize the pgx statement caching mechanism with pgxpool ? Because all SQL statements are predefined, it will be very efficient to cache all available queries on start of new connection.
There are some details here: jackc/pgx#791

Any idea how to utilize it with pggen?

Better IDE integration?

Hello, I'm testing out your library.

I'm wondering if you have any advice or opinion on better using IDEs with it.

Jetbrains IDEs have a wonderful SQL auto complete (from dynamically inspecting the schema).

However, the pggen namespace is unknown is so we get useless error messages.

image

Injecting a bogus function in the dev environment does the trick.

-- Improve IDE integration
CREATE SCHEMA pggen;
CREATE FUNCTION pggen.arg(str VARCHAR) RETURNS INTEGER AS
$$
BEGIN
    RETURN 1;
END;
$$ language plpgsql;

image

One can always clean this up if need be:

-- Remove IDE integration improvements
DROP FUNCTION pggen.arg(str VARCHAR);
DROP SCHEMA pggen;

I'm thinking this little hack could be mentioned in the README. Or maybe it'd be ok to let the tooling handle this, by adding a switch, something like pggen gen --add-ide-integration.

What do you think?

Discussion about -go-type

I noticed that the --pg-type maps a pg type to a destination go type. The go type have to implement the ValueTranscoder interface, which are quiet a lot functions.
Furthermore, a transcoder type cannot be reused.

Often (including pggen itself) has following mechanics:
pg type <-> transcoder type <-> destination go type

Screenshot_20210423_133235
Screenshot_20210423_133308
note: this is a view of the pgx.ConnInfo state with it's default data set + 2 registerd Composit Types
note: the light green "yes" is the default one - the dark green "yes" is implemented but not the default

What do you think to optionally define separate pgx type / destination types?

-go-type=pgtype=valuetype=assigntotype

Using output dir with multiple query files with same basename overwrites files

pggen release: 2021-04-18

Code generation failed for following example configuration:

 pggen gen go \
	 --schema-glob '${schema_path}/*.up.sql' \
	 --query-glob '${query_path}/A/query.sql' \
	 --query-glob '${query_path}/B/query.sql' \
	 --query-glob '${query_path}/C/query.sql' \
	 --query-glob '${query_path}/D/query.sql' \
	 --query-glob '${query_path}/E/query.sql' \
	 --query-glob '${query_path}/F/query.sql' \
	 --go-type 'int8=int8' \
	 --go-type 'int=int' \
	 --go-type 'varchar=string' \
	 --postgres-connection '${db_url}' \
	 --output-dir 'db'  

result : generated 6 query files

Although pggen is generating Codes for only F query directory and entire DBQuerier and Querier interface is missing from the output directory. I am using golang migrate for maintaining database migrations so my db schema are in A.up.sql format in schema directory

update: renaming query.sql files generates all codes. it seems pggen is overwriting generated codes because of same file name

Support for custom tags in struct

I've been using sqlc for quite sometime. But I want to migrate to pggen since pgx library is faster than libpq. But is it possible to implement something similar to this : sqlc-dev/sqlc#534

I want to add custom tags like "json" or "validate" in the generated struct. In that way, we can reuse struct generated by pggen when binding json to struct and marshaling struct to json. Below is an example use case:

CREATE TABLE user (
  id   BIGSERIAL PRIMARY KEY,

  -- tags:`validate:"min=10,max=0"`
  username text NOT NULL,
  
  --tags: `json:"-"`
  password text NOT NULL 

  -- tags:`validate:"gt=18"`
  age  integer NOT NULL,

); 

So in the above example we can use the struct generated by pggen to bind json directly to this struct and use Validate module to validate, Also use the same struct to send JSON reponse to user without password field since we are using custom tags for JSON. Unlike sqlc, pggen uses actual database server to get information about tables. Is this possible to implement this feature? Like using --schema-glob file or some custom config files ?

Encoding of Numeric inside a custom type panics

I receive a panic when I try to insert entries with custom types containing Numeric fields. The relevant schema looks like this:

CREATE TYPE product_dimension AS (
  type TEXT,
  unit TEXT,
  value NUMERIC(8,2)
);

CREATE TABLE product (
  [...]
  dimensions product_dimension[],
  [...]
);

I get the following error message If I try to insert a row with value = 55.44:

panic encode []ProductDimension: cannot convert {5044 -2 2 false} to Numeric

I guess he is trying to convert a Numeric struct to a Numeric struct, but I might be wrong. This is the generated code that causes the panic:

func (tr *typeResolver) newProductDimensionArrayInit(ps []ProductDimension) pgtype.ValueTranscoder {
	dec := tr.newProductDimensionArray()
	if err := dec.Set(tr.newProductDimensionArrayRaw(ps)); err != nil {
		panic("encode []ProductDimension: " + err.Error()) // should always succeed
	}
	return textPreferrer{ValueTranscoder: dec, typeName: "_product_dimension"}
}

Considering that the comment says should always succeed I'm inclined to think that this might be a bug.

Would it be possible to make Postgres `bigint` map to Go `int64` by default?

Currently, it maps to int, which may not actually be 64 bits.

A workaround is for me to pass in --go-type bigint=*int64, but this converts all the non-null bigint columns into *int64s as well instead of int64. If I pass in --go-type bigint=int64, then I lose nullability altogether.

EDIT: Is it intended behavior that trying to map a bigint to an int64 means that all bigint columns (nullable and not-null) become int64 instead of nullable ones becoming *int64 and non-null ones becoming int64?

Interval processing causes panics

Hello, i'm getting the following when trying to pass interval to postgresql stored function:

panic: encode []ProductIn: cannot convert {31207680000000 0 0 2} to Interval

I tried to debug and found that error happens here: https://github.com/jackc/pgtype/blob/master/interval.go#L45
Object that is passed there is of type pgtype.Interval, which can't be processed properly as you can see.
Could you please suggest how i can fix the issue and i will be happy to issue PR

a question about dynamic queries

hello again ๐Ÿ‘‹

in my application a feature would be to allow the user to sort the results of a search by some or multiple columns. eg

create table screenshots (
   id        serial      primary key,
   timestamp timestamptz not null,
   width     int         not null
);

the user may like to search screenshots by timestamp ascending, width descending, or whatever other column

as far as I know with postgres, it is not possible to make a prepared query for the order by clause
eg ORDER BY $1
https://www.postgresql-archive.org/ORDER-BY-in-prepared-statements-td5834944.html

and so since we have no idea of a query builder etc here, one idea might be to do

-- name: SearchScreenshots :many
select
  *
from
  screenshots
order by
  case
    when pggen.arg('timestamp_asc')  then timestamp end asc
    when pggen.arg('timestamp_desc') then timestamp end desc
    when pggen.arg('width_asc')      then width end asc
    when pggen.arg('width_desc')     then width end desc
  end;

to pass a bunch of bools to the generated query and switch case inside of it ( though this performance of doing this might not be great - but not sure )

so my question is, what would you do in this situation?
perhaps there is a better trick? or a change to pggen?

thanks!

bug: whitespace between bracket and argument returns in error

This query works:

-- name: FindAllArticles :many
select * from articles where name = pggen.arg('Name')

This one, generated by our formatter with the default settings, does not work (notice the space between arg and ()

-- name: FindAllArticles :many
select * from articles where name = pggen.arg ('Name')

I think, this is related to this line, although I am not sure.

if p.tok == token.QueryFragment && strings.HasSuffix(p.lit, "pggen.arg(") {

And btw, thanks for this project, I love it! ๐Ÿ’œ

Feature: SELECT with nested structs

In SQLX, the following syntax is correct:

SELECT
  mac,
  name,
  "user".id "user.id",
  "user".name "user.name"
FROM device
LEFT JOIN "user"
  ON "user".id=device.user_id;

and can be scanned into a model such as:

type Device struct {
  Mac string
  Name string
  User *struct {
    ID string
    Name string
  }
}

Doing this in pggen tries to generate struct fields containing a dot, which is not a valid format.

Are you planning to add support for such scans with sub structs?

Add schema-glob flag

Schema glob allows uses cases like pointing to a directory of migrations or schema files. Implementing schema globbing is more difficult than query globs because the order or running schema files is important. For example, assuming the following directory structure:

one.sql
migrations/
   - 01_create_tables.sql
   - 02_create_roles.sql
   - 03_custom_script.sh
two.sh

Then, the command:

pggen gen go --schema-file 'one.sql' --schema-glob 'migrations/*.sql' --schema-file 'two.sh'

should populate the Docker entrypoint directory with:

01_one.sql
02_01_create_tables.sql
03_02_create_roles.sql
04_03_custom_script.sh

Using enums in composite type row results fails to compile

I pack whole rows in composit types, the generated enum fields cannot handled by the internal newCompositeType function internally.:
I get following error:

Cannot use '&MyEnum{}' (type *MyEnum) as type pgtype.ValueTranscoder Type does not implement 'pgtype.ValueTranscoder'

SELECT * FROM x and UPDATE|DELETE|INSERT x RETURNING * should use the same type

I'm evaluating a pggen to use it instead of sqlc, but there a re thing that sqlc does better, one of them is reuse of return types.
The pggen generates a ton of types and it doesn't check if the types could be reused by different queries. This is especially annoying as one must do another ton of copy From/To functions for basic CRUD methods because

Support for psql-like parameters (:parameter)

In order for the .sql files to be tested by a regression tool, e.g. regresql, it would be nice, if the psql-like parameters could be supported.

So, the following SQL statements should be equal:

-- name: CurrentVersion :one
select name from albums where id = pggen.arg('id');

-- name: NewVersion :one
select name from albums where id = :id;

Support for JSONB?

Is there support to specify how to convert JSONB data to Go structs? I've searched the documentation but haven't found anything.

proposal: panicing function on non-empty-row error

I propose to generate a panicking alternative function.
Panicking in go happens when something go really wrong and the program flow should be disrupted - this is mostly the case on any pg error - with one exception: no row error.
The row instead is a pointer to a row, which is nil when no row is found.

Foo() // current query function
FooX() // panicking query function
// https://github.com/ent/ent has this methods

proposal: generate "queries" by stored pg function

Motivation

Generally allow any complex nested queries. Currently it's unclear what is possible and when pggen cannot handle it
(as a result this issue is in contract to the promising project description)

In contrast the limitation caused on pg side ("optional arguments" because no reliable argument & result type reflection is possible) are clear.

example schema

create type list_item as (
   name text,
   color text
);

create type list_stats as (
   val1 hstore,
   val2 int[]
);

create or replace function fn1(
    param1 bigint,
    page int = 1,
    item_count int = 10,
    out res_items list_item[],
    out res_stats list_stats
) language plpgsql as $$begin
    -- single or many query which fills res_items and res_stats 
    -- ex: with x as (...subquery...) select array_agg(...), array_agg(...) into res_items, res_stats
end$$;

keep in mind the same return values could be also created by a single statement used directly as pggen query.
this example just demonstrate the way trough stored functions since this was my initial situation I was confronted with this idea.

example query

-- name: Fn1 :one
select fn1(
    pggen.arg('x')
);

My intention is to create plpgsql stored functions which takes all needed arguments once and has at least two different datasets as return values.
There could be an arbitrary number of return values. They can be arrays, single values and of course nested.

advantages

  • one function with one parameter list to create multiple datasets
  • pggen (future): optional function parameters could be recognized (when create go code directly from a function signature)

disadvantages

  • increased complexity

challenges

  • OID's of the composit types or array types must be known? when i understand correctly this is one of the main issue, right?

questions

  • what are the current issues to support this? I saw that pggen queries all the information about the result schema like FindDescendantOIDs. When some OID's must be preprocessed in pgx, maybe a preparation function (like PrepareAll in #26 ) could be a solution (since the pg binary msg format don't support more info on query time, a preparation of pgx is anyway indispensable)

workaround

  • batch execute multiple standalone queries

unclear

  • performance
    • regarding the scan process of the nested composit type arrays
    • call plpgsql function vs run batched prepared statements in general

_ltree (ltree[]) not recognized

I currently have the problem that I don't can make _ltree (array of ltree values) to work.

pggen gen go --go-type "ltree=github.com/jackc/pgtype.Text" \
  --go-type "_ltree=github.com/jackc/pgtype.TextArray"
infer output types for query: fetch oid types: find array types: find type for array elem _ltree oid=1266834

Any ideas?

How can I use pointers like `*string` or `*int` for nullable columns in generated models?

Curently we have support custom types, but it would be nice to have support for pointers to standard types (string, int, time.Time etc...)

This is how it is done in pgx driver

pgx can map nulls in two ways. The first is Null* types that have a data field and a valid field. They work in a similar fashion to database/sql. The second is to use a pointer to a pointer.

https://pkg.go.dev/github.com/jackc/[email protected]+incompatible#hdr-Null_Mapping

Generate the same type for queries with the same SELECT ... FROM

First of all: thanks so much for this library! It's just incredible -- it feels exactly like how I want to work with databases in go.

Background

Most of our tables are representations of an rpc type, so that we have an entity CostCenter that is stored in the cost_center table.

Then we have several different ways of querying for the CostCenter (FindByID, FindBySubscriber, ListBySubscribers etc). All of these queries always do SELECT * FROM cost_center and then do the same mapping from the returned database type to our internal rpc type.

This requires repeating the mapping from the pggen-generated row type to the rpc type for each query -- even though the structs have exactly the same structure.

Suggestion

Add a way to have SELECT statements that return the exact same thing, also use the same type.

e.g. currently we have this generated code (removing comments and batching for readability):

	FindByID(ctx context.Context, costCenterID db.UUID) (FindByIDRow, error)
	FindBySubscriber(ctx context.Context, subscriberID db.UUID) (FindBySubscriberRow, error)
	ListBySubscribers(ctx context.Context, subscriberIDs pgtype.UUIDArray) ([]ListBySubscribersRow, error)

But FindByIDRow, FindBySubscriberRow and ListBySubscribersRow are all identical structs (except for the name).

What would be nice is something like:

	FindByID(ctx context.Context, costCenterID db.UUID) (SelectCostCenterRow, error)
	FindBySubscriber(ctx context.Context, subscriberID db.UUID) (SelectCostCenterRow, error)
	ListBySubscribers(ctx context.Context, subscriberIDs pgtype.UUIDArray) ([]SelectCostCenterRow, error)

Dynamic queries: add support for predicates in WHERE clauses

Use case is to enable queries with dynamic expressions for WHERE clauses, like:

SELECT *
FROM foo
WHERE pggen.predicate('filter_expr')

Sketch:

  • Generated Go code takes a FilterExpr function that returns (string, error), maybe implementing a generic interface so it's easy to wrap existing Go SQL codegen.
  • If we implement an interface, we should move it to a different library so folks don't have to depend on the relatively heavyweight dependencies of pggen, namely Docker.
  • pggen calls FilterExpr and injects code as-is into the query.

Question: Insert statement with multiple value rows.

Hello,

Is it possible to generate an insert func that can insert multiple rows at once?
Ex:

INSERT INTO user 
    (username, password, created_at) 
VALUES
    ('a', '123', NOW()),
    ('b', 'abc', NOW()),
    ('c', '456', NOW()),
    ('d', 'def', NOW());

I understand that there's a batch API available but that still executes N queries on the DB vs just one with multiple value rows. At that point, would you know if there's a difference on the DB side performance wise?

Thanks!

pgtype: custom types support

Currently custom declared types declared in pgtype are not supported.

// this is typically located in pgx connection config AfterConnect function:

ci := conn.ConnInfo()

ci.RegisterDataType(pgtype.DataType{
	Value: new(shopspring.Numeric),
	Name: "numeric",
	OID: pgtype.NumericOID,
})

// register custom enums/composit types with combination of:
// pgxtype.LoadDataType
// ci.RegisterDataType
// ci.RegisterDefaultPgType

I know pggen is in heavy development, but I create this issue to make sure what I mean.

Querier only has the methods from the Leader file

Querier interface only has the methods generated from the Leader file.

Example: https://gist.github.com/abijr/ea58ff718b99a830c79772c803b86743

You can see that only the QueryOne methods are added to the Querier interface, QueryTwo is ignored: https://gist.github.com/abijr/ea58ff718b99a830c79772c803b86743#file-one-sql-go-L17-L24

I expected it to include all the queries from all the generated files.

I think that the problem is that only the queries that correspond to a single file are stored the TemplatedFile struct. Not sure how you would like to approach the fix.

Mapping SQL types to slices and/or pointers of imported types is broken

Mapping an SQL type to a pointer and/or a slice of a go type which must be imported results in invalid generated go code as pggen is confused by the slice and pointer markers ([] and *).

Minimally reproducible example

pggen Version

$ pggen version
pggen version 2021-09-07, commit 7f87888

Gen Command

pggen gen go -query-glob "queries/*.sql" \
    --postgres-connection "postgres://postgres@localhost/tit?sslmode=disable" \
    --go-type "timestamp=*time.Time" \
    --go-type "_timestamptz=[]time.Time" \
    --go-type "_timestamp=[]*time.Time" \
    --go-type "_interval=[]util/custom/times.Interval" \
    --go-type "_date=util/custom/[]times.Date" 

Queries

-- name: GetNilTime :one
SELECT pggen.arg('data')::TIMESTAMP;

-- name: GetTimes :many
SELECT * FROM UNNEST(pggen.arg('data')::TIMESTAMP WITH TIME ZONE[]);

-- name: GetNilTimes :many
SELECT * FROM UNNEST(pggen.arg('data')::TIMESTAMP[]);

-- name: GetCustomIntervals :many
SELECT * FROM UNNEST(pggen.arg('data')::INTERVAL[]);

-- name: GetCustomDates :many
SELECT * FROM UNNEST(pggen.arg('data')::DATE[]);

Generated File

// Code generated by pggen. DO NOT EDIT.

package queries

import (
	// --snip--

	"time"
	"util/custom/[]times"
	"util/custom/times"
)

// --snip--
type Querier interface {
	GetNilTime(ctx context.Context, data time.*Time) (time.*Time, error)
	// --snip--

	GetTimes(ctx context.Context, data time.[]Time) ([]pgtype.Timestamptz, error)
	// --snip--

	GetNilTimes(ctx context.Context, data time.[]*Time) ([]time.*Time, error)
	// --snip--

	GetCustomIntervals(ctx context.Context, data times.[]Interval) ([]pgtype.Interval, error)
	// --snip--

	GetCustomDates(ctx context.Context, data []times.Date) ([]pgtype.Date, error)
	// --snip--
}

Expected

// Code generated by pggen. DO NOT EDIT.

package queries

import (
	// --snip--

	"time"
	"util/custom/times"
)

// --snip--
type Querier interface {
	GetNilTime(ctx context.Context, data *time.Time) (*time.Time, error)
	// --snip--

	GetTimes(ctx context.Context, data []time.Time) ([]time.Time, error)
	// --snip--

	GetNilTimes(ctx context.Context, data []*time.Time) ([]*time.Time, error)
	// --snip--

	GetCustomIntervals(ctx context.Context, data []times.Interval) ([]times.Interval, error)
	// --snip--

	GetCustomDates(ctx context.Context, data []times.Date) ([]times.Date, error)
	// --snip--
}

The two main issues are the miss-handling of the [] and * markers and the failure to correctly map the return values for:

  • "_timestamptz=[]time.Time" (GetTimes)
  • "_interval=[]util/custom/times.Interval" (GetCustomIntervals)
  • "_date=util/custom/[]times.Date" (GetCustomDates)

For custom types, I placed the slice marker ([]) at different position, each result in the correct placement either as the argument type or the import and fails at the other location.


As a side note, (tho this might be worth a separate issue if you think it's worth it) I used different SQL types for each queries as example since I cannot map a single SQL type to multiple Go types. It would be nice to be able to further specify a type mapping per query argument and for the return value. I.e. I can map overall timestamp=time.Time but for a query I could add a comment to the query to map a specific argument to a different type, like:

-- name: GetNilTime :one *time.Time
-- param: data *time.Time
SELECT pggen.arg('data')::TIMESTAMP;

Query Files by Glob Pattern

pggen gen go --query-file dbquery/*.sql --output-dir dbquery --postgres-connection "host=$PGHOST user=$PGUSER"

using go std globbing or https://github.com/bmatcuk/doublestar (just found this few days ago, and it's globbing related)

Motivation
I have split up all my sql files to make it managable, while development those sql files can often change or renamed.
Theoretically I could implement this globbing by a manual shell script. But because pggen support process multiple files, the globbing should be integrated.

Working with Postgres extensions

This might be a long shot, but will it be possible to use pggen with Postgres extensions?

Currently, it does not seem to work. For example, given the following schema:

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE "user"(
  email TEXT PRIMARY KEY,
  pass TEXT NOT NULL
);

And query:

-- name: CreateUser :one
INSERT INTO "user"(email, pass)
VALUES (pggen.arg('email'), crypto(pggen.arg('password'), gen_salt('bf')));

The following error appears:

infer typed named query CreateUser: infer input types for query:
exec prepare statement to infer input query types:
ERROR: function crypto(unknown, text) does not exist (SQLSTATE 42883)

Use array_arg on nullable relationship

I've got a 1:n relationship between two tables (here: question, option). If I try to use array_agg on options where I have 1:0, then get the following error message:

assign GetSurveyQuestions row: cannot assign NULL to *queries.Option

This is the query I'm using:

SELECT sq.survey_id, sq.question_id, q.type, q.category, array_agg(o) as options
FROM survey_question sq
  LEFT JOIN question q ON q.question_id = sq.question_id
  LEFT JOIN option o ON o.question_id = q.question_id
WHERE sq.survey_id = pggen.arg('surveyId')
GROUP BY sq.survey_id, sq.question_id, q.type, q.category, sq.position
ORDER BY sq.position;

Is this case supported?

The code output by `pggen` is dependent on the order of the columns in a table in `schema.sql`, though this order doesn't matter according to spec

Thanks for building pggen -- it's pretty remarkable.

I've been running into an issue where the order of columns listed in schema.sql changes sometimes (though the columns themselves don't change). According to the SQL spec, reorderings of the columns in a table are no-ops, but the code output by pggen is substantively different since the fields in a given struct have reordered. The resulting symptom is an error due to type mismatch when trying to scan data into a struct.

Am I just using this library wrong? Is there a workaround for this issue?

Better detection of null values

Hi,

I am currently evaluating pggen with various type of queries and I am struggling with null values.

Let's say I have a simple table

CREATE TYPE job_completion as ENUM (
    'ok',
    'error',
    'abort'
    );

CREATE TABLE job
(
    id         bigint primary key generated always as identity,
    created_at timestamptz    not null default now(),
    updated_at timestamptz    not null default now(),
    ended_at   timestamptz   ,
    sync_since timestamptz   ,
    completion job_completion,
    data       int           ,
    error      text          
);

1. Null values in parameters

To update a job I would write the following:

-- name: JobUpdate :one
update job
set updated_at = now(),
    data       = pggen.arg('data'),
    completion = pggen.arg('completion'),
    ended_at   = pggen.arg('ended_at')
where id = pggen.arg('id')
returning *;

resulting in the following parameters

type JobUpdateParams struct {
	Data       int32
	Completion JobCompletion
	EndedAt    pgtype.Timestamptz
	ID         int
}

But as the columns are NULLABLE I would expect Data Completion and EndedAt to be pointers.

To go around this one can use NULLIF in the query but that make queries more verbose. Is there another way to do this ?

2. Null values in output

When limiting or ordering results in a select statement I get all fields as pointers

select *
from job
limit 1;
type JobGetLatestRow struct {
	ID         *int               `json:"id"`
	CreatedAt  pgtype.Timestamptz `json:"created_at"`
	UpdatedAt  pgtype.Timestamptz `json:"updated_at"`
	EndedAt    pgtype.Timestamptz `json:"ended_at"`
	SyncSince  pgtype.Timestamptz `json:"sync_since"`
	Completion JobCompletion      `json:"completion"`
	Data       *int32             `json:"data"`
	Error      *string            `json:"error"`
}

It seems the Sort and Limit plan types are not handled in isColNullable.

The list of plan types is rather long (https://github.com/postgres/postgres/blob/master/src/backend/commands/explain.c#L1163) do you think we could have a simple case where the column is used to detect nullability if it's a select statement ?

Let me know if you want a PR
Many thanks for your time and for this library.

Using custom types + arrays as pggen.arg()

Actual:

When using custom types as argument (or an array) the generated code just pass the generated []CustomType go argument to QueryRow()

select pggen.arg('my_types')::custom_type[]

However []CustomType is not encodable by pgx, so it fails.

Expected:
It should create a encodable pgtype Value (pgtype.NewArrayType) before passing it to the query.

Support for migration tools

Is it possible to enable support for migration tools. I tried to use pggen with golang migrate. Pggen didn't ignore down sql files and codegen failed.

Proposal: Accessor methods for row structs

Might it be a good idea to provide accessor methods on the row structs, specifically "getters"? Here's an example of how they help abstract across different row structs with the same fields.

type FindAuthorsRow struct {
	AuthorID  int32   `json:"author_id"`
	FirstName string  `json:"first_name"`
	LastName  string  `json:"last_name"`
	Suffix    *string `json:"suffix"`
}

type FindAuthorByIDRow struct {
	AuthorID  int32   `json:"author_id"`
	FirstName string  `json:"first_name"`
	LastName  string  `json:"last_name"`
	Suffix    *string `json:"suffix"`
}

type Name interface {
	GetFirstName() string
	GetLastName() string
}

func handleName(name Name) {
	first := name.GetFirstName()
	last := name.GetLastName()
	...
}

authors, _ := q.FindAuthors(context.Background(), "john")
authorByID, _ := q.FindAuthorByID(context.Background(), 123)

for _, author := range authors {
	handleName(author)
}
handleName(authorByID)

Improvement on PGX nullable type usage

Inside the readme, the following is found

Choosing an appropriate type is more difficult than might seem at first glance due to null. When Postgres reports that a column has a type text, 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 in internal/pginfer/nullability.go is rudimentary; a proper approach requires a full explain plan with some control flow analysis.

However, if you use PGX directly and not the standard lib SQL version, the null types from pgtype are rarely required. Instead, you can simply use the native type with a pointer to infer what is optional and what is not. Is this package designed to support PGXs usage of the raw postgres protocol? If so it would be great to re-visit when pgtypes actually need to be used.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.