Check Playground web runtime compatibility#417
Merged
Conversation
…313) Running `composer install` fails with the following error: ``` Loading composer repositories with package information Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires phpunit/phpunit 8.5.38 (exact version match: 8.5.38 or 8.5.38.0), found phpunit/phpunit[8.5.38] but these were not loaded, because they are affected by security advisories ("PKSA-z3gr-8qht-p93v"). Go to https://packagist.org/security-advisories/ to find advisory details. To ignore the advisories, add them to the audit "ignore" config. To turn the feature off entirely, you can set "block-insecure" to false in your "audit" config. Problem 2 - Root composer.json requires yoast/phpunit-polyfills 2.0.0 -> satisfiable by yoast/phpunit-polyfills[2.0.0]. - yoast/phpunit-polyfills 2.0.0 requires phpunit/phpunit ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 -> found phpunit/phpunit[5.7.21, ..., 5.7.27, 6.0.0, ..., 6.5.14, 7.0.0, ..., 7.5.20, 8.0.0, ..., 8.5.52, 9.0.0, ..., 9.6.34, 10.0.0, ..., 10.5.63] but these were not loaded, because they are affected by security advisories ("PKSA-z3gr-8qht-p93v"). Go to https://packagist.org/security-advisories/ to find advisory details. To ignore the advisories, add them to the audit "ignore" config. To turn the feature off entirely, you can set "block-insecure" to false in your "audit" config. ``` This is due to a security advisory GHSA-vvj3-c3rp-c85p. It only affects PHPUnit, updating it is an easy fix.
…migrator (#312) When migrating from the legacy SQLite driver to the new AST-based driver, the information schema reconstructor reads MySQL column type metadata from the `_mysql_data_types_cache` table. Some older versions of the legacy driver stored invalid type definitions in this table, causing migration failures. This PR detects and handles two types of invalid data: **1. Truncated decimal definitions** — Before [PR #126](#126), columns with multiple type arguments like `decimal(26, 8)` were incorrectly stored as `decimal(26,` (missing the second argument and closing parenthesis). For WooCommerce tables, the fix restores the correct decimal precision based on known WooCommerce column patterns. For other tables, these invalid definitions are discarded and the column type is inferred from SQLite. **2. Index definitions mistaken for columns** — Before [commit b5a9fba](b5a9fba), index definitions like `KEY timestamp (timestamp)` were incorrectly parsed and stored as column `KEY` with type `timestamp(timestamp)`. The fix validates that type arguments are numeric, rejecting these malformed entries and falling back to the SQLite type inference. Fixes WordPress/wordpress-playground#3050.
… `NULL` bytes (#314) Fixes `SELECT` queries that have string literals with `NULL` bytes in them and no column alias. E.g.: ```sql SELECT `a<null-byte>b`; -- result column name should be: 'a' ``` This is because in MySQL: - There can’t be `\0` bytes in identifiers (like column names). The lexer will reject that. - But string literals can have them. - And there is a way to make a string literal “identifier-like” (implicit alias = the result “column name”): `SELECT 'abc\0xyz'` The result column name will be: `abc`
…315) This PR implements two additional PDO statement proxy methods: - `PDOStatement::fetchColumn()` - `PDOStatement::fetchObject()` Both of these methods delegate the calls to the underlying SQLite statement. There are more PDO APIs to cover, but I had these in an in-progress branch, and there is no need to hold it off. Additional PDO API coverage will be done in subsequent PRs.
## Summary This PR adds configuration for AI coding agents and a development container: - **AI agent guidance:** Add `AGENTS.md` with project architecture overview, commands, coding conventions, security and compatibility notes, and git practices. `CLAUDE.md` references it. - **Claude Code settings:** Add `.claude/settings.json` with pre-approved permissions for common development commands. - **Devcontainer:** Add root-level `.devcontainer/` with PHP 8.4, pdo_sqlite, Node.js 20, and Docker-in-Docker — suitable for developing the SQLite driver and running all test suites.
Improve escaping consistency in the legacy SQLite translator: - Use `quote_identifier()` for all identifier interpolations (table names, column names, index names, trigger names) in SQL queries, token values, and DDL reconstruction output. - Use parameterized queries and `PDO::quote()` for all string literals processed by the translator. See added tests for more details.
These functions allow encoding and decoding base64 data in SQL queries, matching the behavior of their MySQL counterparts. They are registered as SQLite user-defined functions backed by PHP's base64_encode() and base64_decode(), so both the legacy and AST-based drivers can use them. FROM_BASE64() returns NULL for NULL or invalid base64 input. TO_BASE64() returns NULL for NULL input.
## Summary This adds support for MySQL's `FROM_BASE64()` and `TO_BASE64()` functions, allowing SQL queries to encode and decode base64 data. Both functions are implemented as SQLite user-defined functions backed by PHP's `base64_encode()` and `base64_decode()`. Because they're registered through `WP_SQLite_PDO_User_Defined_Functions::register_for()`, they work with both the legacy and AST-based drivers automatically. `FROM_BASE64()` returns `NULL` for `NULL` or invalid base64 input. `TO_BASE64()` returns `NULL` for `NULL` input. ## Test plan - Run `composer run test -- --filter 'testFromBase64|testToBase64'` - Verify basic encoding/decoding, NULL handling, empty string handling, and round-trip correctness
…ws (#325) ### Summary Fixes #322 WP_SQLite_Driver::query() returned null instead of 0 for statements affecting zero rows (e.g., DELETE FROM x WHERE id = 1 when no matching row exists). This caused wpdb::query() to also return null, breaking its documented contract of returning int|bool. ### Problem In WP_SQLite_Driver::query(), the return logic for non-SELECT queries was: 1. rowCount() > 0 - return row count 2. Otherwise - return null This meant any DELETE, UPDATE, INSERT statement with 0 affected rows returned null instead of 0. ### Fix Simplified the logic to always return rowCount() for non-SELECT statements. DELETE / UPDATE / INSERT will now correctly returns 0 instead of null when no rows affected. CREATE / ALTER / DROP / TRUNCATE now returns 0 instead of null, but wp-includes/sqlite/class-wp-sqlite-db.php never reads this value for these queries (it returns true unconditionally at line 448-449) ### Testing - Added testDeleteReturnsZeroAffectedRowsWhenNoMatchingRows - verifies DELETE returns 0 when no rows match. - Added testUpdateReturnsZeroAffectedRowsWhenNoMatchingRows - verifies UPDATE returns 0 when no rows match. - Updated existing test assertions from assertNull to assertSame( 0, ... ) to reflect the corrected behaviour - Full test suite passes (777 tests)
… off
MySQL's behavior around zero dates ('0000-00-00') and dates with zero
parts ('2020-00-15') depends on two SQL modes: NO_ZERO_DATE and
NO_ZERO_IN_DATE. Both are enabled by default in MySQL 8.0, but
applications can disable them.
Previously, the SQLite driver always rejected zero dates through
SQLite's DATE()/DATETIME() functions, which return NULL for such
values. This caused incorrect errors when strict mode was on but
the zero-date modes were off.
Now the CASE expression in cast_value_for_saving() checks the active
SQL modes and inserts additional WHEN clauses to accept zero dates
when appropriate. The translate_datetime_literal() method also
preserves dates with zero month/day parts when NO_ZERO_IN_DATE is off,
instead of truncating them via strtotime().
Instead of conditionally building $zero_date_whens in PHP, the WHEN clauses are always present in the CASE statement with the SQL mode check embedded as a literal boolean (AND NOT 0/1). This keeps the generated SQL structure consistent regardless of active modes.
One argument per line in the sprintf call, align equals signs, and use single quotes for CREATE TABLE string in tests.
Replace sprintf with strtr so each substituted value appears once in the argument list and the template reads like annotated SQL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, not the raw value
The strtr refactor accidentally changed `WHEN $function_call > '0'`
to `WHEN {value} > '0'`, comparing the wrong operand and breaking
date validation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that UPDATE statements produce errors for zero dates and zero-in-dates when strict mode is combined with NO_ZERO_DATE or NO_ZERO_IN_DATE, matching the existing INSERT rejection tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add tests verifying that stored zero dates and zero-in-dates can be selected, compared, ordered, and filtered – matching MySQL behavior. Fix YEAR(), MONTH(), and DAY() functions to return 0 for zero date parts. Previously, strtotime() couldn't parse dates like '0000-00-00' or '2020-00-15', producing wrong results. Now the date parts are extracted directly from the string when possible. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The PHP manual references for format specifiers used by gmdate() were accidentally removed when adding zero-date handling. These comments document why specific format characters (n, Y, j) are used in the fallback path and should be preserved.
Add ^ anchors to the YEAR/MONTH/DAY regex patterns so they only match date strings starting at the beginning of the input, not arbitrary substrings.
## Summary MySQL controls zero-date acceptance through two SQL modes: `NO_ZERO_DATE` and `NO_ZERO_IN_DATE`. Both are on by default, but applications can turn them off to allow values like `'0000-00-00 00:00:00'` or `'2020-00-15'`. The SQLite driver wasn't honoring these modes – SQLite's `DATE()`/`DATETIME()` functions return NULL for zero dates, so they always fell through to the strict-mode error, even when the zero-date modes were disabled. This PR adds zero-date acceptance checks to `cast_value_for_saving()` that inspect the active SQL modes before rejecting a date. When `NO_ZERO_DATE` is off (or on without strict mode), all-zero dates pass through. When `NO_ZERO_IN_DATE` is off, dates with zero month/day parts like `'2020-00-15'` are accepted too. The `translate_datetime_literal()` method is also updated to preserve zero-part dates instead of truncating them via `strtotime()`. The behavior matrix now matches MySQL's documentation: | Mode combination | `'0000-00-00'` | `'2020-00-15'` | |---|---|---| | Both modes off | Accepted | Accepted | | Mode on, non-strict | Accepted (warning in MySQL) | Stored as `'0000-00-00'` | | Mode on + strict | Error | Error | ## Test plan - [x] 12 new PHPUnit tests covering all combinations of NO_ZERO_DATE × NO_ZERO_IN_DATE × strict mode - [x] Full test suite passes (772 tests, 0 failures) - [ ] Review that WordPress core's `test_insert_empty_post_date` still works with default modes
…329) This PR adds the `3948349` integer as the [SQLite Application ID](https://sqlite.org/fileformat.html#application_id) that is standard procedure to take when a SQLite database is used as an application file format[^1]. A new consistent file extension has been chosen. 1. `PRAGMA application_id = SQLITE_DB_APPLICATION_ID` is set for new and old databases 2. New databases are created with the new file extension 3. Legacy files are renamed for backward compatibility [^1]: https://sqlite.org/appfileformat.html
## What I propose adding a `file_exists()` check before calling `is_plugin_inactive()` in the `admin_footer` action inside `db.copy`. ## Why If the SQLite plugin is inactive for some reason but the `db.php` drop-in file exists, the logic automatically activates the plugin. This works only when the SQLite plugin is installed into the `plugins/` directory and throws warnings if plugin is installed into the `mu-plugins/` directory. The new guard ensures that the plugin is installed in `plugins/` before running activation logic. ## Testing 1. Install WordPress 2. Install the SQLite plugin from the .org directory 3. Activate the plugin - confirm `db.php` drop-in is created 4. Deactivate the plugin by bypassing normal logic: ``` wp option delete active_plugins ``` 6. Confirm `db.php` still exists 7. Reload WP Admin 8. Confirm the plugin is auto-activated
## Summary Define `WP_MYSQL_ON_SQLITE_PATH` constant in `wp-pdo-mysql-on-sqlite.php` so that dependent projects can reliably determine the path to the SQLite driver loader file.
## Summary Add a small entry point in `load.php` so an optional native (e.g. Rust) MySQL lexer/parser extension can plug itself in as `WP_MySQL_Lexer` / `WP_MySQL_Parser`. The extension pre-declares `WP_MySQL_Native_Lexer` / `WP_MySQL_Native_Parser` before this file loads; otherwise the existing pure-PHP classes ship as the implementation, unchanged. The existing PHP files keep their original names, locations, and class names — no rename, no polyfill scaffolding. Native shims live in a separate `mysql/native/` directory and are only required when the native classes are present. ## Testing - `php -l` on changed files - Smoke: `WP_MySQL_Lexer` and `WP_MySQL_Parser` instantiate and parse `SELECT 1` end-to-end with no native extension loaded - `phpcs` clean
## Summary When a native MySQL parser extension declares `WP_MySQL_Native_Parser`, load a small bridge file that exposes `WP_Parser_Grammar` internals to the extension. The native parser needs the grammar's terminal table, rules, and lookahead map, but those live on a PHP object — the bridge function hands them out as a plain array the extension can consume. The bridge file lives under `mysql/native/` since it's only required when a native parser is in use. ## Testing - `php -l` on changed files - `phpcs` clean
## Summary Adds `WP_MySQL_Native_Parser_Node`, a `WP_Parser_Node` subclass the native MySQL parser extension constructs when it produces an AST. Read methods delegate into the Rust-owned AST so children aren't copied into PHP unless something walks the tree. On the first `append_child()` / `merge_fragment()`, the node copies its native children into the inherited `$children` array and behaves like a plain `WP_Parser_Node` from then on — that's what `was_mutated()` tracks. Mutation matters because `WP_PDO_MySQL_On_SQLite` rewrites parsed queries (e.g. synthetic `count(*)` expressions) by appending children to existing nodes. ## Other change `WP_Parser_Node::$children` private → protected so the subclass can populate it during materialization.
## Summary Drop the `was_mutated()` getter from `WP_MySQL_Native_Parser_Node` and read the property directly. The flag is checked on every read of a native-backed node, so a method call per access shows up on full-tree traversals. The naming still reads at the call sites (`if ( \$this->was_mutated )`), so we don't lose anything by inlining.
## What it does Adds an optional Rust PHP extension for the MySQL lexer/parser. When the extension is loaded, the existing public PHP API stays the same, but `WP_MySQL_Lexer` and `WP_MySQL_Parser` delegate lexing/parsing to native Rust code. ```bash php -d extension=/path/to/libwp_mysql_parser.so your-script.php ``` ```php require 'packages/mysql-on-sqlite/src/load.php'; $driver = new WP_PDO_MySQL_On_SQLite( 'mysql-on-sqlite:path=:memory:;dbname=wp;' ); $parser = $driver->create_parser( 'SELECT ID, post_title FROM wp_posts WHERE ID IN (1, 2, 3)' ); $parser->next_query(); $ast = $parser->get_query_ast(); echo $ast->rule_name; // query ``` Without the extension, the same code uses the existing PHP parser. ## Rationale The PHP parser is correct but expensive on large query sets. On the MySQL corpus, the native path measured against trunk's PHP implementation was: | Scenario | Trunk PHP | Native extension | | --- | ---: | ---: | | Parse only | 14.3114s / 4,862 QPS / 68.0MB | 1.1574s / 60,105 QPS / 30.0MB | | Parse + walk | 20.0804s / 3,465 QPS / 70.0MB | 2.9751s / 23,383 QPS / 48.0MB | That is about **12.37x faster** for parse-only and **6.75x faster** for parse+walk. Raw numbers are in #381 (comment). ## Implementation The extension lives in `packages/php-ext-wp-mysql-parser/` and exports `WP_MySQL_Native_Lexer`, `WP_MySQL_Native_Token_Stream`, `WP_MySQL_Native_Parser`, and `WP_MySQL_Native_Parser_Node`. The SQLite driver selects the native path only when the native lexer class is active: ```php $lexer = new WP_MySQL_Lexer( $sql ); if ( $lexer instanceof WP_MySQL_Native_Lexer ) { $tokens = $lexer->native_token_stream(); } else { $tokens = $lexer->remaining_tokens(); } ``` Native AST nodes are lazy PHP wrappers over a Rust-owned AST. Wrapper identity is stable through a per-AST cache, and Rust state is stored in a Rust-side registry keyed by the PHP wrapper object pointer. That avoids the previous PHP/Rust reference cycle while keeping repeated child reads referentially stable. The pure-PHP parser remains the fallback and `WP_MySQL_Parser` remains an `instanceof WP_Parser`. ## Testing instructions Run the PHP suite normally: ```bash cd packages/mysql-on-sqlite php ./vendor/bin/phpunit -c ./phpunit.xml.dist ``` Run it against the extension: ```bash cd packages/php-ext-wp-mysql-parser cargo build cd ../mysql-on-sqlite WP_SQLITE_REQUIRE_NATIVE_PARSER_EXTENSION=1 \ php -d extension=../php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so \ ./vendor/bin/phpunit -c ./phpunit.xml.dist ``` CI currently passes on `bde34d5` for PHP 7.2-8.5, including extension-loaded SQLite integration tests on PHP 8.0-8.5 and WordPress PHPUnit with the extension loaded.
## What it does Cleans up the native parser follow-up from #381 so the merged code reads as permanent code, not review scaffolding. It replaces duplicated inline verifier logic with one `tests/tools/verify-native-parser-extension.php` entry point for `mysql-on-sqlite`. The parser-extension workflow and PHPUnit bootstrap both call the same verifier: ```bash php -d extension=../php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so \ tests/tools/verify-native-parser-extension.php ``` It also collapses `WP_PDO_MySQL_On_SQLite::create_parser()` to one token selection and one parser reset/create return, and rewrites native parser test comments to describe behavior instead of PR review history. ## Rationale #381 landed functional native parser support, but a few follow-up surfaces still carried review-era wording and copied verifier blocks. That makes future changes harder to read and easier to drift: native parser routing, Rust AST handle storage, wrapper identity, and materialized child behavior were being checked in multiple places. The verifier now pins that runtime contract from one script: extension loaded, `WP_MySQL_Lexer` resolves native, `WP_MySQL_Parser` delegates to `WP_MySQL_Native_Parser`, the SQLite driver returns a native-backed AST, native wrapper handle properties are absent, child identity is stable, and materialized child mutations survive. ## Implementation Added `wp_sqlite_verify_native_parser_extension()` with a shared delegate check: ```php function wp_sqlite_assert_native_parser_delegate( WP_MySQL_Parser $parser, string $context ): void { $reflection = new ReflectionObject( $parser ); if ( ! $reflection->hasProperty( 'native' ) ) { wp_sqlite_native_parser_verification_fail( $context ); } $native_property = $reflection->getProperty( 'native' ); $native_property->setAccessible( true ); if ( ! ( $native_property->getValue( $parser ) instanceof WP_MySQL_Native_Parser ) ) { wp_sqlite_native_parser_verification_fail( $context ); } } ``` `WP_SQLITE_REQUIRE_NATIVE_PARSER_EXTENSION=1` in the PHPUnit bootstrap now loads that verifier instead of inlining the same checks. `create_parser()` now selects tokens once: ```php $tokens = $lexer instanceof WP_MySQL_Native_Lexer ? $lexer->native_token_stream() : $lexer->remaining_tokens(); return $this->reset_or_create_parser( $tokens ); ``` The WordPress PHPUnit extension setup keeps its container-specific verifier, but factors the repeated reflection checks into the same small helper shape. ## Testing instructions ```bash cargo fmt --check bash -n .github/workflows/wp-tests-phpunit-native-extension-setup.sh node --check .github/workflows/wp-tests-phpunit-run.js php -l packages/mysql-on-sqlite/tests/tools/verify-native-parser-extension.php php -l packages/mysql-on-sqlite/tests/bootstrap.php php ./vendor/bin/phpcs .github/workflows/wp-tests-phpunit-native-extension-setup.sh packages/mysql-on-sqlite/tests/bootstrap.php packages/mysql-on-sqlite/tests/tools/verify-native-parser-extension.php packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Native_Parser_Node_Identity_Tests.php packages/mysql-on-sqlite/tests/mysql/native/WP_MySQL_Parser_Instanceof_Tests.php cd packages/mysql-on-sqlite php -d extension=../php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so tests/tools/verify-native-parser-extension.php php ./vendor/bin/phpunit -c ./phpunit.xml.dist tests/mysql/native/WP_MySQL_Parser_Instanceof_Tests.php tests/mysql/native/WP_MySQL_Native_Parser_Node_Identity_Tests.php tests/mysql/native/WP_MySQL_Native_Parser_Node_Cycle_Tests.php WP_SQLITE_REQUIRE_NATIVE_PARSER_EXTENSION=1 php -d extension=../php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so ./vendor/bin/phpunit -c ./phpunit.xml.dist --filter 'WP_MySQL_(Native_Parser_Node_(Identity|Cycle)|Parser_Instanceof)_Tests' php ./vendor/bin/phpunit -c ./phpunit.xml.dist tests/WP_SQLite_Driver_Query_Tests.php WP_SQLITE_REQUIRE_NATIVE_PARSER_EXTENSION=1 php -d extension=../php-ext-wp-mysql-parser/target/debug/libwp_mysql_parser.so ./vendor/bin/phpunit -c ./phpunit.xml.dist tests/WP_SQLite_Driver_Query_Tests.php ``` CI is passing on `3f4153f`, including the PHP 8.0-8.5 Rust-extension matrix and `WordPress PHPUnit Tests / Rust extension`.
## What it does Builds `wp_mysql_parser` as a PHP.wasm JSPI side module and verifies that path in CI. The `WASM extension build` workflow runs the build/load smoke for PHP `8.0`, `8.1`, `8.2`, `8.3`, `8.4`, and `8.5`. Each lane builds a versioned `wp_mysql_parser-php*-jspi.so`, verifies the generated `manifest.json` hash against the `.so`, loads the extension in Playground, and checks that `WP_MySQL_Native_Lexer` tokenizes `SELECT 1 FROM wp_posts`. The workflow uses the recent Playground `@php-wasm/compile-extension` tooling from `WordPress/wordpress-playground` via sparse checkout until that package is available from npm. ## Rationale `wp_mysql_parser` works as a native PHP extension, but Playground needs it as a dynamically loaded PHP.wasm side module. The Playground helper now owns the generic extension build work: Docker image setup, `phpize`, `emconfigure`, `emmake`, static archive linking, `wasm-opt`, and manifest generation. This PR keeps the local glue focused on the Rust-specific part: compiling the parser crate to a wasm32-unknown-emscripten `staticlib`, wrapping it with a tiny phpize shim, and patching the vendored `ext-php-rs` registry copy for PHP.wasm's side-module ABI. PHP `7.4` is intentionally not in this Rust extension matrix. `ext-php-rs` `0.15` depends on PHP 8 Zend APIs and does not compile against PHP `7.4` headers, so the WASM Rust build currently covers PHP `8.0` through `8.5`. That limitation is documented in the package README, the spike result notes, the workflow matrix comment, and the unsupported-version message in the build script. ## Implementation - Sparse-checks out only the needed Playground packages, including `packages/php-wasm/compile-extension`, `compile-extension-cli`, `universal`, `node`, and the PHP `8.0`-`8.5` node builds. - Builds the Playground compile-extension image through the helper's Docker functions. - Uses a host PHP info shim instead of copying Linux PHP binaries/libs into the Emscripten build image. - Passes each matrix PHP version's Zend API number into the Rust build so `ext-php-rs` generates bindings for the target PHP.wasm version. - Invokes `@php-wasm/compile-extension` with `--extra-ldflags /build/libwp_mysql_parser.a` so the helper owns the final `.so` and `manifest.json`. - Serializes the WASM matrix with `max-parallel: 1` to avoid parallel Docker/apt mirror flakes while still verifying every supported PHP version in one workflow run. ## Testing instructions Latest PR-head WASM CI passed on `9f822dfcdc97b0d495acf28497f421d29a6f2348`: https://github.com/WordPress/sqlite-database-integration/actions/runs/25288673841 That run passed all six lanes: PHP `8.5`, `8.4`, `8.3`, `8.2`, `8.1`, and `8.0`. Local checks run before pushing: ```bash git diff --check bash -n packages/php-ext-wp-mysql-parser/wasm-spike/build-in-docker-rust.sh node --check packages/php-ext-wp-mysql-parser/wasm-spike/run-spike.mjs ``` To run the WASM path locally with Docker: ```bash PLAYGROUND_REPO=/path/to/wordpress-playground \ bash packages/php-ext-wp-mysql-parser/wasm-spike/build-in-docker-rust.sh PLAYGROUND_REPO=/path/to/wordpress-playground \ node packages/php-ext-wp-mysql-parser/wasm-spike/run-spike.mjs ```
## Summary Replaces the old `mt_rand(0, 1)` stub — which returned an integer 0 or 1, not a float in `[0, 1)` as MySQL requires — with MySQL-compatible `RAND()` behavior. Replaces #341. ## Unseeded `RAND()` Compiles to a native SQLite expression: ```sql ((RANDOM() & 0x001FFFFFFFFFFFFF) / 9007199254740992.0) ``` The 53-bit mask matches the IEEE 754 double mantissa, so division by 2^53 is exact and strictly less than 1.0. Matches MySQL, where unseeded `RAND()` uses a thread-level state independent of `RAND(N)`. ## Seeded `RAND(N)` Routes through a PHP UDF implementing MySQL's exact LCG from `my_rnd_init()` / `my_rnd()` (`sql/item_func.cc`, `mysys/my_rnd.cc`), bit-exact against MySQL 9.6. Requires 64-bit PHP. Seed handling matches `val_int()`: `NULL` becomes 0, floats round to nearest (`RAND(3.9) == RAND(4)`), numeric strings follow the same path. ## Known divergences - MySQL distinguishes constant vs non-constant seeds at parse time (constant = init once per statement, non-constant = reinit per row). A SQLite UDF can't see the expression, so we approximate by reinitializing only when the seed value changes. This diverges when a non-constant expression yields a stable value. - The UDF keeps one LCG state per connection, so multiple `RAND(N)` call sites in one query share a stream: `SELECT RAND(1), RAND(1)` returns `(v1, v2)` here vs `(v, v)` in MySQL. Both are documented in the `rand()` docblock. ## Metadata `RAND()` column metadata is now reported as `DOUBLE` / `PARAM_STR`, removing a long-standing TODO. ## Test plan - [ ] CI green. - [ ] Bit-exact reference values against MySQL 9.6 (seeds 0, 1, 3, 5, and multi-row sequences). - [ ] NULL, float, numeric/non-numeric string, and negative seed handling. - [ ] `RAND()` in `WHERE`, `UPDATE`, `INSERT`, `ORDER BY` (`LIMIT` + seeded deterministic permutation). - [ ] Per-statement flush contract inside a transaction.
## What it does Builds a Playground-loadable `wp_mysql_parser` WASM PHP extension bundle for PHP `8.0` through `8.5`. On `trunk` pushes or manual `workflow_dispatch`, the new publish workflow: 1. Builds one JSPI side module per PHP version. 2. Uploads the collected bundle as an Actions artifact named `wp_mysql_parser-wasm-extension`. 3. Publishes the same files to the `gh-pages` branch: ```text wp_mysql_parser-wasm-extension/latest/manifest.json wp_mysql_parser-wasm-extension/<commit-sha>/manifest.json ``` The `latest/` path is replaced on every publish. The `<commit-sha>/` path is immutable. ## Rationale Playground needs public `manifest.json` and `.so` files it can fetch at runtime. An Actions artifact is useful for debugging, but it is not a stable public URL and should not be treated as a production plugin release. Publishing the bundle from `gh-pages` gives Playground a static URL without repackaging the WordPress plugin itself. The repository still needs GitHub Pages configured to publish from the `gh-pages` branch root before these URLs resolve: ```text https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/latest/manifest.json https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/<commit-sha>/manifest.json ``` PHP `7.4` is intentionally excluded. The Rust path uses `ext-php-rs` `0.15`, which depends on PHP 8 Zend APIs. ## Implementation Adds `.github/workflows/publish-wasm-extension-artifact.yml` with a serialized PHP matrix for `8.5`, `8.4`, `8.3`, `8.2`, `8.1`, and `8.0`. The package job downloads the per-version `.so` artifacts, writes a combined Playground extension manifest, writes `SHA256SUMS`, uploads the bundle, then commits the static bundle to `gh-pages`. The manifest follows the WordPress Playground extension format from WordPress/wordpress-playground#3580: ```json { "name": "wp_mysql_parser", "mode": "php-extension", "artifacts": [ { "phpVersion": "8.4", "sourcePath": "wp_mysql_parser-php8.4-jspi.so" } ] } ``` The manifest uses `sourcePath` and does not include the retired `file` or `sha256` artifact fields. Checksums are published separately in `SHA256SUMS`. The Rust build now runs the published `@php-wasm/compile-extension@3.1.27` CLI from an isolated temporary npm prefix. A sparse `WordPress/wordpress-playground` checkout is still required for `packages/php-wasm/compile` Docker assets and for the Playground load smoke test. Follow-up for Playground: make `@php-wasm/compile-extension` self-contained so external extension projects do not need to shallow or sparse checkout `WordPress/wordpress-playground` just to access Docker assets or test harness files. ## Testing instructions Run the local static checks: ```bash bash -n packages/php-ext-wp-mysql-parser/wasm-spike/build-in-docker-rust.sh actionlint .github/workflows/wasm-spike.yml .github/workflows/publish-wasm-extension-artifact.yml node --check packages/php-ext-wp-mysql-parser/wasm-spike/write-extension-manifest.mjs git diff --check ``` Verify the PR `WASM extension build` check is green for PHP `8.0` through `8.5`. The latest PR run passed all six PHP versions. After merge, run `Publish WASM extension artifact` with `workflow_dispatch` or let a matching `trunk` push trigger it, then verify both published manifests resolve from GitHub Pages once Pages is configured for the `gh-pages` branch root.
## What changed Removes the temporary WASM workflow noise that was added while stabilizing the extension build PR: - Drops the `Configure Docker DNS` steps from the WASM CI and publish workflows. - Drops the failure-only diagnostic artifact uploads from both workflows. The normal build artifacts are unchanged. ## Why Those steps were useful while chasing transient GitHub-hosted runner and Ubuntu mirror failures, but they make the workflows harder to read and maintain now that the publishing path has landed. ## Validation ```bash actionlint .github/workflows/wasm-spike.yml .github/workflows/publish-wasm-extension-artifact.yml git diff --check ```
## What it does Builds the native Rust MySQL parser extension with `cargo build --release` in the WordPress PHPUnit native-extension job, then loads `target/release/libwp_mysql_parser.so`. ## Rationale The previous workflow loaded `target/debug/libwp_mysql_parser.so`, so the WordPress PHPUnit timing compared pure PHP against an unoptimized Rust extension. That made the Rust-extension job look slower for the wrong reason. With this change, the PR run confirms the release extension is faster in the PHPUnit phase: | Job | Wall time | PHPUnit time | | --- | ---: | ---: | | WordPress PHPUnit Tests | `10m49s` | `06:41.969` | | WordPress PHPUnit Tests / Rust extension | `10m24s` | `05:05.385` | The Rust-extension job still pays separate setup/build cost, but the optimized extension is about `1m36s` faster during PHPUnit itself in this run. ## Implementation Changes the generated build script from: ```sh cargo build ``` to: ```sh cargo build --release ``` and copies the built extension from `target/release` instead of `target/debug`. ## Testing instructions - `bash -n .github/workflows/wp-tests-phpunit-native-extension-setup.sh` - Built `target/release/libwp_mysql_parser.so` locally and verified PHP loads `wp_mysql_parser` - CI: WordPress PHPUnit Tests passed - normal: https://github.com/WordPress/sqlite-database-integration/actions/runs/25343877499/job/74307884595 - Rust extension: https://github.com/WordPress/sqlite-database-integration/actions/runs/25343877499/job/74307884603 - Full PR check suite is passing on #398
## What it does Runs the WASM extension PHP-version matrix concurrently and shares the expensive Playground base image across the matrix. Both the PR smoke workflow and the publish workflow now build `playground-php-wasm:base` once, upload it as a short-lived internal artifact, then fan out all six PHP 8.x builds in parallel. ## Rationale The publish workflow was running one PHP build at a time. In the observed trunk run, PHP 8.5 finished before PHP 8.4 started, and the build spent about 24m30s inside `build-in-docker-rust.sh` for that one PHP version. Blindly running all six jobs in parallel exposed another bottleneck: every job rebuilt the same Ubuntu-based Playground base image and hit `apt-get update` at the same time. Sharing that base image keeps the wall-time win while avoiding six duplicate apt/install passes. A later CI run showed one more duplicate path: `@php-wasm/compile-extension@3.1.27` rebuilt the Playground base image and PHP-specific compile-extension image again during Stage 2. The build script now reuses the images prepared in Stage 0, so each matrix job does one Docker image preparation pass instead of two. ## Implementation - Add a `base-image` job to `.github/workflows/wasm-spike.yml` and `.github/workflows/publish-wasm-extension-artifact.yml` - Upload the saved `playground-php-wasm:base` image with `retention-days: 1` - Make each PHP matrix job download and `docker load` that base image before building its PHP-specific compile-extension image - Set `max-parallel: 6` for both PHP matrices - Add `SKIP_BASE_IMAGE_BUILD=1` support to `build-in-docker-rust.sh` so CI can require a preloaded base image while local runs still build it normally - Patch the pinned compile-extension CLI at install time to skip its duplicate Docker image build and use the Stage 0 images; the patch fails loudly if the package shape changes I also tried wiring `docker buildx build --cache-to type=gha`, but dropped it. The GHA cache backend requires a non-default Buildx driver, and that driver does not automatically expose locally tagged intermediate images like `playground-php-wasm:base` to later Dockerfile `FROM` lines. The internal image artifact is simpler and matches this build's local image chain. Follow-up: `@php-wasm/compile-extension` should expose a first-class way to use already-prepared images so this repo does not need to patch the pinned CLI at runtime. ## Testing instructions - `bash -n packages/php-ext-wp-mysql-parser/wasm-spike/build-in-docker-rust.sh` - `actionlint .github/workflows/publish-wasm-extension-artifact.yml .github/workflows/wasm-spike.yml` - `git diff --check` - Verified in PR CI run `25348528195`: shared base image job passed in 4m18s, all six PHP matrix jobs passed in 9m30s-11m55s, and the workflow completed successfully
## What changed - Raises the WASM extension matrix build timeout from 45 to 60 minutes in the publish workflow. - Keeps the same timeout in the interactive WASM spike workflow, which runs the same build shape. ## Why The first publish run after merging #399 built the PHP 8.3 side module successfully, but hit the 45-minute job timeout before the verify/upload steps could run. The log ended with the artifact written and then the job cancellation.
## What Fixes the publish workflow's manual `git push` to `gh-pages` by configuring the Actions token as a Basic auth header. ## Why The WASM extension build and package jobs now complete, but the static publish step failed with `fatal: could not read Username for https://github.com`. ## Testing - Observed publish run 25365457168 complete all build/package steps before failing only at `git push` authentication.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This updates the GitHub Actions workflow files to: - Grant minimally-scoped permissions to each job to adhere to the principle of least privilege - Specify a timeout on each job to prevent runaway processes consuming too many minutes (the default is 360) Once this PR is merged, [the Settings -> Actions -> Workflow permissions setting](https://github.com/WordPress/sqlite-database-integration/settings/actions) can be changed by a repo admin to "Read repository contents and packages permissions". ## References - https://docs.github.com/en/actions/reference/security/secure-use - https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idtimeout-minutes ## Use of AI Claude Code was used to create the initial changes. All permissions and timeouts changes were reviewed and adjusted by me where necessary.
## What changed Reorganizes the root README so Quick start stays first and the optional Native MySQL Parser Extension is discoverable without taking over the landing flow. The README links to the GitHub Pages landing page with the published WASM release list, manifest links, “Run in Playground” buttons, and native extension details. Also expands `packages/php-ext-wp-mysql-parser/README.md` with local build/load instructions, WASM artifact details, checksum verification steps, and benchmark commands. The benchmark scripts now run cleanly on PHP 8.4+, can emit JSON, report exact processed counts, and the parser benchmark goes through the integration loader so `php -d extension=...` measures the native parser path instead of accidentally staying on the pure-PHP classes. ## Why The native extension and WASM artifacts were hard to discover. This keeps the normal README quick start prominent while giving users a clear path to optional native-extension docs, Playground links, and reproducible benchmark commands. ## Self-review fixes - Fixed benchmark query counts to use an explicit processed counter instead of loop-index arithmetic. - Fixed `run-parser-benchmark.php` to include the first query and to load through `src/load.php`, matching runtime native-vs-PHP class selection. - Tightened docs so build commands do not leave readers in the package directory before repository-root verification commands. - Added checksum verification commands and refreshed benchmark numbers, including native parser results. ## Related Pages work - GitHub Pages native-extension demo: #408 (merged) - GitHub Pages release list + Playground CLI instructions: #409 (merged) - GitHub Pages benchmark refresh: #410 (merged) - Published page: https://wordpress.github.io/sqlite-database-integration/ ## Validation - `composer validate --no-check-all` - `php -l packages/mysql-on-sqlite/tests/tools/run-lexer-benchmark.php` - `php -l packages/mysql-on-sqlite/tests/tools/run-parser-benchmark.php` - `php -l packages/mysql-on-sqlite/tests/tools/run-native-extension-benchmark.php` - `php packages/mysql-on-sqlite/tests/tools/run-lexer-benchmark.php --json --limit=100` - `php packages/mysql-on-sqlite/tests/tools/run-parser-benchmark.php --json --limit=100` - `php packages/mysql-on-sqlite/tests/tools/run-native-extension-benchmark.php --json --limit=100` - `php -d extension=/path/to/libwp_mysql_parser.dylib packages/mysql-on-sqlite/tests/tools/run-native-extension-benchmark.php --json --limit=100` - `php -d extension=/path/to/libwp_mysql_parser.dylib packages/mysql-on-sqlite/tests/tools/run-parser-benchmark.php --json --limit=100` - Full benchmark rerun for published numbers: lexer PHP, parser PHP, native lexer, native parser. - `npx @wp-playground/cli@latest run-blueprint --php=8.4 --php-extension=https://wordpress.github.io/sqlite-database-integration/wp_mysql_parser-wasm-extension/latest/manifest.json --blueprint=https://wordpress.github.io/sqlite-database-integration/native-extension/blueprint.json --verbosity=quiet` Note: `composer run check-cs` could not run in this workspace because `vendor/bin/phpcs` is not installed locally; CI runs it with dependencies installed.
adamziel
added a commit
that referenced
this pull request
May 27, 2026
## What? Updates the GitHub Pages native parser demo to use PHP 8.5 instead of the temporary PHP 8.3 pin: - Playground links now use `php=8.5`. - The Blueprint preferred PHP version is now 8.5. - The page text says the manifest supports PHP 8.0 through 8.5. ## Why? The PHP 8.3 pin was a workaround for missing Zend exports in the Playground browser runtime. Once WordPress/wordpress-playground#3690 and rebuilt web PHP.wasm artifacts ship, the browser runtime should support the published `wp_mysql_parser` side modules across PHP 8.0-8.5. ## Dependency Do not merge/publish this until the Playground web-runtime fix and rebuilt artifacts are live, and #417 passes against them.
adamziel
added a commit
that referenced
this pull request
May 27, 2026
## What? Adds a browser-runtime compatibility check for the `wp_mysql_parser` WASM side modules and wires it into both WASM workflows: - `wasm-spike.yml` - `publish-wasm-extension-artifact.yml` The check compares each side module's `env` function imports against the matching Playground web PHP.wasm runtime exports. It runs for PHP 8.0 through 8.5 so the published manifest cannot claim browser support for a PHP version that will crash during extension startup. Also updates the native extension README/demo URL to use the live `blueprint.json` location and documents PHP 8.0-8.5 browser-runtime coverage instead of the temporary PHP 8.3 pin. ## Why? The previous Node-based smoke test was insufficient: PHP 8.4 loaded in the Node/CLI runtime but crashed in the browser runtime because the web runtime did not export Zend symbols imported by the extension side module. This PR makes that class of failure visible in CI before publishing a WASM manifest. ## Dependency This depends on WordPress/wordpress-playground#3690 and updated Playground web PHP.wasm artifacts. Current Playground web builds are known to miss required exports for PHP 8.0, 8.1, 8.4, and 8.5; the new CI check is expected to pass once those runtime exports ship. ## Testing - `node --check packages/php-ext-wp-mysql-parser/wasm-spike/check-playground-web-compat.mjs` - `git diff --check` - Verified locally with the Playground checkout: - PHP 8.3 currently passes. - PHP 8.4 currently fails with missing `zend_declare_class_constant_ex` and `zend_register_internal_class_ex`, matching the browser crash root cause. ## Note Replacement for #417, which was accidentally opened and merged against `develop` instead of `trunk`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Adds a browser-runtime compatibility check for the
wp_mysql_parserWASM side modules and wires it into both WASM workflows:wasm-spike.ymlpublish-wasm-extension-artifact.ymlThe check compares each side module's
envfunction imports against the matching Playground web PHP.wasm runtime exports. It runs for PHP 8.0 through 8.5 so the published manifest cannot claim browser support for a PHP version that will crash during extension startup.Also updates the native extension README/demo URL to use the live
blueprint.jsonlocation and documents PHP 8.0-8.5 browser-runtime coverage instead of the temporary PHP 8.3 pin.Why?
The previous Node-based smoke test was insufficient: PHP 8.4 loaded in the Node/CLI runtime but crashed in the browser runtime because the web runtime did not export Zend symbols imported by the extension side module.
This PR makes that class of failure visible in CI before publishing a WASM manifest.
Dependency
This depends on WordPress/wordpress-playground#3690 and updated Playground web PHP.wasm artifacts. Current Playground web builds are known to miss required exports for PHP 8.0, 8.1, 8.4, and 8.5; the new CI check is expected to pass once those runtime exports ship.
Testing
node --check packages/php-ext-wp-mysql-parser/wasm-spike/check-playground-web-compat.mjsgit diff --checkzend_declare_class_constant_exandzend_register_internal_class_ex, matching the browser crash root cause.