Skip to content

Add MySQL binary protocol proxy for SQLite integration#3393

Closed
wp-playground-bot wants to merge 6 commits intoWordPress:trunkfrom
wp-playground-bot:mysql-binary-protocol-sqlite
Closed

Add MySQL binary protocol proxy for SQLite integration#3393
wp-playground-bot wants to merge 6 commits intoWordPress:trunkfrom
wp-playground-bot:mysql-binary-protocol-sqlite

Conversation

@wp-playground-bot
Copy link
Copy Markdown

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/wordpress gains a new mysqlProxyPort option
that 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/decoding
  • mysql-proxy.ts – TCP server implementing the MySQL server side
  • sqlite-over-mysql-proxy.ts – bridges the proxy to sqlite-database-integration
  • mysql-proxy.spec.ts – protocol-level tests (5 test cases)
  • mysql-proxy-boot.spec.ts – integration test booting WordPress via the proxy

Depends on #3284 – rebase onto that branch once merged.

Test plan

  • npx nx test-group-4-asyncify php-wasm-node passes (includes mysql-proxy.spec.ts)
  • npx nx test-group-4-jspi php-wasm-node passes
  • WordPress boot integration test passes (mysql-proxy-boot.spec.ts)
  • Existing tests remain green

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.
@wp-playground-bot wp-playground-bot requested review from a team, brandonpayton and Copilot March 14, 2026 22:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 mysqlProxyPort option (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,
});
claude added 2 commits March 15, 2026 00:36
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.
@JanJakes
Copy link
Copy Markdown
Member

Closing in favor of #3490.

@JanJakes JanJakes closed this Apr 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants