diff --git a/.github/workflows/job-pg-query-extension.yml b/.github/workflows/job-pg-query-extension.yml index 37a5d7641..f8261818a 100644 --- a/.github/workflows/job-pg-query-extension.yml +++ b/.github/workflows/job-pg-query-extension.yml @@ -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: @@ -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: | @@ -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 @@ -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: | diff --git a/.nix/pkgs/php-pg-query-ext/package.nix b/.nix/pkgs/php-pg-query-ext/package.nix index a3efac4d0..d98b9f83c 100644 --- a/.nix/pkgs/php-pg-query-ext/package.nix +++ b/.nix/pkgs/php-pg-query-ext/package.nix @@ -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 = '' diff --git a/documentation/components/extensions/pg-query-ext.md b/documentation/components/extensions/pg-query-ext.md index 05a277df6..738f8478f 100644 --- a/documentation/components/extensions/pg-query-ext.md +++ b/documentation/components/extensions/pg-query-ext.md @@ -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 @@ -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 ( @@ -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 | @@ -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` | -| `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` | +| `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 diff --git a/src/extension/pg-query-ext/ext/config.m4 b/src/extension/pg-query-ext/ext/config.m4 index 581bfbcc3..524268329 100644 --- a/src/extension/pg-query-ext/ext/config.m4 +++ b/src/extension/pg-query-ext/ext/config.m4 @@ -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 diff --git a/src/extension/pg-query-ext/ext/pg_query.c b/src/extension/pg-query-ext/ext/pg_query.c index cb8c08225..0cc8bbc46 100644 --- a/src/extension/pg-query-ext/ext/pg_query.c +++ b/src/extension/pg-query-ext/ext/pg_query.c @@ -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(); } @@ -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); +} diff --git a/src/extension/pg-query-ext/ext/pg_query.stub.php b/src/extension/pg-query-ext/ext/pg_query.stub.php index bf46795e1..babea4b8e 100644 --- a/src/extension/pg-query-ext/ext/pg_query.stub.php +++ b/src/extension/pg-query-ext/ext/pg_query.stub.php @@ -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 +{ +} diff --git a/src/extension/pg-query-ext/ext/pg_query_arginfo.h b/src/extension/pg-query-ext/ext/pg_query_arginfo.h index c64ca4836..c937c7358 100644 --- a/src/extension/pg-query-ext/ext/pg_query_arginfo.h +++ b/src/extension/pg-query-ext/ext/pg_query_arginfo.h @@ -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); @@ -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) @@ -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 }; diff --git a/src/extension/pg-query-ext/tests/phpt/011_is_utility_stmt.phpt b/src/extension/pg-query-ext/tests/phpt/011_is_utility_stmt.phpt new file mode 100644 index 000000000..c879df6e9 --- /dev/null +++ b/src/extension/pg-query-ext/tests/phpt/011_is_utility_stmt.phpt @@ -0,0 +1,27 @@ +--TEST-- +pg_query_is_utility_stmt() functionality +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true)