pg_describe

This Release
pg_describe 1.0.0
Date
Status
Stable
Abstract
Report what a query would return, without executing it
Description
Runs PostgreSQL's own parser and analyser over a statement and stops before the executor, reporting the inferred type of every parameter and the name, type, source column and nullability of every result column. This is the server-side equivalent of sending Parse/Describe over the extended query protocol without Bind or Execute, as pgTyped, sqlc and sqlx do. Unlike tools that read pg_attribute.attnotnull alone, it walks the query's join tree, so a column on the nullable side of an outer join is correctly reported as nullable. Ships with a TypeScript code generator.
Released By
sajonaro
License
MIT
Resources
Special Files
Tags

Extensions

pg_describe 1.0.0
Report what a query would return, without executing it

Documentation

how-it-works
How pg_describe works
README
TypeScript example
credit
credit
function-reference
function-reference
README
pg-describe-gen
nullability
nullability
generated-code
generated-code
contributing
contributing
cli-configuration
cli-configuration
installation
installation
overview
overview
end-to-end-example
end-to-end-example
type-mapping
type-mapping
getting-started
getting-started
queries-in-sql-files
queries-in-sql-files
permissions
permissions
features
features

README

pg_describe

CI PGXN npm PostgreSQL 17 License: MIT

pg_describe reports what a query would return, without running it.

SELECT * FROM pg_describe('SELECT id, email FROM users WHERE id = $1');
  kind  | ord | name  | type_name | source_table | base_not_null | result_not_null
--------+-----+-------+-----------+--------------+---------------+-----------------
 param  |   1 |       | integer   |              |               |
 column |   1 | id    | integer   | users        | t             | t
 column |   2 | email | text      | users        | t             | t

Features

  • Nothing is executed — parse and analysis only, so describing DELETE FROM orders WHERE id = $1 is safe.
  • Parameter types are inferred; the query text never declares one.
  • Result columns as the wire protocol sees them — name, type OID and SQL type name, with ORDER BY-only columns excluded.
  • Outer-join-aware nullability: a column on the nullable side of a LEFT JOIN is reported nullable even when it is declared NOT NULL. Tools that read attnotnull alone get this wrong.
  • Column provenance — the source table and column behind each result field.
  • One round trip. It is an ordinary SELECT, callable from any client in any language, with no wire-protocol code to write.
  • Privileges are enforced, so it cannot be used to read a schema the caller has no rights to.
  • TypeScript code generation from plain .sql files, included.

Documentation

Full documentation: https://sajonaro.github.io/pg_describe/

Getting started

git clone https://github.com/sajonaro/pg_describe
cd pg_describe
docker compose up -d          # PGPORT=5433 docker compose up -d  if 5432 is taken

psql -h localhost -U postgres -d pg_describe_demo \
     -c "SELECT * FROM pg_describe('SELECT 1 AS n')"

Or install it into a database you already have:

pgxn install pg_describe
CREATE EXTENSION pg_describe;

Needs PostgreSQL 17 (16 will probably work, untested) and the ability to install extensions — see Installation. For TypeScript types:

npm install --save-dev pg-describe-gen

Example

Write plain SQL. Native $1, no dialect, so the file runs in psql as it is:

-- queries/orders.sql
-- @name ListRecentOrders
-- Orders with their customer, if any.
SELECT o.id, o.total, c.email
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE o.placed_at >= $1;
npx pg-describe-gen

Get types the database itself vouches for:

export interface ListRecentOrdersParams {
  p1: Date
}

export interface ListRecentOrdersRow {
  id: string            // orders.id       — bigint arrives as a string
  total: string         // orders.total    — numeric arrives as a string
  email: string | null  // customers.email — NOT NULL, but LEFT JOINed
}

/**
 * Orders with their customer, if any.
 */
export async function listRecentOrders(
  client: ClientBase,
  params: ListRecentOrdersParams,
): Promise<ListRecentOrdersRow[]>

email is nullable because the join can null-extend it, not because the schema says so — that is the difference. Add pg-describe-gen --check to CI and a migration that changes what a query returns fails the pull request instead of the deploy.

The end-to-end example walks the whole loop, and examples/typescript is it, runnable.

Resources

Repository

src/pg_describe.c        the extension
test/                    pg_regress suite, 29 assertions
packages/codegen/        pg-describe-gen, published to npm
examples/typescript/     runnable example, generated output committed
docs/                    documentation source
website/                 Docusaurus site that renders docs/

packages/* and examples/* are npm workspaces: npm install at the root wires the example to the local generator. See Contributing for the test and docs workflows.

Project state

The extension and the generator are complete and tested on every commit, but this is young. The API of pg_describe(text) is what the TypeScript generator depends on and is not expected to change; the generator’s config file may still gain keys. Issues and pull requests are welcome.

Credit

The idea is taken from pgTyped by Adel Salakh, which demonstrated that TypeScript types for SQL can come from a live database rather than a hand-maintained model of it. sqlc and sqlx do the same for Go and Rust.

pgTyped asks the server the same question over the wire protocol, from the client, and needs no extension — which is what you want on RDS, Cloud SQL and most managed Postgres, where pg_describe cannot be installed at all. It is mature, actively maintained and has a larger feature surface than this project. pg_describe moves the question into the server for the cases where you can install an extension: one SELECT instead of a wire-protocol implementation per language, and nullability that accounts for outer joins. See Credit.

License

MIT © 2026-present, see LICENSE.