What happened?
When building pg-query-ext, ext/config.m4 searches the system for an existing libpg_query by default (SEARCH_PATH="/usr/local /usr /opt/local /opt/homebrew") and statically links the first libpg_query.a it finds, with no version check. If a libpg_query for a different PostgreSQL major than the extension targets is installed (e.g. via Homebrew), the build succeeds but pg_query.so ends up with the wrong PostgreSQL grammar permanently baked in.
config.m4 even announces the intended version (Using libpg_query 18-latest (PostgreSQL 18 grammar)) and then logs checking for libpg_query... found in /opt/homebrew, silently linking the wrong one. On a machine with no system libpg_query, the identical build correctly logs not found, will download and build -> Downloading libpg_query 18-latest.
Because libpg_query's protobuf AST field numbers differ between PG majors (SelectStmt is field 136 in PG17, 138 in PG18) and flow-php/postgresql's generated message classes are version-locked, a mismatched build mis-decodes every parse: a SELECT NULL::text tree comes back stamped version: 170007 with the statement node decoded as UpdateStmt, so getSelectStmt() is null and ColumnTypeParser throws Expected SelectStmt node, got unknown.
This was very hard to diagnose: php --ri pg_query reports libpg_query version => 18.0.0 even on the PG17-linked build (a hardcoded constant, decoupled from the bundled sources), and the extension version, protobuf-c, and package commits all matched between a working and a broken machine. The only reliable tells were the parse-output version field (170007 vs 180004) and strings pg_query.so | grep postgres-1 (postgres-17 vs postgres-18).
Expected: the extension should build against the version it targets (LIBPG_QUERY_VERSION), not silently adopt an incompatible system library.
Root cause in config.m4:
SEARCH_PATH="/usr/local /usr /opt/local /opt/homebrew" dnl searched by default
...
if test -r "$i/pg_query.h" && test -r "$i/postgres_deparse.h" && test -r "$i/libpg_query.a"; then
PG_QUERY_DIR=$i
AC_MSG_RESULT([found in $i]) dnl no version comparison
break
fi
Suggested fixes
-
Don't auto-search system paths by default. Only use a system libpg_query when --with-pg-query=DIR is passed explicitly; otherwise always download/build the pinned LIBPG_QUERY_VERSION. Matches the "optional" wording in the --with-pg-query help and what CI/Docker already get (no system lib, so they always download).
-
Verify the version of whatever lib is used (found via search or passed via --with-pg-query). The PG major is a macro in the header config.m4 already requires (pg_query.h: #define PG_MAJORVERSION "17"), so a few lines of sed reject a mismatch and fall through to download:
PG_EXPECTED_MAJOR=`echo "$LIBPG_QUERY_VERSION" | sed -E 's/-.*//'` dnl "18-latest" -> "18"
...
_found_major=`sed -nE 's/^#define[[:space:]]+PG_MAJORVERSION[[:space:]]+"([0-9]+)".*/\1/p' "$header"`
if test "x$_found_major" = "x$PG_EXPECTED_MAJOR"; then
: accept
else
AC_MSG_WARN([ignoring libpg_query in $i: PostgreSQL $_found_major, need $PG_EXPECTED_MAJOR])
dnl leave PG_QUERY_DIR empty -> downloads the pinned version
fi
Major-version granularity is correct - field numbers shift at major boundaries, not minor releases.
- Make the runtime error self-diagnosing.
ColumnTypeParser::parse() throws unexpectedNodeType('SelectStmt', 'unknown') with a hardcoded 'unknown', so it reports neither the node actually decoded nor the version skew that almost always causes it. Node::getNode() returns the real oneof case and ParseResult::getVersion() the libpg_query PG_VERSION_NUM:
$node = $stmts[0]->getStmt();
if ($node?->getSelectStmt() === null) {
throw InvalidAstException::unexpectedNodeType(
'SelectStmt',
$node?->getNode() ?: 'unknown', // actual case, e.g. "update_stmt"
$parsed->raw()->getVersion(), // e.g. 170007
);
}
A message like Expected SelectStmt, got "update_stmt" (parse result reports PostgreSQL 170007); the pg_query extension was likely built against a different PostgreSQL major than this package expects would turn a multi-hour investigation into a one-line diagnosis.
How to reproduce?
<?php
// This is a build/linking bug, not a PHP-level one. Precondition: the pg_query
// extension must have been built while a system libpg_query of a DIFFERENT
// PostgreSQL major than flow-php/postgresql targets was installed. On macOS:
//
// brew install libpg_query # currently installs a PostgreSQL 17 keg under /opt/homebrew
// pie install "flow-php/pg-query-ext:1.x-dev#<commit>"
// # configure logs: checking for libpg_query... found in /opt/homebrew
// # and statically links the PG17 lib instead of downloading 18-latest
//
// Then every parse mis-decodes:
use Flow\PostgreSql\Parser;
$version = (new Parser())->parse('SELECT NULL::text')->raw()->getVersion();
var_dump($version);
// wrong-version build: 170007 (statement node decodes as UpdateStmt)
// correct build: 180004 (statement node decodes as SelectStmt)
// The higher-level path that actually threw for us:
\Flow\PostgreSql\DSL\column_type_from_string('text');
// -> Flow\PostgreSql\QueryBuilder\Exception\InvalidAstException: Expected SelectStmt node, got unknown
Playground snippet
N/A - this is a native-extension build/linking issue and cannot be reproduced on the Playground.
Data required to reproduce bug locally
No data needed - reproduction depends only on the build environment: a libpg_query for a PostgreSQL major different from LIBPG_QUERY_VERSION present under a configure search prefix (/usr/local /usr /opt/local /opt/homebrew) at extension build time. On macOS, brew install libpg_query (currently a PG17 keg) before building the extension is enough to trigger it.
Version
flow-php/postgresql and flow-php/pg-query-ext: 1.x-dev (postgresql @ 995926b). config.m4 LIBPG_QUERY_VERSION="18-latest" on current 1.x. Toolchain: PHP 8.4, pie 1.4.6, macOS arm64.
Relevant error output
Flow\PostgreSql\QueryBuilder\Exception\InvalidAstException: Expected SelectStmt node, got unknown
at .../flow-php/postgresql/src/Flow/PostgreSql/QueryBuilder/Exception/InvalidAstException.php:26
InvalidAstException::unexpectedNodeType()
at .../flow-php/postgresql/src/Flow/PostgreSql/Parser/ColumnTypeParser.php:36
ColumnTypeParser->parse()
at .../flow-php/postgresql/src/Flow/PostgreSql/DSL/schema.php:1086
Flow\PostgreSql\DSL\column_type_from_string()
at .../flow-php/etl-adapter-postgresql/src/Flow/ETL/Adapter/PostgreSql/SchemaConverter.php:196
SchemaConverter->columnType()
... (application frames trimmed) ...
# build log, broken machine (system PG17 lib linked):
configure: Using libpg_query 18-latest (PostgreSQL 18 grammar)
checking for libpg_query... found in /opt/homebrew
cc ... -o .libs/pg_query.so ... /opt/homebrew/lib/libpg_query.a -lprotobuf-c ...
# build log, working machine (no system lib -> downloads 18):
configure: Using libpg_query 18-latest (PostgreSQL 18 grammar)
checking for libpg_query... not found, will download and build
configure: Downloading libpg_query 18-latest...
# parse of `SELECT NULL::text`:
# broken (PG17): {"version":170007,"stmts":[{"stmt":{"UpdateStmt":{...}}}]}
# correct (PG18): {"version":180004,"stmts":[{"stmt":{"SelectStmt":{...}}}]}
What happened?
When building
pg-query-ext,ext/config.m4searches the system for an existinglibpg_queryby default (SEARCH_PATH="/usr/local /usr /opt/local /opt/homebrew") and statically links the firstlibpg_query.ait finds, with no version check. If alibpg_queryfor a different PostgreSQL major than the extension targets is installed (e.g. via Homebrew), the build succeeds butpg_query.soends up with the wrong PostgreSQL grammar permanently baked in.config.m4even announces the intended version (Using libpg_query 18-latest (PostgreSQL 18 grammar)) and then logschecking for libpg_query... found in /opt/homebrew, silently linking the wrong one. On a machine with no systemlibpg_query, the identical build correctly logsnot found, will download and build->Downloading libpg_query 18-latest.Because libpg_query's protobuf AST field numbers differ between PG majors (
SelectStmtis field 136 in PG17, 138 in PG18) andflow-php/postgresql's generated message classes are version-locked, a mismatched build mis-decodes every parse: aSELECT NULL::texttree comes back stampedversion: 170007with the statement node decoded asUpdateStmt, sogetSelectStmt()is null andColumnTypeParserthrowsExpected SelectStmt node, got unknown.This was very hard to diagnose:
php --ri pg_queryreportslibpg_query version => 18.0.0even on the PG17-linked build (a hardcoded constant, decoupled from the bundled sources), and the extension version,protobuf-c, and package commits all matched between a working and a broken machine. The only reliable tells were the parse-outputversionfield (170007 vs 180004) andstrings pg_query.so | grep postgres-1(postgres-17 vs postgres-18).Expected: the extension should build against the version it targets (
LIBPG_QUERY_VERSION), not silently adopt an incompatible system library.Root cause in
config.m4:Suggested fixes
Don't auto-search system paths by default. Only use a system
libpg_querywhen--with-pg-query=DIRis passed explicitly; otherwise always download/build the pinnedLIBPG_QUERY_VERSION. Matches the "optional" wording in the--with-pg-queryhelp and what CI/Docker already get (no system lib, so they always download).Verify the version of whatever lib is used (found via search or passed via
--with-pg-query). The PG major is a macro in the headerconfig.m4already requires (pg_query.h:#define PG_MAJORVERSION "17"), so a few lines of sed reject a mismatch and fall through to download:Major-version granularity is correct - field numbers shift at major boundaries, not minor releases.
ColumnTypeParser::parse()throwsunexpectedNodeType('SelectStmt', 'unknown')with a hardcoded'unknown', so it reports neither the node actually decoded nor the version skew that almost always causes it.Node::getNode()returns the real oneof case andParseResult::getVersion()the libpg_queryPG_VERSION_NUM:A message like
Expected SelectStmt, got "update_stmt" (parse result reports PostgreSQL 170007); the pg_query extension was likely built against a different PostgreSQL major than this package expectswould turn a multi-hour investigation into a one-line diagnosis.How to reproduce?
Playground snippet
N/A - this is a native-extension build/linking issue and cannot be reproduced on the Playground.
Data required to reproduce bug locally
No data needed - reproduction depends only on the build environment: a
libpg_queryfor a PostgreSQL major different fromLIBPG_QUERY_VERSIONpresent under a configure search prefix (/usr/local /usr /opt/local /opt/homebrew) at extension build time. On macOS,brew install libpg_query(currently a PG17 keg) before building the extension is enough to trigger it.Version
flow-php/postgresqlandflow-php/pg-query-ext:1.x-dev(postgresql @995926b).config.m4LIBPG_QUERY_VERSION="18-latest"on current1.x. Toolchain: PHP 8.4, pie 1.4.6, macOS arm64.Relevant error output