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

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions