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
41 changes: 33 additions & 8 deletions .github/workflows/job-pg-query-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ on:
jobs:
build:
name: Build and Test Extension
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'macos-latest']
php: ['8.3', '8.4', '8.5']

steps:
Expand All @@ -27,11 +28,17 @@ jobs:
extensions: ':psr, bcmath, dom, hash, json, mbstring, xml, xmlwriter, xmlreader, zlib, protobuf'
tools: 'composer:v2, phpize, php-config'

- name: Install build dependencies
- name: Install build dependencies (Ubuntu)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y build-essential autoconf automake libtool protobuf-compiler libprotobuf-c-dev

- name: Install build dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install autoconf automake libtool protobuf protobuf-c

- name: Build libpg_query and extension
working-directory: src/extension/pg-query-ext
run: |
Expand All @@ -52,7 +59,11 @@ jobs:

pie-install-test:
name: Test PIE Installation
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'macos-latest']

steps:
- uses: actions/checkout@v5
Expand All @@ -65,20 +76,34 @@ jobs:
extensions: ':psr, bcmath, dom, hash, json, mbstring, xml, xmlwriter, xmlreader, zlib'
tools: 'composer:v2, phpize, php-config'

- name: Install build dependencies
- name: Install build dependencies (Ubuntu)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y build-essential autoconf automake libtool protobuf-compiler libprotobuf-c-dev

- name: Install build dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install autoconf automake libtool protobuf protobuf-c

- name: Install PIE
run: |
curl -L -o /usr/local/bin/pie https://github.com/php/pie/releases/latest/download/pie.phar
chmod +x /usr/local/bin/pie
sudo curl -L -o /usr/local/bin/pie https://github.com/php/pie/releases/latest/download/pie.phar
sudo chmod +x /usr/local/bin/pie
pie --version

- name: Install extension via PIE
- name: Prepare local package for PIE installation
working-directory: src/extension/pg-query-ext
run: |
jq '. + {"version": "0.0.9999"}' composer.json > composer.tmp.json
mv composer.tmp.json composer.json

- name: Install extension via PIE (from local)
run: |
sudo pie install flow-php/pg-query-ext:1.x-dev
sudo pie repository:remove packagist.org
sudo pie repository:add path ${{ github.workspace }}/src/extension/pg-query-ext
sudo pie install flow-php/pg-query-ext:0.0.9999@dev

- name: Verify extension is loaded
run: |
Expand Down
6 changes: 3 additions & 3 deletions .nix/pkgs/php-pg-query-ext/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
let
libpg_query = stdenv.mkDerivation {
pname = "libpg_query";
version = "17-latest";
version = "17-6.2.1";

src = fetchFromGitHub {
owner = "pganalyze";
repo = "libpg_query";
rev = "03e2f436c999a1d22dbce439573e8cfabced5720"; # 17-latest branch as of 2025-11-28
hash = "sha256-0fnQF4KSIVpNqxzdvS0UtHnqUmLXgBKI/XRZjNrYLSo=";
rev = "b2217bfeac36b09eb053a65a315878586723df08"; # 17-6.2.1 tag
hash = "sha256-+7JR5rup+9ie6wUaU5cuTyVhaEkH7X1eC7kYn0NNVrc=";
};

buildPhase = ''
Expand Down
42 changes: 24 additions & 18 deletions documentation/components/extensions/pg-query-ext.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface with strongly-typed AST nodes, see the [pg-query library](/documentati
- Split multiple SQL statements
- Scan SQL into tokens
- Generate query summaries for logging/monitoring
- Check if queries contain utility/DDL statements (without full parsing)

## Requirements

Expand All @@ -35,11 +36,8 @@ interface with strongly-typed AST nodes, see the [pg-query library](/documentati
[PIE](https://github.com/php/pie) is the modern PHP extension installer.

```bash
# Simple installation (auto-downloads libpg_query for PostgreSQL 17)
# Simple installation
pie install flow-php/pg-query-ext

# Install with a specific PostgreSQL grammar version (15, 16, or 17)
pie install flow-php/pg-query-ext --with-pg-version=16
```

The extension will automatically download and build the appropriate libpg_query version. Build dependencies (
Expand All @@ -49,7 +47,7 @@ The extension will automatically download and build the appropriate libpg_query

| PostgreSQL | libpg_query version |
|------------|---------------------|
| 17 | 17-6.1.0 (default) |
| 17 | 17-6.2.1 (default) |
| 16 | 16-5.2.0 |
| 15 | 15-4.2.4 |

Expand Down Expand Up @@ -127,23 +125,31 @@ $sql = pg_query_deparse_opts(

// Generate query summary (protobuf format, useful for logging)
$summary = pg_query_summary('SELECT * FROM users WHERE id = 1');

// Check if query contains utility statements (DDL) - fast, without full parsing
$isUtility = pg_query_is_utility_stmt('CREATE TABLE users (id int)');
// Returns: true

$isUtility = pg_query_is_utility_stmt('SELECT * FROM users');
// Returns: false
```

## Functions Reference

| Function | Description | Returns |
|--------------------------------------------------------------|-----------------------------------|---------------------|
| `pg_query_parse(string $sql)` | Parse SQL to JSON AST | `string` (JSON) |
| `pg_query_parse_protobuf(string $sql)` | Parse SQL to protobuf AST | `string` (protobuf) |
| `pg_query_fingerprint(string $sql)` | Generate query fingerprint | `string\|false` |
| `pg_query_normalize(string $sql)` | Normalize query with placeholders | `string\|false` |
| `pg_query_normalize_utility(string $sql)` | Normalize DDL/utility statements | `string\|false` |
| `pg_query_parse_plpgsql(string $sql)` | Parse PL/pgSQL function | `string` (JSON) |
| `pg_query_split(string $sql)` | Split multiple statements | `array<string>` |
| `pg_query_scan(string $sql)` | Scan SQL into tokens | `string` (protobuf) |
| `pg_query_deparse(string $protobuf)` | Convert protobuf AST back to SQL | `string` |
| `pg_query_deparse_opts(...)` | Deparse with formatting options | `string` |
| `pg_query_summary(string $sql, int $options, int $truncate)` | Generate query summary | `string` (protobuf) |
| Function | Description | Returns |
|--------------------------------------------------------------|------------------------------------------------|---------------------|
| `pg_query_parse(string $sql)` | Parse SQL to JSON AST | `string` (JSON) |
| `pg_query_parse_protobuf(string $sql)` | Parse SQL to protobuf AST | `string` (protobuf) |
| `pg_query_fingerprint(string $sql)` | Generate query fingerprint | `string\|false` |
| `pg_query_normalize(string $sql)` | Normalize query with placeholders | `string\|false` |
| `pg_query_normalize_utility(string $sql)` | Normalize DDL/utility statements | `string\|false` |
| `pg_query_parse_plpgsql(string $sql)` | Parse PL/pgSQL function | `string` (JSON) |
| `pg_query_split(string $sql)` | Split multiple statements | `array<string>` |
| `pg_query_scan(string $sql)` | Scan SQL into tokens | `string` (protobuf) |
| `pg_query_deparse(string $protobuf)` | Convert protobuf AST back to SQL | `string` |
| `pg_query_deparse_opts(...)` | Deparse with formatting options | `string` |
| `pg_query_summary(string $sql, int $options, int $truncate)` | Generate query summary | `string` (protobuf) |
| `pg_query_is_utility_stmt(string $sql)` | Check if query contains utility/DDL statements | `bool` |

### pg_query_deparse_opts Parameters

Expand Down
57 changes: 55 additions & 2 deletions src/extension/pg-query-ext/ext/config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,66 @@ if test "$PHP_PG_QUERY" != "no"; then
AC_MSG_ERROR([libpg_query.a not found in $PG_QUERY_LIB_DIR])
fi

dnl protobuf-c is bundled in libpg_query.a for static builds
dnl protobuf-c is required for shared builds (libpg_query.a needs it)
if test "$ext_shared" = "yes"; then
PHP_ADD_LIBRARY(protobuf-c,, PG_QUERY_SHARED_LIBADD)
AC_MSG_CHECKING([for protobuf-c])

dnl Try pkg-config first (the standard way to find libraries)
if test -z "$PKG_CONFIG"; then
AC_PATH_PROG(PKG_CONFIG, pkg-config, no)
fi

if test "$PKG_CONFIG" != "no" && $PKG_CONFIG --exists libprotobuf-c 2>/dev/null; then
PROTOBUF_C_LIBS=$($PKG_CONFIG --libs libprotobuf-c)
PROTOBUF_C_LIBDIR=$($PKG_CONFIG --variable=libdir libprotobuf-c)
AC_MSG_RESULT([found via pkg-config])

if test -n "$PROTOBUF_C_LIBDIR"; then
PHP_ADD_LIBPATH($PROTOBUF_C_LIBDIR, PG_QUERY_SHARED_LIBADD)
fi
PHP_ADD_LIBRARY(protobuf-c,, PG_QUERY_SHARED_LIBADD)
else
dnl Fallback: search common paths (for systems without pkg-config)
PROTOBUF_C_SEARCH_PATHS="/opt/homebrew /usr/local /usr"
PROTOBUF_C_FOUND=""

for i in $PROTOBUF_C_SEARCH_PATHS; do
if test -r "$i/lib/libprotobuf-c.dylib" || test -r "$i/lib/libprotobuf-c.so"; then
PROTOBUF_C_FOUND=$i
break
fi
done

if test -n "$PROTOBUF_C_FOUND"; then
AC_MSG_RESULT([found in $PROTOBUF_C_FOUND])
PHP_ADD_LIBPATH($PROTOBUF_C_FOUND/lib, PG_QUERY_SHARED_LIBADD)
PHP_ADD_LIBRARY(protobuf-c,, PG_QUERY_SHARED_LIBADD)
else
AC_MSG_RESULT([not found, assuming system default])
PHP_ADD_LIBRARY(protobuf-c,, PG_QUERY_SHARED_LIBADD)
fi
fi
fi

PHP_SUBST(PG_QUERY_SHARED_LIBADD)

dnl Define extension
PHP_NEW_EXTENSION(pg_query, pg_query.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)

dnl macOS libtool fix for flat namespace issue
dnl libpg_query.a bundles its own copy of protobuf-c. On macOS, libtool defaults to
dnl -flat_namespace which pools all symbols together. If system protobuf-c is also loaded
dnl (e.g., via grpc extension), symbol conflicts cause segfaults. This fix keeps the
dnl two-level namespace so bundled symbols stay isolated.
dnl See: https://bugs.php.net/80393, https://github.com/protocolbuffers/protobuf/issues/7611
case $host_os in
darwin*)
AC_CONFIG_COMMANDS([libtool-macos-fix], [
if test -f libtool; then
sed -i.bak 's/.*flat_namespace.*suppress.*/allow_undefined_flag="-undefined dynamic_lookup"/' libtool
rm -f libtool.bak
fi
])
;;
esac
fi
30 changes: 29 additions & 1 deletion src/extension/pg-query-ext/ext/pg_query.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ PHP_MINFO_FUNCTION(pg_query)
php_info_print_table_start();
php_info_print_table_header(2, "pg_query support", "enabled");
php_info_print_table_row(2, "Version", PHP_PG_QUERY_VERSION);
php_info_print_table_row(2, "libpg_query version", "17-6.1.0");
php_info_print_table_row(2, "libpg_query version", "17-6.2.1");
php_info_print_table_end();
}

Expand Down Expand Up @@ -488,3 +488,31 @@ PHP_FUNCTION(pg_query_summary)

RETURN_STR(protobuf_data);
}

PHP_FUNCTION(pg_query_is_utility_stmt)
{
char *sql;
size_t sql_len;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(sql, sql_len)
ZEND_PARSE_PARAMETERS_END();

PgQueryIsUtilityResult result = pg_query_is_utility_stmt(sql);

if (result.error) {
pg_query_free_is_utility_result(result);
RETURN_FALSE;
}

bool has_utility = false;
for (int i = 0; i < result.length; i++) {
if (result.items[i]) {
has_utility = true;
break;
}
}

pg_query_free_is_utility_result(result);
RETURN_BOOL(has_utility);
}
12 changes: 12 additions & 0 deletions src/extension/pg-query-ext/ext/pg_query.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,15 @@ function pg_query_deparse_opts(
function pg_query_summary(string $sql, int $options = 0, int $truncate_limit = 0) : string
{
}

/**
* Check if query contains utility statements (DDL like CREATE, ALTER, DROP)
* without full parsing. More efficient than full parse when only checking statement type.
*
* @param string $sql The SQL query to check
*
* @return bool True if the query contains utility statements, false otherwise
*/
function pg_query_is_utility_stmt(string $sql) : bool
{
}
6 changes: 6 additions & 0 deletions src/extension/pg-query-ext/ext/pg_query_arginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pg_query_summary, 0, 1, IS_STRIN
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, truncate_limit, IS_LONG, 0, "0")
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pg_query_is_utility_stmt, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, sql, IS_STRING, 0)
ZEND_END_ARG_INFO()

ZEND_FUNCTION(pg_query_parse);
ZEND_FUNCTION(pg_query_parse_protobuf);
ZEND_FUNCTION(pg_query_fingerprint);
Expand All @@ -61,6 +65,7 @@ ZEND_FUNCTION(pg_query_scan);
ZEND_FUNCTION(pg_query_deparse);
ZEND_FUNCTION(pg_query_deparse_opts);
ZEND_FUNCTION(pg_query_summary);
ZEND_FUNCTION(pg_query_is_utility_stmt);

static const zend_function_entry ext_functions[] = {
ZEND_FE(pg_query_parse, arginfo_pg_query_parse)
Expand All @@ -74,5 +79,6 @@ static const zend_function_entry ext_functions[] = {
ZEND_FE(pg_query_deparse, arginfo_pg_query_deparse)
ZEND_FE(pg_query_deparse_opts, arginfo_pg_query_deparse_opts)
ZEND_FE(pg_query_summary, arginfo_pg_query_summary)
ZEND_FE(pg_query_is_utility_stmt, arginfo_pg_query_is_utility_stmt)
ZEND_FE_END
};
27 changes: 27 additions & 0 deletions src/extension/pg-query-ext/tests/phpt/011_is_utility_stmt.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
pg_query_is_utility_stmt() functionality
--SKIPIF--
<?php if (!extension_loaded("pg_query")) die("skip pg_query extension not loaded"); ?>
--FILE--
<?php
// DML statements should return false
var_dump(pg_query_is_utility_stmt('SELECT * FROM users'));
var_dump(pg_query_is_utility_stmt('INSERT INTO users VALUES (1)'));
var_dump(pg_query_is_utility_stmt('UPDATE users SET name = $1'));
var_dump(pg_query_is_utility_stmt('DELETE FROM users WHERE id = 1'));

// DDL/utility statements should return true
var_dump(pg_query_is_utility_stmt('CREATE TABLE users (id int)'));
var_dump(pg_query_is_utility_stmt('ALTER TABLE users ADD COLUMN name text'));
var_dump(pg_query_is_utility_stmt('DROP TABLE users'));
var_dump(pg_query_is_utility_stmt('CREATE INDEX idx ON users (id)'));
?>
--EXPECT--
bool(false)
bool(false)
bool(false)
bool(false)
bool(true)
bool(true)
bool(true)
bool(true)
Loading