pgx code emitter

CodeEmitterPort adapter for the Piko querier code generator that emits Go source targeting pgx/v5 instead of database/sql.

Overview

Emitters are build-time artefacts. After the querier service builds a catalogue and analyses your queries, an emitter turns the analysed query tree into Go source files. Those files include query methods, parameter structs, result structs, and SQL constants. Piko ships three emitters. The default database/sql emitter works with any *sql.DB-compatible driver. This pgx emitter generates code that uses pgx's native interfaces, pgx.Rows, pgx.Batch, pgx.CopyFromSource, and pgconn.CommandTag. A third emitter targets ClickHouse.

Reach for the pgx emitter when you want native pgx affordances in your generated code. Examples include :batch queries that batch-queue statements through pgx.Batch and :copyfrom queries that stream via CopyFrom. The pgx emitter also exposes pgconn.CommandTag from :execresult queries, whose RowsAffected() returns int64 directly without an error. Reach for the default database/sql emitter when you want generated code that is portable across drivers. It also fits when you are not on Postgres, or when you want to keep the generated surface narrow and database/sql-shaped.

This emitter implements the whole CodeEmitterPort contract. It provides EmitModels, EmitQuerier, EmitQueries, EmitPrepared, and EmitOTel. It builds models, the querier scaffold, the DBTX interface, the query methods, and the OpenTelemetry resolver, all as Go source text. It is a complete drop-in for the default emitter, not a query-method add-on. Generation is deterministic and syntactically valid because the emitter constructs go/ast nodes instead of string templates.

EmitPrepared is an intentional no-op. A pgx pool caches prepared statements per connection, so the emitter leans on the pgx runtime instead of generating a prepared-statement wrapper. This keeps the generated surface lean.

The emitter shares its method-strategy and batch handlers with the default database/sql and ClickHouse emitters. All three stay behaviourally consistent and route through the same querier pipeline with no extra glue.

The emitter produces source text only. The generated code imports github.com/jackc/pgx/v5 and github.com/jackc/pgx/v5/pgconn. The emitter package itself does not import pgx, so adding it to your build-time toolchain does not pull pgx into binaries that do not use it.

Configuration

import "piko.sh/piko/wdk/db/db_emitter_pgx"

emitter := db_emitter_pgx.NewPgxEmitter()

NewPgxEmitter takes no arguments. The querier service inputs (package name, query files) drive the generated code shape (struct names, package layout), not emitter configuration.

Bootstrap

The pgx emitter is not registered through piko.With*. The built-in generate-sql and build pipeline hard-codes the database/sql emitter, and neither db.EngineConfig nor db.DatabaseRegistration exposes an emitter field. A database registered the normal way and built through the standard pipeline produces database/sql code, never pgx code.

To emit pgx code, drive the generator yourself. db.NewQuerierService is the only public seam that accepts a custom emitter. Pass it through db.QuerierPorts.Emitter:

import (
    "context"
    "embed"

    "piko.sh/piko/wdk/db"
    "piko.sh/piko/wdk/db/db_emitter_pgx"
    "piko.sh/piko/wdk/db/db_engine_postgres"
)

//go:embed migrations queries
var sqlFS embed.FS

func generate(ctx context.Context) error {
    service, err := db.NewQuerierService(db.QuerierPorts{
        Engine:     db_engine_postgres.NewPostgresEngine(),
        Emitter:    db_emitter_pgx.NewPgxEmitter(),
        FileReader: db.NewFSFileReader(sqlFS),
    })
    if err != nil {
        return err
    }

    config := &db.DatabaseConfig{
        MigrationDirectory: "migrations",
        QueryDirectory:     "queries",
    }

    result, err := service.GenerateDatabase(ctx, "generated", config)
    if err != nil {
        return err
    }

    // Write each result.Files entry to disk under your output directory.
    _ = result
    return nil
}

QuerierPorts.Engine is the dialect parser used at generation time. For Postgres, use db_engine_postgres.NewPostgresEngine(), not the Postgres() engine config used for runtime registration. FileReader must be non-nil, since NewQuerierService rejects a nil reader. Use db.NewFSFileReader to back it with an embed.FS.

Runtime handle

The generated Queries struct binds to a DBTX interface with pgx-native methods: Exec(ctx) (pgconn.CommandTag, error), Query(ctx) (pgx.Rows, error), QueryRow(ctx) pgx.Row, SendBatch(ctx, *pgx.Batch) pgx.BatchResults, CopyFrom(ctx, ...) (int64, error), and Begin(ctx) (pgx.Tx, error). The New(db DBTX) constructor takes that interface.

A *sql.DB, including one from pgx's database/sql adapter (github.com/jackc/pgx/v5/stdlib), does not satisfy DBTX. It exposes ExecContext, QueryContext, and QueryRowContext instead. Pass a pgx-native handle: *pgxpool.Pool, *pgx.Conn, or pgx.Tx. Construct and own that pool yourself, outside db.WithDatabase, since piko's database registration manages a database/sql handle.

Migrations and seeds still run on database/sql. A pgx-runtime project that uses piko's migration executor also keeps a database/sql connection for schema management, even though queries run on pgx.

See also

Postgres pieces:

  • PostgreSQL engine, runtime engine and dialect; pair with this emitter for Postgres-specific query generation.
  • PostgreSQL Catalogue, live-database introspection that pairs with this emitter when migrations are not the source of truth.

Framework docs:

External: