C - pg-query Extension Development
This page tells you how to develop the pg-query-ext PHP extension. The extension is written in C. It wraps
libpg_query, a C library that uses the real PostgreSQL parser.
Overview
The pg-query-ext extension gives PHP access to the SQL parser of PostgreSQL. Its functions parse, normalize,
fingerprint and split SQL queries. It links statically against libpg_query, which contains the PostgreSQL 18
grammar.
For the usage documentation, read pg-query Extension.
Development Setup
nix-shell --arg with-pg-query-ext false --arg with-c true
This shell gives you the C toolchain (gcc, autoconf, automake, libtool, protobuf). It also gives you the PHP dev headers for a build of the extension from source.
Project Structure
src/extension/pg-query-ext/
├── Makefile # Build orchestration (autotools + libpg_query fetch)
├── ext/ # C source code
│ ├── config.m4 # PHP extension build config (autoconf)
│ ├── pg_query.c # Extension implementation
│ ├── pg_query.stub.php # PHP stubs for static analysis
│ └── php_pg_query.h # Header file
├── tests/
│ └── phpt/ # PHPT test files
└── vendor/
└── libpg_query/ # Auto-downloaded libpg_query source (gitignored)
Commands
Build the extension. The first build downloads libpg_query:
nix-shell --arg with-pg-query-ext false --arg with-c true --run "cd src/extension/pg-query-ext && make build"
A system libpg_query must have a PG_MAJORVERSION that matches the pinned LIBPG_QUERY_VERSION in config.m4.
If it does not match, the build does not use it:
- If
--with-pg-query=DIRpoints to a library with a different version, configure stops with an error. - If the build finds a library with a different version, for example a Homebrew keg for a different PostgreSQL major, it skips that library and downloads the pinned version.
This rule prevents a silent build of pg_query.so with the wrong PostgreSQL grammar
(#2483).
Run the PHPT tests:
nix-shell --arg with-pg-query-ext false --arg with-c true --run "cd src/extension/pg-query-ext && make test"
After a change to the C source, rebuild only the extension. This skips libpg_query and is faster:
nix-shell --arg with-pg-query-ext false --arg with-c true --run "cd src/extension/pg-query-ext && make rebuild"
Remove the build artifacts of the extension and keep libpg_query:
nix-shell --arg with-pg-query-ext false --arg with-c true --run "cd src/extension/pg-query-ext && make clean"
Remove everything, also the downloaded libpg_query:
nix-shell --arg with-pg-query-ext false --arg with-c true --run "cd src/extension/pg-query-ext && make distclean"
Make Targets
| Target | Description |
|---|---|
build |
Full build (download a missing libpg_query, compile) |
test |
Run PHPT tests |
install |
Install to system PHP |
rebuild |
Rebuild extension only (skip libpg_query) |
clean |
Remove extension build artifacts |
distclean |
Remove everything, also libpg_query |
Upgrading libpg_query (keep the C, nix, and PHP sides in sync)
The extension is only one part. Each libpg_query version pins one PostgreSQL grammar. Three places use that
grammar, and they must always agree:
| Place | What it is | Used by |
|---|---|---|
src/extension/pg-query-ext/vendor/libpg_query (LIBPG_QUERY_VERSION) |
the C library compiled into the extension | CI (pie build) |
.nix/pkgs/php-pg-query-ext/package.nix (libpg_query version/rev/hash) |
the C library compiled into the nix dev shell | local nix-shell |
src/lib/postgresql/resources/proto/pg_query.proto + the generated stubs in src/lib/postgresql/src/Flow/PostgreSql/Protobuf/ |
the protobuf schema that the PHP side uses to encode and decode parse trees | the flow-php/postgresql library |
The PHP library sends parse trees through protobuf in both directions (pg_query_parse_protobuf → mutate →
pg_query_deparse). Protobuf encodes each Node oneof case by its field number. When a new PostgreSQL major adds a
node, all later field numbers change. If you update the C library and not the PHP stubs, PHP encodes parse trees with
the wrong tags. pg_query_deparse can then fail to decode them, and the process stops with a segfault. This
happened in a real incident: the upgrade from PG17 to PG18 did not update the stubs, and CI crashed at random. A
stale nix pin hid the problem on local machines.
When you change the libpg_query version, do all of these steps. Then run the full PHP suite:
- Update the C side: set
LIBPG_QUERY_VERSIONinsrc/extension/pg-query-ext/Makefileandext/config.m4. Use a release tag, never a branch. The18-latestbranch once moved while CI ran. Then runmake distclean build. - Update the nix pin in
.nix/pkgs/php-pg-query-ext/package.nixto the samelibpg_querytag, and update itshash(nix-prefetch-url --unpack <github-archive-url>→nix hash convert --to sri). - Generate the PHP stubs again from the new grammar. Copy
vendor/libpg_query/protobuf/pg_query.prototoresources/proto/pg_query.proto, and keep theoption php_namespace/php_metadata_namespacelines. Then runjust gen-protobuf-pginsidenix-shell --arg with-protoc true. - Change the query builder and the AST for each grammar change, for example new
Constraintfields or clauses with a new structure.mago analyzeand the test suite show these changes.
Always validate against the full PHP suite, not just the PHPT tests
make test runs only the PHPT tests at the C level. It does not run the parse and deparse round trips of the PHP
flow-php/postgresql library, and grammar drift breaks these round trips. After each extension change, run the full
suite in a shell that has the extension that you changed:
# uses the nix-built extension (the package.nix pin) - must match the vendored/CI version
nix-shell --run "just test"
# or, at minimum, the suites that exercise parse/deparse round-trips:
nix-shell --run "tools/phpunit/vendor/bin/phpunit --testsuite lib-postgresql-unit,lib-postgresql-integration,bridge-phpunit-postgresql-unit,bridge-phpunit-postgresql-integration"
A segfault shows as signal 11 / exit 139. To get a backtrace, run the failed command with lldb (macOS) or gdb
(Linux):
nix-shell --run 'lldb -b -o "settings set target.process.stop-on-exec false" -o run -o bt -o quit \
-- "$(command -v php)" tools/phpunit/vendor/bin/phpunit --testsuite lib-postgresql-unit'
Found a typo or an outdated section? Edit this page on GitHub