Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions documentation/contributing/c.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ Build the extension (first build downloads libpg_query automatically):
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` is only used when its `PG_MAJORVERSION` matches the pinned `LIBPG_QUERY_VERSION`
in `config.m4`. A mismatched `--with-pg-query=DIR` is a hard configure error; a mismatched
auto-discovered library (e.g. a Homebrew keg for a different PostgreSQL major) is skipped in favor of
downloading the pinned version. This prevents silently baking the wrong PostgreSQL grammar into
`pg_query.so` ([#2483](https://github.com/flow-php/flow/issues/2483)).

Run PHPT tests:

```bash
Expand Down
38 changes: 28 additions & 10 deletions src/extension/pg-query-ext/ext/config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,47 @@ if test "$PHP_PG_QUERY" != "no"; then

PG_QUERY_DIR=""

dnl Expected PostgreSQL major derived from the pinned version ("18-latest" -> "18", "18.0.0" -> "18")
PG_EXPECTED_MAJOR=`echo "$LIBPG_QUERY_VERSION" | sed -E 's/^([[0-9]]+).*/\1/'`

dnl Search for existing libpg_query installation
if test "$PHP_PG_QUERY" != "yes" && test -n "$PHP_PG_QUERY"; then
SEARCH_PATH="$PHP_PG_QUERY"
PG_QUERY_EXPLICIT="yes"
else
SEARCH_PATH="/usr/local /usr /opt/local /opt/homebrew"
PG_QUERY_EXPLICIT="no"
fi

AC_MSG_CHECKING([for libpg_query])

for i in $SEARCH_PATH ; do
dnl Check flat directory structure (headers and lib in same dir)
dnl Locate headers + static lib in either flat or include/lib layout
_hdr=""; _inc=""; _lib=""
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])
break
_hdr="$i/pg_query.h"; _inc="$i"; _lib="$i"
elif test -r "$i/include/pg_query.h" && test -r "$i/include/postgres_deparse.h" && test -r "$i/lib/libpg_query.a"; then
_hdr="$i/include/pg_query.h"; _inc="$i/include"; _lib="$i/lib"
fi

if test -z "$_hdr"; then
continue
fi
dnl Check standard include/lib directory structure
if test -r "$i/include/pg_query.h" && test -r "$i/include/postgres_deparse.h" && test -r "$i/lib/libpg_query.a"; then
PG_QUERY_DIR=$i
PG_QUERY_INCLUDE_DIR="$i/include"
PG_QUERY_LIB_DIR="$i/lib"
AC_MSG_RESULT([found in $i])

dnl PG major is a macro in the header we already require: #define PG_MAJORVERSION "18"
_found_major=`sed -nE 's/^#define[[:space:]]+PG_MAJORVERSION[[:space:]]+"([0-9]+)".*/\1/p' "$_hdr"`

if test "x$_found_major" = "x$PG_EXPECTED_MAJOR"; then
PG_QUERY_DIR="$i"
PG_QUERY_INCLUDE_DIR="$_inc"
PG_QUERY_LIB_DIR="$_lib"
AC_MSG_RESULT([found in $i (PostgreSQL $_found_major)])
break
elif test "$PG_QUERY_EXPLICIT" = "yes"; then
AC_MSG_RESULT([version mismatch])
AC_MSG_ERROR([libpg_query in $i is for PostgreSQL ${_found_major:-unknown}, but this extension targets PostgreSQL $PG_EXPECTED_MAJOR (libpg_query $LIBPG_QUERY_VERSION). Point --with-pg-query at a matching install, or omit it to download the pinned version.])
else
AC_MSG_WARN([ignoring libpg_query in $i: PostgreSQL ${_found_major:-unknown}, need $PG_EXPECTED_MAJOR])
fi
done

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,15 @@ public function parse(string $typeName): ColumnType
throw InvalidAstException::invalidFieldValue('stmts', 'ParseResult', 'expected at least one statement');
}

$selectStmt = $stmts[0]->getStmt()?->getSelectStmt();
$stmt = $stmts[0]->getStmt();
$selectStmt = $stmt?->getSelectStmt();

if ($selectStmt === null) {
throw InvalidAstException::unexpectedNodeType('SelectStmt', 'unknown');
throw InvalidAstException::unexpectedNodeType(
'SelectStmt',
$stmt?->getNode() ?: 'unknown',
$parsed->raw()->getVersion(),
);
}

$targetList = $selectStmt->getTargetList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,15 @@ public function parse(string $expression): Node
throw InvalidAstException::invalidFieldValue('stmts', 'ParseResult', 'expected at least one statement');
}

$selectStmt = $stmts[0]->getStmt()?->getSelectStmt();
$stmt = $stmts[0]->getStmt();
$selectStmt = $stmt?->getSelectStmt();

if ($selectStmt === null) {
throw InvalidAstException::unexpectedNodeType('SelectStmt', 'unknown');
throw InvalidAstException::unexpectedNodeType(
'SelectStmt',
$stmt?->getNode() ?: 'unknown',
$parsed->raw()->getVersion(),
);
}

$targetList = $selectStmt->getTargetList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,17 @@ public static function missingRequiredField(string $field, string $nodeType): se
return new self(sprintf('Missing required field "%s" in %s node', $field, $nodeType));
}

public static function unexpectedNodeType(string $expected, string $actual): self
public static function unexpectedNodeType(string $expected, string $actual, ?int $version = null): self
{
if ($version !== null) {
return new self(sprintf(
'Expected %s node, got "%s" (parse result reports PostgreSQL %d); the pg_query extension was likely built against a different PostgreSQL major than this package expects',
$expected,
$actual,
$version,
));
}

return new self(sprintf('Expected %s node, got %s', $expected, $actual));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Flow\PostgreSql\Tests\Unit\QueryBuilder\Exception;

use Flow\PostgreSql\QueryBuilder\Exception\InvalidAstException;
use PHPUnit\Framework\TestCase;

final class InvalidAstExceptionTest extends TestCase
{
public function test_invalid_field_value(): void
{
static::assertSame(
'Invalid value for field "stmts" in ParseResult node: expected at least one statement',
InvalidAstException::invalidFieldValue(
'stmts',
'ParseResult',
'expected at least one statement',
)->getMessage(),
);
}

public function test_missing_required_field(): void
{
static::assertSame(
'Missing required field "val" in ResTarget node',
InvalidAstException::missingRequiredField('val', 'ResTarget')->getMessage(),
);
}

public function test_unexpected_node_type_with_version(): void
{
$message = InvalidAstException::unexpectedNodeType('SelectStmt', 'update_stmt', 170007)->getMessage();

static::assertStringContainsString('Expected SelectStmt node', $message);
static::assertStringContainsString('"update_stmt"', $message);
static::assertStringContainsString('170007', $message);
static::assertStringContainsString('different PostgreSQL major', $message);
}

public function test_unexpected_node_type_without_version(): void
{
static::assertSame(
'Expected SelectStmt node, got unknown',
InvalidAstException::unexpectedNodeType('SelectStmt', 'unknown')->getMessage(),
);
}
}
Loading