Add MySQL binary protocol proxy for SQLite integration#3393
Closed
wp-playground-bot wants to merge 6 commits intoWordPress:trunkfrom
Closed
Add MySQL binary protocol proxy for SQLite integration#3393wp-playground-bot wants to merge 6 commits intoWordPress:trunkfrom
wp-playground-bot wants to merge 6 commits intoWordPress:trunkfrom
Conversation
Enable compiling PHP 5.6 to WebAssembly so WordPress Playground can run
WordPress 1.x through 4.x, which require PHP 5.2+. PHP 5.6 is backward
compatible with PHP 5.2 code, so a single legacy version covers all older
WordPress releases.
Changes:
- Dockerfile: Add PHP 5.x version guards (fiber-asm, imagick, chunk-alloc
patches, ASM arithmetic, cli_server)
- C sources: Add PHP 5.x compatibility for proc_open, dns_polyfill,
post_message_to_js, and wasm_memory_storage extensions
- Package scaffolding: node-builds/5-6 and web-builds/5-6 with full NX
project configuration
- Loader modules: Add case '5.6' to node and web getPHPLoaderModule()
- Version registry: Add PHP 5.6 to supported-php-versions.mjs and
LegacyPHPVersions in supported-php-versions.ts (separate from
SupportedPHPVersions to avoid test regressions)
- Test suite: Dedicated legacy-php-versions.spec.ts with 12 tests
covering execution, file I/O, networking, proc_open, SQLite, JSON,
sessions, mbstring, error handling, memory, and filesystem ops
- tsconfig.base.json: Path aliases for new packages
Note: WASM binaries must be compiled in a Docker-enabled environment:
node packages/php-wasm/compile/build.js --PLATFORM=node --PHP_VERSION=5.6 \
--WITH_IMAGICK=no --WITH_OPCACHE=no
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PHP 5.6 is now functional in Playground with both web JSPI and node asyncify variants. This required fixing several compatibility issues between PHP 5.6's C codebase and modern Emscripten/clang: Dockerfile: SQLite duplicate symbol fix for PHP 5.x bundled sqlite3, readdir_r compatibility, x86_64 inline assembly guards, PCRE JIT disable, curl-config shim for PHP 5.x detection, and compiler warning suppression flags (-Wno-implicit-int, -Wno-implicit-function-declaration, -Wno-incompatible-function-pointer-types, etc). php_wasm.c: PHP 5.x API compat for size_t vs int return types in SAPI callbacks, and dup parameter in add_next_index_stringl / RETVAL_STRINGL macros. The test suite's isVersionBuilt() now checks for actual .wasm binary files instead of just directory existence, preventing crashes when placeholder stubs are present but no real binaries exist. Built with limited extensions (no OpenSSL, cURL, GD, ImageMagick, MySQL, mbregex) for this initial release. Verified: PHP boots, initializes SAPI, serves HTTP 200 with X-Powered-By: PHP/5.6.40, and executes PHP code producing correct output.
Resolved three merge conflicts: - packages/php-wasm/supported-php-versions.mjs: Kept PR's newer version numbers and PHP 5.6 entry - packages/php-wasm/node/project.json: Kept both test-legacy-php (PR) and test-file-locking targets (trunk) - packages/php-wasm/node/src/lib/load-runtime.ts: Merged LegacyPHPVersion import with trunk's new imports, kept modernVersion cast with trunk's truthy check
WordPress Playground uses SQLite under the hood, but PHP 5.6 can't run the sqlite-database-integration plugin (it requires PHP 7.2+). This adds a MySQL wire protocol proxy that lets PHP 5.6 connect to what it thinks is a real MySQL server. Behind the scenes, a second PHP 8.x instance translates the MySQL queries to SQLite using the sqlite-database-integration plugin. The proxy implements the MySQL binary protocol: handshake, authentication, COM_QUERY, COM_PING, COM_QUIT, result sets, OK/ERR/EOF packets. The boot process accepts a new `mysqlProxyPort` option that configures WordPress with standard MySQL database constants instead of the db.php drop-in approach.
Contributor
There was a problem hiding this comment.
Pull request overview
Introduces a MySQL wire-protocol proxy that lets WordPress running on PHP 5.6 talk “MySQL” while a separate PHP 8.x instance translates queries to SQLite via sqlite-database-integration, and adds scaffolding for PHP 5.6 wasm builds (node + web) and related tests.
Changes:
- Add MySQL protocol implementation + TCP proxy server, plus a SQLite-backed bridge via the WP SQLite driver.
- Extend WordPress boot to support a
mysqlProxyPortoption (MySQL constants instead of SQLite drop-in). - Add PHP 5.6 “legacy” version plumbing and build package scaffolding for node/web, plus new protocol + integration tests.
Reviewed changes
Copilot reviewed 41 out of 46 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.base.json | Adds TS path aliases for new PHP 5.6 node/web build packages. |
| packages/playground/wordpress/src/test/mysql-proxy-boot.spec.ts | Adds an integration test booting WP (PHP 5.6) via the MySQL proxy. |
| packages/playground/wordpress/src/boot.ts | Adds mysqlProxyPort option and MySQL-proxy WP configuration helper. |
| packages/php-wasm/web/src/lib/get-php-loader-module.ts | Allows choosing PHP 5.6 loader module in web runtime. |
| packages/php-wasm/web-builds/5-6/tsconfig.lib.json | Build config for PHP 5.6 web binaries package. |
| packages/php-wasm/web-builds/5-6/tsconfig.json | Project TS config for PHP 5.6 web binaries package. |
| packages/php-wasm/web-builds/5-6/src/index.ts | Loader entrypoint for PHP 5.6 web (jspi/asyncify). |
| packages/php-wasm/web-builds/5-6/project.json | Nx targets for building/publishing the PHP 5.6 web binaries package. |
| packages/php-wasm/web-builds/5-6/package.json | Package metadata for @php-wasm/web-5-6. |
| packages/php-wasm/web-builds/5-6/build.js | esbuild bundle script for the PHP 5.6 web binaries package. |
| packages/php-wasm/web-builds/5-6/asyncify/php_5_6.js | Placeholder loader module for asyncify PHP 5.6 web build. |
| packages/php-wasm/web-builds/5-6/README.md | Docs for @php-wasm/web-5-6. |
| packages/php-wasm/universal/src/lib/supported-php-versions.ts | Introduces LegacyPHPVersions / LegacyPHPVersion types (5.6). |
| packages/php-wasm/universal/src/lib/index.ts | Re-exports new legacy version types/constants. |
| packages/php-wasm/supported-php-versions.mjs | Updates release metadata and adds 5.6 to version list. |
| packages/php-wasm/node/src/test/mysql-proxy.spec.ts | Adds protocol-level tests for the MySQL proxy. |
| packages/php-wasm/node/src/test/legacy-php-versions.spec.ts | Adds a separate test suite for legacy PHP 5.6 runtime basics. |
| packages/php-wasm/node/src/lib/networking/sqlite-over-mysql-proxy.ts | Adds SQLite-backed MySQL proxy bridge using WP SQLite driver. |
| packages/php-wasm/node/src/lib/networking/mysql-proxy.ts | Adds TCP server implementing MySQL handshake + COM_QUERY/PING/QUIT. |
| packages/php-wasm/node/src/lib/networking/mysql-protocol.ts | Adds low-level MySQL packet parsing/encoding utilities. |
| packages/php-wasm/node/src/lib/load-runtime.ts | Extends node runtime loader to accept legacy version type. |
| packages/php-wasm/node/src/lib/index.ts | Re-exports new proxy/bridge APIs from @php-wasm/node. |
| packages/php-wasm/node/src/lib/get-php-loader-module.ts | Adds support for loading @php-wasm/node-5-6. |
| packages/php-wasm/node/project.json | Adds MySQL proxy test file and a dedicated legacy test target. |
| packages/php-wasm/node-builds/5-6/tsconfig.lib.json | Build config for PHP 5.6 node binaries package. |
| packages/php-wasm/node-builds/5-6/tsconfig.json | Project TS config for PHP 5.6 node binaries package. |
| packages/php-wasm/node-builds/5-6/src/index.ts | Loader entrypoint for PHP 5.6 node (jspi/asyncify). |
| packages/php-wasm/node-builds/5-6/project.json | Nx targets for building/publishing the PHP 5.6 node binaries package. |
| packages/php-wasm/node-builds/5-6/package.json | Package metadata for @php-wasm/node-5-6. |
| packages/php-wasm/node-builds/5-6/jspi/php_5_6.js | Placeholder loader module for JSPI PHP 5.6 node build. |
| packages/php-wasm/node-builds/5-6/build.js | esbuild bundle script for the PHP 5.6 node binaries package. |
| packages/php-wasm/node-builds/5-6/README.md | Docs for @php-wasm/node-5-6. |
| packages/php-wasm/compile/php/proc_open.h | Adds PHP 5.x compatible pipe type to proc_open handle. |
| packages/php-wasm/compile/php/proc_open.c | Adds PHP 5.x stub implementation + conditional includes. |
| packages/php-wasm/compile/php/php_wasm.c | Adds PHP 5.x compatible return macros and sapi function signatures. |
| packages/php-wasm/compile/php/php5.6.patch | Adds PHP 5.6 source patch (copy() empty file crash workaround). |
| packages/php-wasm/compile/php/apply-mysqlnd-patch.sh | Makes mysqlnd patch non-fatal for versions/layouts where it doesn’t apply. |
| packages/php-wasm/compile/php/Dockerfile | Adjusts build steps/flags for PHP 5.x compatibility and conditional patches. |
| packages/php-wasm/compile/php-wasm-memory-storage/wasm_memory_storage.c | No-op memory storage module for PHP < 7. |
| packages/php-wasm/compile/php-wasm-dns-polyfill/dns_polyfill.c | Adds PHP 5.x compatible parameter parsing/types and TSRMLS usage. |
| packages/php-wasm/compile/php-post-message-to-js/post_message_to_js.c | Fixes size_t comparison and adds PHP 5.x compatible return path. |
Comments suppressed due to low confidence (1)
packages/php-wasm/compile/php/php5.6.patch:1
- This patch introduces
//comments into PHP 5.6 C sources. PHP 5.6 is typically compiled as C89/C90 in many toolchains, where//comments can be rejected (leading to build failures). Replace these with/* ... */comments in the patch to avoid compilation portability issues.
--- a/php-src/ext/standard/file.c 2026-03-12 23:02:36.638186358 +0100
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+259
to
+269
| const usesMyqlProxy = !!options.mysqlProxyPort; | ||
| if (usesMyqlProxy) { | ||
| /** | ||
| * When a MySQL proxy is configured, WordPress connects to it | ||
| * via standard MySQL functions (mysqli). No db.php drop-in | ||
| * is needed. This is used for PHP 5.6 where the modern | ||
| * sqlite-database-integration plugin can't run directly. | ||
| */ | ||
| usesSqlite = true; | ||
| await configureWordPressForMySQLProxy(php, options.mysqlProxyPort!); | ||
| } else if (options.sqliteIntegrationPluginZip) { |
Comment on lines
+352
to
+353
| // If a MySQL proxy is configured (usesSqlite is true in that case), | ||
| // the database is handled through the proxy. Skip further checks. |
Comment on lines
+108
to
+196
| socket.on('data', async (data) => { | ||
| const packets = parser.feed(data); | ||
|
|
||
| for (const { sequenceId, payload } of packets) { | ||
| try { | ||
| if (!handshakeDone) { | ||
| // This is the client's handshake response | ||
| const { username, database } = | ||
| parseHandshakeResponse(payload); | ||
| log( | ||
| `Auth from user=${username}, db=${database}` | ||
| ); | ||
| handshakeDone = true; | ||
| // Accept any credentials | ||
| socket.write(buildOkPacket(sequenceId + 1)); | ||
| continue; | ||
| } | ||
|
|
||
| // Command phase | ||
| const commandByte = payload.readUInt8(0); | ||
| const commandPayload = payload.subarray(1); | ||
|
|
||
| switch (commandByte) { | ||
| case COM_QUIT: | ||
| log(`#${threadId} COM_QUIT`); | ||
| socket.end(); | ||
| break; | ||
|
|
||
| case COM_PING: | ||
| log(`#${threadId} COM_PING`); | ||
| socket.write(buildOkPacket(sequenceId + 1)); | ||
| break; | ||
|
|
||
| case COM_INIT_DB: { | ||
| const dbName = | ||
| commandPayload.toString('utf8'); | ||
| log(`#${threadId} COM_INIT_DB: ${dbName}`); | ||
| socket.write(buildOkPacket(sequenceId + 1)); | ||
| break; | ||
| } | ||
|
|
||
| case COM_QUERY: { | ||
| const query = | ||
| commandPayload.toString('utf8'); | ||
| log( | ||
| `#${threadId} COM_QUERY: ${query.substring(0, 200)}` | ||
| ); | ||
| await handleQuery( | ||
| socket, | ||
| sequenceId + 1, | ||
| query, | ||
| options.queryHandler | ||
| ); | ||
| break; | ||
| } | ||
|
|
||
| default: | ||
| log( | ||
| `#${threadId} Unknown command: 0x${commandByte.toString(16)}` | ||
| ); | ||
| socket.write( | ||
| buildErrPacket( | ||
| sequenceId + 1, | ||
| 1047, | ||
| '08S01', | ||
| `Unknown command: 0x${commandByte.toString(16)}` | ||
| ) | ||
| ); | ||
| break; | ||
| } | ||
| } catch (err: any) { | ||
| log( | ||
| `#${threadId} Error processing packet:`, | ||
| err | ||
| ); | ||
| try { | ||
| socket.write( | ||
| buildErrPacket( | ||
| sequenceId + 1, | ||
| 1105, | ||
| 'HY000', | ||
| err.message || 'Internal proxy error' | ||
| ) | ||
| ); | ||
| } catch { | ||
| // Socket may already be closed | ||
| } | ||
| } | ||
| } |
Comment on lines
+213
to
+222
| resolve({ | ||
| port: addr.port, | ||
| host: addr.address, | ||
| close: () => | ||
| new Promise<void>((res) => { | ||
| server.close(() => res()); | ||
| // Force close all existing connections | ||
| server.unref(); | ||
| }), | ||
| }); |
Comment on lines
+83
to
+97
|
|
||
| return async (query: string): Promise<QueryResult | null> => { | ||
| if (!initialized) { | ||
| await initializeDriver(php, sqlitePluginPath, sqliteDatabasePath); | ||
| initialized = true; | ||
| } | ||
|
|
||
| // Write the query to a temp file to avoid escaping issues | ||
| // with embedded quotes, backslashes, etc. | ||
| php.writeFile('/tmp/mysql-proxy-query.sql', query); | ||
|
|
||
| const result = await php.run({ | ||
| code: `<?php | ||
| $driver = $GLOBALS['_mysql_proxy_driver']; | ||
| $query = file_get_contents('/tmp/mysql-proxy-query.sql'); |
Comment on lines
+291
to
+313
| // Extensions are only available for modern PHP versions. | ||
| const modernVersion = phpVersion as SupportedPHPVersion; | ||
| if (options?.withXdebug) { | ||
| emscriptenOptions = await withXdebug( | ||
| phpVersion, | ||
| modernVersion, | ||
| emscriptenOptions, | ||
| typeof options.withXdebug === 'object' ? options.withXdebug : {} | ||
| ); | ||
| } | ||
|
|
||
| if (options?.withIntl === true) { | ||
| emscriptenOptions = await withIntl(phpVersion, emscriptenOptions); | ||
| emscriptenOptions = await withIntl(modernVersion, emscriptenOptions); | ||
| } | ||
|
|
||
| if (options?.withRedis === true) { | ||
| emscriptenOptions = await withRedis(phpVersion, emscriptenOptions); | ||
| emscriptenOptions = await withRedis(modernVersion, emscriptenOptions); | ||
| } | ||
|
|
||
| if (options?.withMemcached === true) { | ||
| emscriptenOptions = await withMemcached(phpVersion, emscriptenOptions); | ||
| emscriptenOptions = await withMemcached( | ||
| modernVersion, | ||
| emscriptenOptions | ||
| ); |
Comment on lines
+67
to
+73
| await using handler = await bootWordPressAndRequestHandler({ | ||
| createPhpRuntime: async () => | ||
| await loadNodeRuntime('5.6' as any), | ||
| siteUrl: 'http://playground-domain/', | ||
| wordPressZip: await getWordPressModule(), | ||
| mysqlProxyPort: proxy.port, | ||
| }); |
3 tasks
Address review feedback for the MySQL binary protocol proxy: - Serialize packet processing with a promise chain to prevent overlapping async handlers when TCP delivers data events in quick succession - Track active sockets and destroy them on server.close() so the close promise resolves immediately instead of hanging - Remove findFreePorts() TOCTOU race by passing port 0 directly to server.listen() and reading the assigned port from server.address() - Use unique temp file names per query to prevent concurrent overwrites - Escape PHP string literals to prevent path injection - Fix typo: usesMyqlProxy → usesMysqlProxy - Rewrite DB constants in wp-config.php via defineWpConfigConstants instead of php.defineConstant to avoid duplicate define() notices - Strengthen test assertions to check HTTP 200 and specific page content
WordPress doesn't use the COM_FIELD_LIST command, and the constant was exported but never imported anywhere. The proxy's default case already handles unknown commands with a proper error response.
Member
|
Closing in favor of #3490. |
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.
Summary
WordPress Playground uses SQLite under the hood, but PHP 5.6 can't run the
sqlite-database-integration plugin directly (it requires PHP 7.2+). This PR
introduces a MySQL wire protocol proxy that lets PHP 5.6 connect to what it
thinks is a real MySQL server. Behind the scenes, a second PHP 8.x instance
translates the MySQL queries to SQLite via the sqlite-database-integration
plugin.
The proxy speaks the MySQL binary protocol – handshake, authentication,
COM_QUERY, COM_PING, COM_QUIT, result sets, OK/ERR/EOF packets. The boot
process in
@wp-playground/wordpressgains a newmysqlProxyPortoptionthat configures WordPress with standard MySQL database constants instead
of the db.php drop-in approach.
New files:
mysql-protocol.ts– low-level MySQL wire protocol encoding/decodingmysql-proxy.ts– TCP server implementing the MySQL server sidesqlite-over-mysql-proxy.ts– bridges the proxy to sqlite-database-integrationmysql-proxy.spec.ts– protocol-level tests (5 test cases)mysql-proxy-boot.spec.ts– integration test booting WordPress via the proxyTest plan
npx nx test-group-4-asyncify php-wasm-nodepasses (includes mysql-proxy.spec.ts)npx nx test-group-4-jspi php-wasm-nodepasses