@@ -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