PluginProbe
SQLite Database Integration / trunk
SQLite Database Integration vtrunk
3.0.2 3.0.1 trunk 2.1.13 2.1.14 2.1.15 2.1.16 2.2.0 2.2.1 2.2.10 2.2.11 2.2.12 2.2.13 2.2.14 2.2.15 2.2.16 2.2.17 2.2.18 2.2.19 2.2.2 2.2.20 2.2.21 2.2.22 2.2.23 2.2.3 All 32 releases
← All changes | wp-includes/sqlite/class-wp-sqlite-db.php +345 -132 2.2.3trunk View file →
@@ -1,10 +1,7 @@
1 1 <?php
2 2 /**
3 3 * Extend and replace the wpdb class.
4 - *
5 - * @package wp-sqlite-integration
6 - * @since 1.0.0
7 4 */
8 5
9 6 /**
10 7 * This class extends wpdb and replaces it.
@@ -15,9 +12,9 @@
15 12
16 13 /**
17 14 * Database Handle
18 15 *
19 - * @var WP_SQLite_Translator
16 + * @var WP_MySQL_On_SQLite
20 17 */
21 18 protected $dbh;
22 19
23 20 /**
@@ -56,8 +53,22 @@
56 53 $this->charset = 'utf8mb4';
57 54 }
58 55
59 56 /**
57 + * Returns the active MySQL-on-SQLite driver.
58 + *
59 + * @return WP_MySQL_On_SQLite The active driver.
60 + * @throws RuntimeException When there is no active database connection.
61 + */
62 + public function get_driver(): WP_MySQL_On_SQLite {
63 + if ( ! $this->dbh ) {
64 + throw new RuntimeException( 'Cannot access the driver without an active database connection.' );
65 + }
66 +
67 + return $this->dbh;
68 + }
69 +
70 + /**
60 71 * Method to set character set for the database.
61 72 *
62 73 * This overrides wpdb::set_charset(), only to dummy out the MySQL function.
63 74 *
@@ -70,22 +81,64 @@
70 81 public function set_charset( $dbh, $charset = null, $collate = null ) {
71 82 }
72 83
73 84 /**
74 - * Method to get the character set for the database.
75 - * Hardcoded to utf8mb4 for now.
85 + * Retrieves the character set for the given column.
76 86 *
77 - * @param string $table The table name.
78 - * @param string $column The column name.
87 + * This overrides wpdb::get_col_charset() to enable the parent's implementation
88 + * for SQLite by temporarily setting the is_mysql flag.
79 89 *
80 - * @return string The character set.
90 + * @see wpdb::get_col_charset()
91 + *
92 + * @param string $table Table name.
93 + * @param string $column Column name.
94 + * @return string|false|WP_Error Column character set as a string. False if the column has
95 + * no character set. WP_Error object on failure.
81 96 */
82 97 public function get_col_charset( $table, $column ) {
83 - // Hardcoded for now.
84 - return 'utf8mb4';
98 + $original_is_mysql = $this->is_mysql ?? null;
99 +
100 + /*
101 + * The parent method returns early when `$this->is_mysql` is falsy.
102 + * Since SQLite doesn't set this flag, we enable it temporarily so
103 + * the parent can run its full logic — querying column metadata via
104 + * SHOW FULL COLUMNS (which the SQLite driver translates) and
105 + * populating the `$this->col_meta` cache.
106 + */
107 + try {
108 + $this->is_mysql = true;
109 + return parent::get_col_charset( $table, $column );
110 + } finally {
111 + $this->is_mysql = $original_is_mysql;
112 + }
85 113 }
86 114
87 115 /**
116 + * Retrieves the maximum string length allowed in a given column.
117 + *
118 + * This overrides wpdb::get_col_length() to enable the parent's implementation
119 + * for SQLite by temporarily setting the is_mysql flag.
120 + *
121 + * @see wpdb::get_col_length()
122 + *
123 + * @param string $table Table name.
124 + * @param string $column Column name.
125 + * @return array|false|WP_Error Column length information, false if the column has
126 + * no length. WP_Error object on failure.
127 + */
128 + public function get_col_length( $table, $column ) {
129 + $original_is_mysql = $this->is_mysql ?? null;
130 +
131 + // See get_col_charset() for an explanation of the is_mysql flag.
132 + try {
133 + $this->is_mysql = true;
134 + return parent::get_col_length( $table, $column );
135 + } finally {
136 + $this->is_mysql = $original_is_mysql;
137 + }
138 + }
139 +
140 + /**
88 141 * Changes the current SQL mode, and ensures its WordPress compatibility.
89 142 *
90 143 * If no modes are passed, it will ensure the current MySQL server modes are compatible.
91 144 *
@@ -93,14 +146,10 @@
93 146 *
94 147 * @param array $modes Optional. A list of SQL modes to set. Default empty array.
95 148 */
96 149 public function set_sql_mode( $modes = array() ) {
97 - if ( ! $this->dbh instanceof WP_SQLite_Driver ) {
98 - return;
99 - }
100 -
101 150 if ( empty( $modes ) ) {
102 - $result = $this->dbh->query( 'SELECT @@SESSION.sql_mode' );
151 + $result = $this->dbh->query( 'SELECT @@SESSION.sql_mode' )->fetchAll( PDO::FETCH_OBJ ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
103 152 if ( ! isset( $result[0] ) ) {
104 153 return;
105 154 }
106 155
@@ -133,17 +182,101 @@
133 182 }
134 183
135 184 /**
136 185 * Closes the current database connection.
137 - * Noop in SQLite.
138 186 *
139 - * @return bool True to indicate the connection was successfully closed.
187 + * This overrides wpdb::close() while closely mirroring its implementation.
188 + *
189 + * @see wpdb::close()
190 + *
191 + * @return bool True if the connection was successfully closed,
192 + * false if it wasn't, or if the connection doesn't exist.
140 193 */
141 194 public function close() {
195 + if ( ! $this->dbh ) {
196 + return false;
197 + }
198 +
199 + $connection = $this->dbh->get_connection();
200 + $pdo = $connection->get_pdo();
201 +
202 + try {
203 + if ( $this->dbh->inTransaction() ) {
204 + $this->dbh->rollBack();
205 + } elseif ( $pdo->inTransaction() ) {
206 + $pdo->rollBack();
207 + } else {
208 + /*
209 + * On PHP < 8.4, PDO cannot detect transactions started via SQL.
210 + * A savepoint ensures ROLLBACK succeeds with or without one.
211 + */
212 + $pdo->exec( 'SAVEPOINT wp_sqlite_db_close' );
213 + $pdo->exec( 'ROLLBACK' );
214 + }
215 + } catch ( Throwable $e ) {
216 + return false;
217 + }
218 +
219 + if (
220 + isset( $GLOBALS['@pdo'] )
221 + && $GLOBALS['@pdo'] === $pdo
222 + ) {
223 + unset( $GLOBALS['@pdo'] );
224 + }
225 +
226 + $connection->set_query_logger( null );
227 + $this->result = null;
228 + $this->dbh = null;
229 + $this->ready = false;
230 + $this->has_connected = false;
231 +
142 232 return true;
143 233 }
144 234
145 235 /**
236 + * Determines the best charset and collation to use given a charset and collation.
237 + *
238 + * For example, when able, utf8mb4 should be used instead of utf8.
239 + *
240 + * This overrides wpdb::determine_charset() while closely mirroring its implementation.
241 + * The override is needed because the parent checks for a mysqli connection object.
242 + *
243 + * @param string $charset The character set to check.
244 + * @param string $collate The collation to check.
245 + * @return array {
246 + * The most appropriate character set and collation to use.
247 + *
248 + * @type string $charset Character set.
249 + * @type string $collate Collation.
250 + * }
251 + */
252 + public function determine_charset( $charset, $collate ) {
253 + if ( ! $this->dbh ) {
254 + return compact( 'charset', 'collate' );
255 + }
256 +
257 + if ( 'utf8' === $charset ) {
258 + $charset = 'utf8mb4';
259 + }
260 +
261 + if ( 'utf8mb4' === $charset ) {
262 + // _general_ is outdated, so we can upgrade it to _unicode_, instead.
263 + if ( ! $collate || 'utf8_general_ci' === $collate ) {
264 + $collate = 'utf8mb4_unicode_ci';
265 + } else {
266 + $collate = str_replace( 'utf8_', 'utf8mb4_', $collate );
267 + }
268 + }
269 +
270 + // _unicode_520_ is a better collation, we should use that when it's available.
271 + if ( $this->has_cap( 'utf8mb4_520' ) && 'utf8mb4_unicode_ci' === $collate ) {
272 + $collate = 'utf8mb4_unicode_520_ci';
273 + }
274 +
275 + return compact( 'charset', 'collate' );
276 + }
277 +
278 + /**
146 279 * Method to select the database connection.
147 280 *
148 281 * This overrides wpdb::select(), only to dummy out the MySQL function.
149 282 *
@@ -165,37 +298,23 @@
165 298 *
166 299 * @param string $data The string to escape.
167 300 *
168 301 * @return string escaped
302 + * @throws RuntimeException When the database connection is not initialized.
169 303 */
170 304 public function _real_escape( $data ) {
171 305 if ( ! is_scalar( $data ) ) {
172 306 return '';
173 307 }
174 - $escaped = addslashes( $data );
175 - return $this->add_placeholder_escape( $escaped );
176 - }
177 308
178 - /**
179 - * Method to dummy out wpdb::esc_like() function.
180 - *
181 - * WordPress 4.0.0 introduced esc_like() function that adds backslashes to %,
182 - * underscore and backslash, which is not interpreted as escape character
183 - * by SQLite. So we override it and dummy out this function.
184 - *
185 - * @param string $text The raw text to be escaped. The input typed by the user should have no
186 - * extra or deleted slashes.
187 - *
188 - * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare()
189 - * or real_escape next.
190 - */
191 - public function esc_like( $text ) {
192 - // The new driver adds "ESCAPE '\\'" to every LIKE expression by default.
193 - // We only need to overload this function to a no-op for the old driver.
194 - if ( $this->dbh instanceof WP_SQLite_Driver ) {
195 - return parent::esc_like( $text );
309 + if ( ! $this->dbh ) {
310 + throw new RuntimeException( 'Cannot escape data without an active database connection.' );
196 311 }
197 - return $text;
312 +
313 + // Escape the string without bounding quotes to mirror mysqli_real_escape_string().
314 + $quoted = $this->dbh->quote( (string) $data );
315 + $escaped = substr( $quoted, 1, -1 );
316 + return $this->add_placeholder_escape( $escaped );
198 317 }
199 318
200 319 /**
201 320 * Prints SQL/DB error.
@@ -293,79 +412,108 @@
293 412 *
294 413 * @see wpdb::db_connect()
295 414 *
296 415 * @param bool $allow_bail Not used.
297 - * @return void
416 + * @return bool True on a successful connection, false on failure.
298 417 */
299 418 public function db_connect( $allow_bail = true ) {
300 419 if ( $this->dbh ) {
301 - return;
420 + return $this->ready;
302 421 }
303 - $this->init_charset();
304 422
305 - $pdo = null;
423 + $this->last_error = '';
306 424 if ( isset( $GLOBALS['@pdo'] ) ) {
307 - $pdo = $GLOBALS['@pdo'];
425 + trigger_error(
426 + 'PDO injection via $GLOBALS[\'@pdo\'] is no longer supported. The existing PDO will be ignored and a new connection will be created.',
427 + E_USER_WARNING
428 + );
308 429 }
309 - if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) {
310 - if ( null === $this->dbname || '' === $this->dbname ) {
311 - $this->bail(
312 - 'The database name was not set. The SQLite driver requires a database name to be set to emulate MySQL information schema tables.',
313 - 'db_connect_fail'
314 - );
315 - return false;
430 +
431 + if ( ! isset( $this->charset ) ) {
432 + $this->init_charset();
433 + }
434 +
435 + // Migrate the database file from a legacy path, if it exists.
436 + if ( ! defined( 'DB_FILE' ) && ! file_exists( FQDB ) ) {
437 + $old_db_path = FQDBDIR . '.ht.sqlite.php';
438 +
439 + if ( file_exists( $old_db_path ) ) {
440 + if ( ! rename( $old_db_path, FQDB ) ) {
441 + wp_die( 'Failed to rename database file.', 'Error!' );
442 + }
443 +
444 + foreach ( array( '-wal', '-shm', '-journal' ) as $suffix ) {
445 + if ( file_exists( $old_db_path . $suffix ) ) {
446 + if ( ! rename( $old_db_path . $suffix, FQDB . $suffix ) ) {
447 + wp_die( 'Failed to rename database file.', 'Error!' );
448 + }
449 + }
450 + }
316 451 }
452 + }
317 453
318 - require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-grammar.php';
319 - require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser.php';
320 - require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-node.php';
321 - require_once __DIR__ . '/../../wp-includes/parser/class-wp-parser-token.php';
322 - require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php';
323 - require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php';
324 - require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php';
325 - require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-connection.php';
326 - require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php';
327 - require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php';
328 - require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php';
329 - require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php';
330 - require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-exception.php';
331 - require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php';
332 - $this->ensure_database_directory( FQDB );
454 + if ( null === $this->dbname || '' === $this->dbname ) {
455 + $this->bail(
456 + 'The database name was not set. The SQLite driver requires a database name to be set to emulate MySQL information schema tables.',
457 + 'db_connect_fail'
458 + );
459 + return false;
460 + }
333 461
334 - try {
335 - $connection = new WP_SQLite_Connection(
336 - array(
337 - 'pdo' => $pdo,
338 - 'path' => FQDB,
339 - 'journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
340 - )
341 - );
342 - $this->dbh = new WP_SQLite_Driver( $connection, $this->dbname );
343 - $GLOBALS['@pdo'] = $this->dbh->get_connection()->get_pdo();
344 - } catch ( Throwable $e ) {
345 - $this->last_error = $this->format_error_message( $e );
346 - }
347 - } else {
348 - $this->dbh = new WP_SQLite_Translator( $pdo );
349 - $this->last_error = $this->dbh->get_error_message();
350 - $GLOBALS['@pdo'] = $this->dbh->get_pdo();
462 + $this->ensure_database_directory( FQDB );
463 +
464 + try {
465 + $options = array(
466 + 'sqlite_journal_mode' => defined( 'SQLITE_JOURNAL_MODE' ) ? SQLITE_JOURNAL_MODE : null,
467 + );
468 + $dbh = new WP_MySQL_On_SQLite(
469 + sprintf(
470 + 'mysql-on-sqlite:path=%s;dbname=%s',
471 + str_replace( ';', ';;', FQDB ),
472 + str_replace( ';', ';;', $this->dbname )
473 + ),
474 + null,
475 + null,
476 + $options
477 + );
478 + $dbh->setAttribute( PDO::ATTR_STRINGIFY_FETCHES, true ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
479 + $this->dbh = $dbh;
480 +
481 + /**
482 + * Exposes the underlying PDO SQLite connection for backward compatibility.
483 + *
484 + * @deprecated 3.0.0 Use WP_SQLite_DB::get_driver() with
485 + * WP_MySQL_On_SQLite::get_sqlite_pdo() instead.
486 + */
487 + $GLOBALS['@pdo'] = $dbh->get_sqlite_pdo();
488 + } catch ( Throwable $e ) {
489 + $this->last_error = $this->format_error_message( $e );
351 490 }
352 491 if ( $this->last_error ) {
353 492 return false;
354 493 }
494 +
495 + $this->has_connected = true;
496 + $this->set_charset( $this->dbh );
497 +
355 498 $this->ready = true;
356 499 $this->set_sql_mode();
500 + return true;
357 501 }
358 502
359 503 /**
360 - * Method to dummy out wpdb::check_connection()
504 + * Checks that the database connection is available.
361 505 *
362 506 * @param bool $allow_bail Not used.
363 507 *
364 - * @return bool
508 + * @return bool True when the connection is available, false otherwise.
365 509 */
366 510 public function check_connection( $allow_bail = true ) {
367 - return true;
511 + if ( $this->dbh ) {
512 + return true;
513 + }
514 +
515 + return $this->db_connect( $allow_bail );
368 516 }
369 517
370 518 /**
371 519 * Prepares a SQL query for safe execution.
@@ -407,9 +555,16 @@
407 555 * @return int|bool Boolean true for CREATE, ALTER, TRUNCATE and DROP queries. Number of rows
408 556 * affected/selected for all other queries. Boolean false on error.
409 557 */
410 558 public function query( $query ) {
559 + // Query Monitor integration:
560 + $query_monitor_active = defined( 'SQLITE_QUERY_MONITOR_LOADED' ) && SQLITE_QUERY_MONITOR_LOADED;
561 + if ( $query_monitor_active && $this->show_errors ) {
562 + $this->hide_errors();
563 + }
564 +
411 565 if ( ! $this->ready ) {
566 + $this->check_current_query = true;
412 567 return false;
413 568 }
414 569
415 570 $query = apply_filters( 'query', $query );
@@ -423,21 +578,32 @@
423 578
424 579 // Log how the function was called.
425 580 $this->func_call = "\$db->query(\"$query\")";
426 581
582 + /*
583 + * Mirror wpdb's query text validation.
584 + * TODO: Add full charset enforcement to MySQL on SQLite, where column
585 + * types and SQL mode are known, so all callers are protected.
586 + */
587 + if ( $this->check_current_query && ! $this->check_ascii( $query ) ) {
588 + $stripped_query = $this->strip_invalid_text_from_query( $query );
589 + // Charset discovery can run queries, so clear their results.
590 + $this->flush();
591 + if ( $stripped_query !== $query ) {
592 + $this->insert_id = 0;
593 + $this->last_query = $query;
594 + wp_load_translations_early();
595 + $this->last_error = __( 'WordPress database error: Could not perform query because it contains invalid data.' );
596 + return false;
597 + }
598 + }
599 + $this->check_current_query = true;
600 +
427 601 // Keep track of the last query for debug.
428 602 $this->last_query = $query;
429 603
430 - /*
431 - * @TODO: WPDB uses "$this->check_current_query" to check table/column
432 - * charset and strip all invalid characters from the query.
433 - * This is an involved process that we can bypass for SQLite,
434 - * if we simply strip all invalid UTF-8 characters from the query.
435 - *
436 - * To do so, mb_convert_encoding can be used with an optional
437 - * fallback to a htmlspecialchars method. E.g.:
438 - * https://github.com/nette/utils/blob/be534713c227aeef57ce1883fc17bc9f9e29eca2/src/Utils/Strings.php#L42
439 - */
604 + // Save the query count after any charset discovery queries.
605 + $last_query_count = count( $this->queries ?? array() );
440 606 $this->_do_query( $query );
441 607
442 608 if ( $this->last_error ) {
443 609 // Clear insert_id on a subsequent failed insert.
@@ -451,17 +617,13 @@
451 617
452 618 if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
453 619 $return_val = true;
454 620 } elseif ( preg_match( '/^\s*(insert|delete|update|replace)\s/i', $query ) ) {
455 - if ( $this->dbh instanceof WP_SQLite_Driver ) {
456 - $this->rows_affected = $this->dbh->get_last_return_value();
457 - } else {
458 - $this->rows_affected = $this->dbh->get_affected_rows();
459 - }
621 + $this->rows_affected = $this->result->rowCount();
460 622
461 623 // Take note of the insert_id.
462 624 if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
463 - $this->insert_id = $this->dbh->get_insert_id();
625 + $this->insert_id = (int) $this->dbh->lastInsertId();
464 626 }
465 627
466 628 // Return number of rows affected.
467 629 $return_val = $this->rows_affected;
@@ -467,11 +629,11 @@
467 629 $return_val = $this->rows_affected;
468 630 } else {
469 631 $num_rows = 0;
470 632
471 - if ( is_array( $this->result ) ) {
472 - $this->last_result = $this->result;
473 - $num_rows = count( $this->result );
633 + if ( $this->result->columnCount() > 0 ) {
634 + $this->last_result = $this->result->fetchAll();
635 + $num_rows = count( $this->last_result );
474 636 }
475 637
476 638 // Log and return the number of rows selected.
477 639 $this->num_rows = $num_rows;
@@ -477,8 +639,33 @@
477 639 $this->num_rows = $num_rows;
478 640 $return_val = $num_rows;
479 641 }
480 642
643 + // Query monitor integration:
644 + if ( $query_monitor_active && class_exists( 'QM_Backtrace' ) ) {
645 + if ( did_action( 'qm/cease' ) ) {
646 + $this->queries = array();
647 + }
648 +
649 + $i = $last_query_count;
650 + if ( ! isset( $this->queries[ $i ] ) ) {
651 + return $return_val;
652 + }
653 +
654 + $this->queries[ $i ]['trace'] = new QM_Backtrace();
655 + if ( ! isset( $this->queries[ $i ][3] ) ) {
656 + $this->queries[ $i ][3] = $this->time_start;
657 + }
658 +
659 + if ( $this->last_error && ! $this->suppress_errors ) {
660 + $this->queries[ $i ]['result'] = new WP_Error( 'qmdb', $this->last_error );
661 + } else {
662 + $this->queries[ $i ]['result'] = (int) $return_val;
663 + }
664 +
665 + // Add SQLite query data.
666 + $this->queries[ $i ]['sqlite_queries'] = $this->dbh->get_last_sqlite_queries();
667 + }
481 668 return $return_val;
482 669 }
483 670
484 671 /**
@@ -495,17 +682,13 @@
495 682 $this->timer_start();
496 683 }
497 684
498 685 try {
499 - $this->result = $this->dbh->query( $query );
686 + $this->result = $this->dbh->query( $query, PDO::FETCH_OBJ ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
500 687 } catch ( Throwable $e ) {
501 688 $this->last_error = $this->format_error_message( $e );
502 689 }
503 690
504 - if ( $this->dbh instanceof WP_SQLite_Translator ) {
505 - $this->last_error = $this->dbh->get_error_message();
506 - }
507 -
508 691 ++$this->num_queries;
509 692
510 693 if ( defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
511 694 $this->log_query(
@@ -528,49 +711,79 @@
528 711 protected function load_col_info() {
529 712 if ( $this->col_info ) {
530 713 return;
531 714 }
532 - $this->col_info = $this->dbh->get_columns();
715 + $this->col_info = array();
716 + if ( null === $this->result ) {
717 + return;
718 + }
719 + for ( $i = 0; $i < $this->result->columnCount(); $i++ ) {
720 + $column = $this->result->getColumnMeta( $i );
721 + $this->col_info[] = (object) array(
722 + 'name' => $column['name'],
723 + 'orgname' => $column['mysqli:orgname'],
724 + 'table' => $column['table'],
725 + 'orgtable' => $column['mysqli:orgtable'],
726 + 'def' => '', // Unused, always ''.
727 + 'db' => $column['mysqli:db'],
728 + 'catalog' => 'def', // Unused, always 'def'.
729 + 'max_length' => 0, // As of PHP 8.1, this is always 0.
730 + 'length' => $column['len'],
731 + 'charsetnr' => $column['mysqli:charsetnr'],
732 + 'flags' => $column['mysqli:flags'],
733 + 'type' => $column['mysqli:type'],
734 + 'decimals' => $column['precision'],
735 + );
736 + }
533 737 }
534 738
535 739 /**
536 - * Method to return what the database can do.
740 + * Determines whether the database supports a given feature.
537 741 *
538 - * This overrides wpdb::has_cap() to avoid using MySQL functions.
539 - * SQLite supports subqueries, but not support collation, group_concat and set_charset.
742 + * The utf8mb4 check is handled here because older WordPress versions inspect
743 + * the MySQL client library. All other capabilities use the parent logic.
540 744 *
541 745 * @see wpdb::has_cap()
542 746 *
543 - * @param string $db_cap The feature to check for. Accepts 'collation',
544 - * 'group_concat', 'subqueries', 'set_charset',
545 - * 'utf8mb4', or 'utf8mb4_520'.
546 - *
547 - * @return bool Whether the database feature is supported, false otherwise.
747 + * @param string $db_cap The feature to check for.
748 + * @return bool True when the database feature is supported, false otherwise.
548 749 */
549 750 public function has_cap( $db_cap ) {
550 - return 'subqueries' === strtolower( $db_cap );
751 + if ( 'utf8mb4' === strtolower( $db_cap ) ) {
752 + return true;
753 + }
754 +
755 + return parent::has_cap( $db_cap );
551 756 }
552 757
553 758 /**
554 - * Method to return database version number.
759 + * Retrieves the emulated database server version number.
555 760 *
556 - * This overrides wpdb::db_version() to avoid using MySQL function.
557 - * It returns mysql version number, but it means nothing for SQLite.
558 - * So it return the newest mysql version.
761 + * This mirrors wpdb::db_version(), but must also be defined here because
762 + * WordPress 5.4 and older fetch server information directly from the MySQL
763 + * extension instead of delegating to wpdb::db_server_info().
559 764 *
560 765 * @see wpdb::db_version()
766 + *
767 + * @return string Version number on success, or an empty string while disconnected.
561 768 */
562 769 public function db_version() {
563 - return '8.0';
770 + return preg_replace( '/[^0-9.].*/', '', $this->db_server_info() );
564 771 }
565 772
566 773 /**
567 - * Returns the version of the SQLite engine.
774 + * Returns the raw version string of the emulated MySQL server.
568 775 *
569 - * @return string SQLite engine version as a string.
776 + * @see wpdb::db_server_info()
777 + *
778 + * @return string Emulated MySQL server version, or an empty string while disconnected.
570 779 */
571 780 public function db_server_info() {
572 - return $this->dbh->get_sqlite_version();
781 + if ( ! $this->dbh ) {
782 + return '';
783 + }
784 +
785 + return $this->dbh->getAttribute( PDO::ATTR_SERVER_VERSION ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__PDO
573 786 }
574 787
575 788 /**
576 789 * Make sure the SQLite database directory exists and is writable.
@@ -620,9 +833,9 @@
620 833 }
621 834
622 835
623 836 /**
624 - * Format SQLite driver error message.
837 + * Format MySQL-on-SQLite driver error message.
625 838 *
626 839 * @return string
627 840 */
628 841 private function format_error_message( Throwable $e ) {
@@ -628,10 +841,10 @@
628 841 private function format_error_message( Throwable $e ) {
629 842 $output = '<div style="clear:both">&nbsp;</div>' . PHP_EOL;
630 843
631 844 // Queries.
632 - if ( $e instanceof WP_SQLite_Driver_Exception ) {
633 - $driver = $e->getDriver();
845 + if ( $e instanceof WP_MySQL_On_SQLite_Exception ) {
846 + $driver = $e->get_driver();
634 847
635 848 $output .= '<div class="queries" style="clear:both;margin-bottom:2px;border:red dotted thin;">' . PHP_EOL;
636 849 $output .= '<p>MySQL query:</p>' . PHP_EOL;
637 850 $output .= '<p>' . $driver->get_last_mysql_query() . '</p>' . PHP_EOL;