Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ phpunit.xml.dist export-ignore
wp-setup.sh export-ignore
/.github export-ignore
/grammar-tools export-ignore
/packages export-ignore
/tests export-ignore
/wp-includes/sqlite/class-wp-sqlite-crosscheck-db.php export-ignore
/wordpress export-ignore
33 changes: 33 additions & 0 deletions .github/workflows/mysql-proxy-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: MySQL Proxy Tests

on:
push:
branches:
- main
pull_request:

jobs:
test:
name: MySQL Proxy Tests
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'

- name: Install Composer dependencies
uses: ramsey/composer-install@v3
with:
ignore-cache: "yes"
composer-options: "--optimize-autoloader"
working-directory: packages/wp-mysql-proxy

- name: Run MySQL Proxy tests
run: composer run test
working-directory: packages/wp-mysql-proxy
37 changes: 37 additions & 0 deletions packages/wp-mysql-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# WP MySQL Proxy
A MySQL proxy that bridges the MySQL wire protocol to a PDO-like interface.

This is a zero-dependency, pure PHP implementation of a MySQL proxy that acts as
a MySQL server, accepts MySQL-native commands, and executes them using a configurable
PDO-like driver. This allows MySQL-compatible clients to connect and run queries
against alternative database backends over the MySQL wire protocol.

Combined with the **WP SQLite Driver**, this allows MySQL-based projects to run
on SQLite.

## Usage

### CLI:

```bash
$ php mysql-proxy.php [--database <path/to/db.sqlite>] [--port <port>]

Options:
-h, --help Show this help message and exit.
-d, --database=<path> The path to the SQLite database file. Default: :memory:
-p, --port=<port> The port to listen on. Default: 3306
```

### PHP:
```php
use WP_MySQL_Proxy\MySQL_Proxy;
use WP_MySQL_Proxy\Adapter\SQLite_Adapter;

require_once __DIR__ . '/vendor/autoload.php';

$proxy = new MySQL_Proxy(
new SQLite_Adapter( $db_path ),
array( 'port' => $port )
);
$proxy->start();
```
46 changes: 46 additions & 0 deletions packages/wp-mysql-proxy/bin/wp-mysql-proxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare( strict_types = 1 );

use WP_MySQL_Proxy\MySQL_Proxy;
use WP_MySQL_Proxy\Adapter\SQLite_Adapter;

require_once __DIR__ . '/../vendor/autoload.php';

define( 'WP_SQLITE_AST_DRIVER', true );

// Process CLI arguments:
$shortopts = 'h:d:p';
$longopts = array( 'help', 'database:', 'port:' );
$opts = getopt( $shortopts, $longopts );

$help = <<<USAGE
Usage: php mysql-proxy.php [--database <path/to/db.sqlite>] [--port <port>]

Options:
-h, --help Show this help message and exit.
-d, --database=<path> The path to the SQLite database file. Default: :memory:
-p, --port=<port> The port to listen on. Default: 3306

USAGE;

// Help.
if ( isset( $opts['h'] ) || isset( $opts['help'] ) ) {
fwrite( STDERR, $help );
exit( 0 );
}

// Database path.
$db_path = $opts['d'] ?? $opts['database'] ?? ':memory:';

// Port.
$port = (int) ( $opts['p'] ?? $opts['port'] ?? 3306 );
if ( $port < 1 || $port > 65535 ) {
fwrite( STDERR, "Error: --port must be an integer between 1 and 65535. Use --help for more information.\n" );
exit( 1 );
}

// Start the MySQL proxy.
$proxy = new MySQL_Proxy(
new SQLite_Adapter( $db_path ),
array( 'port' => $port )
);
$proxy->start();
22 changes: 22 additions & 0 deletions packages/wp-mysql-proxy/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "wordpress/wp-mysql-proxy",
"type": "library",
"bin": [
"bin/wp-mysql-proxy.php"
],
"scripts": {
"test": "phpunit"
},
"require-dev": {
"phpunit/phpunit": "^8.5",
"symfony/process": "^5.4"
},
"autoload": {
"classmap": [
"src/"
],
"files": [
"../../php-polyfills.php"
]
}
}
8 changes: 8 additions & 0 deletions packages/wp-mysql-proxy/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap/bootstrap.php" colors="true" stopOnFailure="false">
<testsuites>
<testsuite name="WP MySQL Proxy Test Suite">
<directory>tests/</directory>
</testsuite>
</testsuites>
</phpunit>
9 changes: 9 additions & 0 deletions packages/wp-mysql-proxy/src/Adapter/class-adapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare( strict_types = 1 );

namespace WP_MySQL_Proxy\Adapter;

use WP_MySQL_Proxy\MySQL_Result;

interface Adapter {
public function handle_query( string $query ): MySQL_Result;
}
122 changes: 122 additions & 0 deletions packages/wp-mysql-proxy/src/Adapter/class-sqlite-adapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php declare( strict_types = 1 );

namespace WP_MySQL_Proxy\Adapter;

use PDOException;
use Throwable;
use WP_MySQL_Proxy\MySQL_Result;
use WP_SQLite_Connection;
use WP_SQLite_Driver;
use WP_MySQL_Proxy\MySQL_Protocol;

define( 'SQLITE_DRIVER_PATH', __DIR__ . '/../../../..' );

require_once SQLITE_DRIVER_PATH . '/version.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/parser/class-wp-parser-grammar.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/parser/class-wp-parser.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/parser/class-wp-parser-node.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/parser/class-wp-parser-token.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/mysql/class-wp-mysql-token.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/mysql/class-wp-mysql-lexer.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/mysql/class-wp-mysql-parser.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-connection.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php';
require_once SQLITE_DRIVER_PATH . '/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php';

class SQLite_Adapter implements Adapter {
/** @var WP_SQLite_Driver */
private $sqlite_driver;

public function __construct( $sqlite_database_path ) {
define( 'FQDB', $sqlite_database_path );
define( 'FQDBDIR', dirname( FQDB ) . '/' );

$this->sqlite_driver = new WP_SQLite_Driver(
new WP_SQLite_Connection( array( 'path' => $sqlite_database_path ) ),
'sqlite_database'
);
}

public function handle_query( string $query ): MySQL_Result {
$affected_rows = 0;
$last_insert_id = null;
$columns = array();
$rows = array();

try {
$return_value = $this->sqlite_driver->query( $query );
$last_insert_id = $this->sqlite_driver->get_insert_id() ?? null;
if ( is_numeric( $return_value ) ) {
$affected_rows = (int) $return_value;
} elseif ( is_array( $return_value ) ) {
$rows = $return_value;
}
if ( $this->sqlite_driver->get_last_column_count() > 0 ) {
$columns = $this->computeColumnInfo();
}
return MySQL_Result::from_data( $affected_rows, $last_insert_id, $columns, $rows ?? array() );
} catch ( Throwable $e ) {
$error_info = $e->errorInfo ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
if ( $e instanceof PDOException && $error_info ) {
return MySQL_Result::from_error( $error_info[0], $error_info[1], $error_info[2] );
}
return MySQL_Result::from_error( 'HY000', 1105, $e->getMessage() ?? 'Unknown error' );
}
}

public function computeColumnInfo() {
$columns = array();

$column_meta = $this->sqlite_driver->get_last_column_meta();

$types = array(
'DECIMAL' => MySQL_Protocol::FIELD_TYPE_DECIMAL,
'TINY' => MySQL_Protocol::FIELD_TYPE_TINY,
'SHORT' => MySQL_Protocol::FIELD_TYPE_SHORT,
'LONG' => MySQL_Protocol::FIELD_TYPE_LONG,
'FLOAT' => MySQL_Protocol::FIELD_TYPE_FLOAT,
'DOUBLE' => MySQL_Protocol::FIELD_TYPE_DOUBLE,
'NULL' => MySQL_Protocol::FIELD_TYPE_NULL,
'TIMESTAMP' => MySQL_Protocol::FIELD_TYPE_TIMESTAMP,
'LONGLONG' => MySQL_Protocol::FIELD_TYPE_LONGLONG,
'INT24' => MySQL_Protocol::FIELD_TYPE_INT24,
'DATE' => MySQL_Protocol::FIELD_TYPE_DATE,
'TIME' => MySQL_Protocol::FIELD_TYPE_TIME,
'DATETIME' => MySQL_Protocol::FIELD_TYPE_DATETIME,
'YEAR' => MySQL_Protocol::FIELD_TYPE_YEAR,
'NEWDATE' => MySQL_Protocol::FIELD_TYPE_NEWDATE,
'VARCHAR' => MySQL_Protocol::FIELD_TYPE_VARCHAR,
'BIT' => MySQL_Protocol::FIELD_TYPE_BIT,
'NEWDECIMAL' => MySQL_Protocol::FIELD_TYPE_NEWDECIMAL,
'ENUM' => MySQL_Protocol::FIELD_TYPE_ENUM,
'SET' => MySQL_Protocol::FIELD_TYPE_SET,
'TINY_BLOB' => MySQL_Protocol::FIELD_TYPE_TINY_BLOB,
'MEDIUM_BLOB' => MySQL_Protocol::FIELD_TYPE_MEDIUM_BLOB,
'LONG_BLOB' => MySQL_Protocol::FIELD_TYPE_LONG_BLOB,
'BLOB' => MySQL_Protocol::FIELD_TYPE_BLOB,
'VAR_STRING' => MySQL_Protocol::FIELD_TYPE_VAR_STRING,
'STRING' => MySQL_Protocol::FIELD_TYPE_STRING,
'GEOMETRY' => MySQL_Protocol::FIELD_TYPE_GEOMETRY,
);

foreach ( $column_meta as $column ) {
$type = $types[ $column['native_type'] ] ?? null;
if ( null === $type ) {
throw new Exception( 'Unknown column type: ' . $column['native_type'] );
}
$columns[] = array(
'name' => $column['name'],
'length' => $column['len'],
'type' => $type,
'flags' => 129,
'decimals' => $column['precision'],
);
}
return $columns;
}
}
Loading
Loading