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
4 changes: 3 additions & 1 deletion .github/workflows/wp-tests-phpunit-run.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const expectedFailures = [
'Tests_Comment_CheckComment::test_should_return_false_when_comment_previously_approved_is_enabled_and_author_does_not_have_approved_comment',
'Tests_Comment_CheckComment::test_should_return_true_when_comment_previously_approved_is_enabled_and_author_has_approved_comment',
'Tests_Comment_CheckComment::test_should_return_false_when_comment_previously_approved_is_enabled_and_user_does_not_have_a_previously_approved_comment_with_any_email',
'Tests_DB_Charset::test_get_column_charset with data set #5',
'Tests_DB_Charset::test_get_column_charset with data set #6',
'Tests_DB_Charset::test_get_column_charset_is_mysql_undefined with data set #1',
'Tests_DB_Charset::test_get_column_charset_is_mysql_undefined with data set #2',
'Tests_DB_Charset::test_get_column_charset_is_mysql_undefined with data set #3',
Expand Down Expand Up @@ -52,7 +54,7 @@ const expectedFailures = [
'Tests_DB_Charset::test_strip_invalid_text with data set #39',
'Tests_DB_Charset::test_strip_invalid_text with data set #40',
'Tests_DB_Charset::test_strip_invalid_text with data set #41',
'Tests_DB_dbDelta::test_spatial_indices',
'Tests_DB_dbDelta::test_column_type_change',
'Tests_DB::test_mysqli_flush_sync',
'Tests_DB::test_query_value_contains_invalid_chars',
'Tests_DB::test_replace',
Expand Down
158 changes: 108 additions & 50 deletions packages/mysql-on-sqlite/src/sqlite/class-wp-mysql-on-sqlite.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ class WP_MySQL_On_SQLite extends PDO {
*/
private const MYSQL_GRAMMAR_PATH = __DIR__ . '/../mysql/mysql-grammar.php';

/**
* The minimum supported MySQL version.
*/
private const MINIMUM_MYSQL_VERSION = 50700;

/**
* The default MySQL version to emulate.
*/
const DEFAULT_MYSQL_VERSION = 80038;

/**
* The minimum required version of SQLite.
*
Expand Down Expand Up @@ -488,7 +498,7 @@ class WP_MySQL_On_SQLite extends PDO {
private $mysql_version;

/**
* The SQLite engine version.
* The emulated MySQL client library version.
*
* This is a mysqli-like property that is needed to avoid a PHP warning in
* the WordPress health info. The "WP_Debug_Data::get_wp_database()" method
Expand Down Expand Up @@ -775,6 +785,7 @@ class WP_MySQL_On_SQLite extends PDO {
* @type string|int|null $synchronous Optional. SQLite synchronous setting.
* }
*
* @throws InvalidArgumentException When the MySQL version is invalid.
* @throws WP_MySQL_On_SQLite_Exception When the driver initialization fails.
*/
public function __construct(
Expand Down Expand Up @@ -824,6 +835,16 @@ public function __construct(
$path = $args['path'] ?? ':memory:';
$db_name = $args['dbname'] ?? 'sqlite_database';

$mysql_version = $options['mysql_version'] ?? self::DEFAULT_MYSQL_VERSION;
if ( ! is_int( $mysql_version ) || $mysql_version < self::MINIMUM_MYSQL_VERSION ) {
throw new InvalidArgumentException(
sprintf(
'The "mysql_version" option must be an integer greater than or equal to %d.',
self::MINIMUM_MYSQL_VERSION
)
);
}

// Create a new SQLite connection.
$pdo_options = array_filter(
$options,
Expand Down Expand Up @@ -852,7 +873,7 @@ function ( $key ) {
}
$this->connection = new WP_SQLite_Connection( $connection_options );

$this->mysql_version = $options['mysql_version'] ?? 80038;
$this->mysql_version = $mysql_version;
$this->main_db_name = $db_name;
$this->db_name = $db_name;
$this->set_sql_modes( $this->get_default_sql_modes() );
Expand Down Expand Up @@ -909,8 +930,8 @@ function ( $key ) {
}
}

// Load SQLite version to a property used by WordPress health info.
$this->client_info = $sqlite_version;
// Load MySQL client information to a property used by WordPress health info.
$this->client_info = $this->getAttribute( PDO::ATTR_CLIENT_VERSION );

// Enable foreign keys. By default, they are off.
$this->connection->query( 'PRAGMA foreign_keys = ON' );
Expand Down Expand Up @@ -1423,11 +1444,16 @@ public function setAttribute( $attribute, $value ): bool {
*/
#[ReturnTypeWillChange]
public function getAttribute( $attribute ) {
// Return the caller's error mode instead of the exception mode used internally.
if ( PDO::ATTR_ERRMODE === $attribute ) {
if ( PDO::ATTR_DRIVER_NAME === $attribute ) {
return 'mysql';
} elseif ( PDO::ATTR_CLIENT_VERSION === $attribute ) {
return 'mysqlnd ' . $this->get_mysql_server_version_string();
} elseif ( PDO::ATTR_SERVER_VERSION === $attribute ) {
return $this->get_mysql_server_version_string();
} elseif ( PDO::ATTR_ERRMODE === $attribute ) {
// Return the caller's error mode instead of the exception mode used internally.
return $this->error_mode;
}
if ( PDO::ATTR_STRINGIFY_FETCHES === $attribute && PHP_VERSION_ID < 80200 ) {
} elseif ( PDO::ATTR_STRINGIFY_FETCHES === $attribute && PHP_VERSION_ID < 80200 ) {
// PDO SQLite cannot report this attribute before PHP 8.2.
return $this->stringify_fetches;
}
Expand Down Expand Up @@ -1546,7 +1572,7 @@ public function get_last_sqlite_queries(): array {
public function create_parser( string $query ): WP_MySQL_Parser {
$lexer = new WP_MySQL_Lexer(
$query,
80038,
$this->mysql_version,
$this->get_active_sql_mode_names()
);
$tokens = $lexer instanceof WP_MySQL_Native_Lexer
Expand Down Expand Up @@ -3247,31 +3273,7 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
$this->execute_show_tables_statement( $node );
return;
case WP_MySQL_Lexer::VARIABLES_SYMBOL:
$this->last_column_meta = array(
array(
'native_type' => 'STRING',
'pdo_type' => PDO::PARAM_STR,
'flags' => array( 'not_null' ),
'table' => 'session_variables',
'name' => 'Variable_name',
'len' => 256,
'precision' => 0,
),
array(
'native_type' => 'STRING',
'pdo_type' => PDO::PARAM_STR,
'flags' => array(),
'table' => 'session_variables',
'name' => 'Value',
'len' => 4096,
'precision' => 0,
),
);
$this->last_result_statement = $this->create_result_statement_from_data(
array_column( $this->last_column_meta, 'name' ),
array()
);
$this->found_rows = 0;
$this->execute_show_variables_statement( $node );
return;
}

Expand All @@ -3284,6 +3286,39 @@ private function execute_show_statement( WP_Parser_Node $node ): void {
);
}

/**
* Translate and execute a MySQL SHOW VARIABLES statement in SQLite.
*
* @param WP_Parser_Node $node The "showStatement" AST node.
*/
private function execute_show_variables_statement( WP_Parser_Node $node ): void {
$like_or_where = $node->get_first_child_node( 'likeOrWhere' );
if ( null !== $like_or_where ) {
$condition = $this->translate_show_like_or_where_condition( $like_or_where, 'Variable_name' );
}

$query = sprintf(
"SELECT column1 AS `Variable_name`, column2 AS `Value`
FROM (
VALUES
('version', ?),
('version_comment', ?)
)
WHERE TRUE %s
ORDER BY column1",
$condition ?? ''
);
$params = array(
$this->get_mysql_server_version_string(),
$this->get_mysql_server_version_comment(),
);

$stmt = $this->execute_sqlite_query( $query, $params );
$this->store_last_column_meta_from_statement( $stmt );
$this->last_result_statement = $stmt;
$this->found_rows = array( $query, $params );
}

/**
* Translate and execute a MySQL SHOW COLLATION statement in SQLite.
*
Expand Down Expand Up @@ -4294,15 +4329,9 @@ private function translate( $node ): ?string {
if ( 'sql_mode' === $name ) {
$value = implode( ',', $this->get_active_sql_mode_names() );
} elseif ( 'version' === $name ) {
$version = (string) $this->mysql_version;
$value = sprintf(
'%d.%d.%d',
$version[0],
substr( $version, 1, 2 ),
substr( $version, 3, 2 )
);
$value = $this->get_mysql_server_version_string();
} elseif ( 'version_comment' === $name ) {
$value = 'MySQL Community Server - GPL';
$value = $this->get_mysql_server_version_comment();
} elseif ( WP_MySQL_Lexer::SESSION_SYMBOL === $type ) {
$value = $this->session_system_variables[ $name ] ?? null;
} else {
Expand Down Expand Up @@ -5077,19 +5106,48 @@ private function translate_function_call( WP_Parser_Node $node ): string {
return 0;
}
case 'VERSION':
$version = (string) $this->mysql_version;
$value = sprintf(
'%d.%d.%d',
$version[0],
substr( $version, 1, 2 ),
substr( $version, 3, 2 )
);
return $this->quote_sqlite_value( $value );
return $this->quote_sqlite_value( $this->get_mysql_server_version_string() );
default:
return $this->translate_sequence( $node->get_children() );
}
}

/**
* Get the configured MySQL version in dotted notation.
*
* @return string The MySQL version.
*/
private function get_mysql_version_string(): string {
return sprintf(
'%d.%d.%d',
intdiv( $this->mysql_version, 10000 ),
intdiv( $this->mysql_version % 10000, 100 ),
$this->mysql_version % 100
);
}

/**
* Get the raw version string of the emulated MySQL server.
*
* @return string The MySQL server version.
*/
private function get_mysql_server_version_string(): string {
return sprintf(
'%s-mysql-on-sqlite-%s',
$this->get_mysql_version_string(),
SQLITE_DRIVER_VERSION
);
}

/**
* Get the comment identifying the emulated MySQL server implementation.
*
* @return string The MySQL server version comment.
*/
private function get_mysql_server_version_comment(): string {
return 'MySQL on SQLite';
}

/**
* Translate a MySQL datetime literal to SQLite.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/
class WP_SQLite_Driver {
/**
* The SQLite engine version.
* The emulated MySQL client library version.
*
* This is a mysqli-like property that is needed to avoid a PHP warning in
* the WordPress health info. The "WP_Debug_Data::get_wp_database()" method
Expand Down Expand Up @@ -75,7 +75,7 @@ class WP_SQLite_Driver {
public function __construct(
WP_SQLite_Connection $connection,
string $database,
int $mysql_version = 80038
int $mysql_version = WP_MySQL_On_SQLite::DEFAULT_MYSQL_VERSION
) {
$this->mysql_on_sqlite_driver = new WP_MySQL_On_SQLite(
sprintf( 'mysql-on-sqlite:dbname=%s', str_replace( ';', ';;', $database ) ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,78 @@ public function test_constructor_applies_pdo_options(): void {
$this->assertSame( PDO::ERRMODE_EXCEPTION, $driver->get_sqlite_pdo()->getAttribute( PDO::ATTR_ERRMODE ) );
}

public function test_reports_mysql_driver_name(): void {
$this->assertSame( 'mysql', $this->driver->getAttribute( PDO::ATTR_DRIVER_NAME ) );
$this->assertSame( 'sqlite', $this->driver->get_sqlite_pdo()->getAttribute( PDO::ATTR_DRIVER_NAME ) );
}

public function test_configured_mysql_version_controls_reporting_and_parsing(): void {
$driver = new WP_MySQL_On_SQLite(
'mysql-on-sqlite:path=:memory:;dbname=WordPress;',
null,
null,
array( 'mysql_version' => 50744 )
);
$server_version = '5.7.44-mysql-on-sqlite-' . SQLITE_DRIVER_VERSION;

$this->assertSame( $server_version, $driver->getAttribute( PDO::ATTR_SERVER_VERSION ) );
$this->assertSame( 'mysqlnd ' . $server_version, $driver->getAttribute( PDO::ATTR_CLIENT_VERSION ) );
$this->assertSame( 'mysqlnd ' . $server_version, $driver->client_info );
$this->assertSame( $server_version, $driver->query( 'SELECT VERSION()' )->fetchColumn() );
$this->assertSame( $server_version, $driver->query( 'SELECT @@version' )->fetchColumn() );
$this->assertEquals( 1, $driver->query( 'SELECT 1 /*!80000 + 1 */' )->fetchColumn() );
}

public function test_formats_six_digit_mysql_version(): void {
$driver = new WP_MySQL_On_SQLite(
'mysql-on-sqlite:path=:memory:;dbname=WordPress;',
null,
null,
array( 'mysql_version' => 100000 )
);
$server_version = '10.0.0-mysql-on-sqlite-' . SQLITE_DRIVER_VERSION;

$this->assertSame( $server_version, $driver->getAttribute( PDO::ATTR_SERVER_VERSION ) );
$this->assertSame( 'mysqlnd ' . $server_version, $driver->getAttribute( PDO::ATTR_CLIENT_VERSION ) );
$this->assertSame( $server_version, $driver->query( 'SELECT VERSION()' )->fetchColumn() );
$this->assertSame( $server_version, $driver->query( 'SELECT @@version' )->fetchColumn() );
}

/**
* @dataProvider data_invalid_mysql_versions
*/
public function test_rejects_invalid_mysql_version( $mysql_version ): void {
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage(
'The "mysql_version" option must be an integer greater than or equal to 50700.'
);

new WP_MySQL_On_SQLite(
'mysql-on-sqlite:path=:memory:;dbname=WordPress;',
null,
null,
array( 'mysql_version' => $mysql_version )
);
}

public function data_invalid_mysql_versions(): array {
return array(
'string' => array( '80038' ),
'float' => array( 80038.0 ),
'boolean' => array( true ),
'array' => array( array( 80038 ) ),
'below minimum' => array( 50699 ),
'zero' => array( 0 ),
'negative' => array( -80038 ),
);
}

public function test_uses_shared_default_mysql_version(): void {
$this->assertSame( 80038, WP_MySQL_On_SQLite::DEFAULT_MYSQL_VERSION );
$this->assertSame( '8.0.38-mysql-on-sqlite-' . SQLITE_DRIVER_VERSION, $this->driver->getAttribute( PDO::ATTR_SERVER_VERSION ) );
$this->assertSame( 'mysqlnd 8.0.38-mysql-on-sqlite-' . SQLITE_DRIVER_VERSION, $this->driver->getAttribute( PDO::ATTR_CLIENT_VERSION ) );
}

public function test_constructor_applies_fetch_column_default(): void {
$driver = new WP_MySQL_On_SQLite(
'mysql-on-sqlite:path=:memory:;dbname=WordPress;',
Expand Down
Loading
Loading