Skip to content

Commit ba5b0ce

Browse files
committed
Use PDO SQLite statement as a source for MySQL proxy statement
1 parent 06f5681 commit ba5b0ce

3 files changed

Lines changed: 127 additions & 176 deletions

File tree

wp-includes/sqlite-ast/class-wp-pdo-mysql-on-sqlite.php

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,13 @@ class WP_PDO_MySQL_On_SQLite extends PDO {
469469
*/
470470
private $last_sqlite_queries = array();
471471

472+
/**
473+
* A PDO SQLite statement that represents the result of the last emulated query.
474+
*
475+
* @var PDOStatement|null
476+
*/
477+
private $last_result_statement;
478+
472479
/**
473480
* Results of the last emulated query.
474481
*
@@ -772,7 +779,8 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar
772779
if ( null === $fetch_mode ) {
773780
// When the default FETCH_BOTH is not set explicitly, additional
774781
// arguments are ignored, and the argument count is not validated.
775-
$fetch_mode = PDO::FETCH_BOTH;
782+
$fetch_mode = PDO::FETCH_BOTH;
783+
$fetch_mode_args = array();
776784
} elseif ( PDO::FETCH_COLUMN === $fetch_mode ) {
777785
if ( 3 !== $arg_count ) {
778786
throw new ArgumentCountError(
@@ -833,7 +841,7 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar
833841
$this->last_mysql_query = $query;
834842

835843
/**
836-
* Use "PDO::FETCH_NUM" fetch mode, as the "WP_PDO_Synthetic_Statement"
844+
* Use "PDO::FETCH_NUM" fetch mode, as "create_result_statement_from_data()"
837845
* expects the row data to be passed as an array of values.
838846
*
839847
* @TODO: We can remove this when we use the SQLite PDOStatements directly,
@@ -883,11 +891,20 @@ public function query( string $query, ?int $fetch_mode = null, ...$fetch_mode_ar
883891
$this->commit_wrapper_transaction();
884892
}
885893

886-
$columns = is_array( $this->last_column_meta ) ? $this->last_column_meta : array();
894+
/*
895+
* For now, create all statements from data loaded in memory. This is
896+
* a temporary solution until all queries set their result statement.
897+
*
898+
* TODO: Use "$this->last_result_statement" with an actual PDO SQLite
899+
* statement whenever possible rather than loading all data.
900+
*/
901+
$columns = is_array( $this->last_column_meta ) ? array_column( $this->last_column_meta, 'name' ) : array();
887902
$rows = is_array( $this->last_result ) ? $this->last_result : array();
888903
$affected_rows = is_int( $this->last_return_value ) ? $this->last_return_value : 0;
889904

890-
$stmt = new WP_PDO_Synthetic_Statement( $this, $columns, $rows, $affected_rows );
905+
$this->last_result_statement = $this->create_result_statement_from_data( $columns, $rows );
906+
907+
$stmt = new WP_PDO_Proxy_Statement( $this->last_result_statement, $affected_rows );
891908
$stmt->setFetchMode( $fetch_mode, ...$fetch_mode_args );
892909
return $stmt;
893910
} catch ( Throwable $e ) {
@@ -6471,6 +6488,85 @@ private function flush(): void {
64716488
$this->wrapper_transaction_type = null;
64726489
}
64736490

6491+
/**
6492+
* Create a PDO SQLite statement from the specified columns and rows.
6493+
*
6494+
* Some emulated MySQL queries don't have an SQLite counterpart and their
6495+
* result data may be generated without a corresponding SQLite statement.
6496+
* In such cases, we can generate a simple SQLite SELECT query that will
6497+
* provide us with the PDOStatement API for the given column and row data.
6498+
*
6499+
* @param array $columns The columns of the result set.
6500+
* @param array $rows The rows of the result set.
6501+
* @return PDOStatement The corresponding PDO SQLite statement.
6502+
*/
6503+
private function create_result_statement_from_data( array $columns, array $rows ): PDOStatement {
6504+
$pdo = $this->connection->get_pdo();
6505+
6506+
/*
6507+
* With 0 columns, we need to create a PDO statement that has no columns.
6508+
* This can be done using a noop INSERT statement that modifies no data.
6509+
*/
6510+
if ( 0 === count( $columns ) ) {
6511+
return $pdo->query(
6512+
sprintf(
6513+
'INSERT INTO %s (rowid) SELECT NULL WHERE FALSE',
6514+
$this->quote_sqlite_identifier( self::GLOBAL_VARIABLES_TABLE_NAME )
6515+
)
6516+
);
6517+
}
6518+
6519+
/*
6520+
* Create an SQLite statement that returns the specified columns and rows.
6521+
* This can be done using a SELECT statement in the following form:
6522+
*
6523+
* -- A dummy header row to assign correct column names.
6524+
* SELECT NULL AS `col1`, NULL AS `col2`, ... WHERE FALSE
6525+
*
6526+
* UNION ALL
6527+
*
6528+
* -- The actual data rows.
6529+
* VALUES
6530+
* (val11, val12, ...),
6531+
* (val21, val22, ...),
6532+
* ...
6533+
*/
6534+
6535+
// Construct column header row ("SELECT <column-list> WHERE FALSE").
6536+
$query = 'SELECT ';
6537+
foreach ( $columns as $i => $column ) {
6538+
$query .= $i > 0 ? ', ' : '';
6539+
$query .= 'NULL AS ' . $pdo->quote( $column );
6540+
}
6541+
$query .= ' WHERE FALSE';
6542+
6543+
// UNION ALL
6544+
if ( count( $rows ) > 0 ) {
6545+
$query .= ' UNION ALL VALUES ';
6546+
}
6547+
6548+
// Construct data rows ("VALUES <row-list>").
6549+
foreach ( $rows as $i => $row ) {
6550+
$query .= $i > 0 ? ', ' : '';
6551+
$query .= '(';
6552+
foreach ( array_values( $row ) as $j => $value ) {
6553+
$query .= $j > 0 ? ', ' : '';
6554+
if ( null === $value ) {
6555+
$query .= 'NULL';
6556+
} elseif ( is_string( $value ) && strpos( $value, "\0" ) !== false ) {
6557+
// Handle null characters; see self::translate_string_literal().
6558+
$query .= sprintf( "CAST(x'%s' AS TEXT)", bin2hex( $value ) );
6559+
} elseif ( is_string( $value ) ) {
6560+
$query .= $pdo->quote( $value );
6561+
} else {
6562+
$query .= $value;
6563+
}
6564+
}
6565+
$query .= ')';
6566+
}
6567+
return $pdo->query( $query );
6568+
}
6569+
64746570
/**
64756571
* Set results of a query() call using fetched data.
64766572
*

0 commit comments

Comments
 (0)