Skip to content

[Bug]: pg-query-ext build silently links a system libpg_query of the wrong PostgreSQL version #2483

Description

@bendavies

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

  1. 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).

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

  1. 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":{...}}}]}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions