diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad56e6c5..d7105c045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] Unreleased +### Changed + +- Updated `sqlite-integration-plugin` and Core PHPUnit suite files. + ## [3.5.7] 2024-04-12; ### Changed diff --git a/includes/core-phpunit/includes/abstract-testcase.php b/includes/core-phpunit/includes/abstract-testcase.php index 877a7bb62..1e448598e 100644 --- a/includes/core-phpunit/includes/abstract-testcase.php +++ b/includes/core-phpunit/includes/abstract-testcase.php @@ -1647,7 +1647,7 @@ protected function update_post_modified( $post_id, $date ) { /** * Touches the given file and its directory if it doesn't already exist. * - * This can be used to ensure a file that is implictly relied on in a test exists + * This can be used to ensure a file that is implicitly relied on in a test exists * without it having to be built. * * @param string $file The file name. diff --git a/includes/core-phpunit/includes/factory/class-wp-unittest-factory-for-thing.php b/includes/core-phpunit/includes/factory/class-wp-unittest-factory-for-thing.php index adc0a4cc7..786e70bdc 100644 --- a/includes/core-phpunit/includes/factory/class-wp-unittest-factory-for-thing.php +++ b/includes/core-phpunit/includes/factory/class-wp-unittest-factory-for-thing.php @@ -155,7 +155,7 @@ public function create_many( $count, $args = array(), $generation_definitions = * @param array|null $callbacks Optional. Array with callbacks to apply on the fields. * Default null. * - * @return array|WP_Error Combined array on success. WP_Error when default value is incorrent. + * @return array|WP_Error Combined array on success. WP_Error when default value is incorrect. */ public function generate_args( $args = array(), $generation_definitions = null, &$callbacks = null ) { $callbacks = array(); diff --git a/includes/core-phpunit/includes/normalize-xml.xsl b/includes/core-phpunit/includes/normalize-xml.xsl index 135556c61..cb6f9f6d2 100644 --- a/includes/core-phpunit/includes/normalize-xml.xsl +++ b/includes/core-phpunit/includes/normalize-xml.xsl @@ -3,7 +3,7 @@ Normalize an XML document to make it easier to compare whether 2 documents will be seen as "equal" to an XML processor. - The normalization is similiar, in spirit, to {@link https://www.w3.org/TR/xml-c14n11/ Canonical XML}, + The normalization is similar, in spirit, to {@link https://www.w3.org/TR/xml-c14n11/ Canonical XML}, but without some aspects of C14N that make the kinds of assertions we need difficult. For example, the following XML documents will be interpreted the same by an XML processor, @@ -23,7 +23,7 @@ > diff --git a/includes/core-phpunit/includes/testcase-ajax.php b/includes/core-phpunit/includes/testcase-ajax.php index de1205a78..aeed4c9d8 100644 --- a/includes/core-phpunit/includes/testcase-ajax.php +++ b/includes/core-phpunit/includes/testcase-ajax.php @@ -136,7 +136,7 @@ public static function set_up_before_class() { /** * Sets up the test fixture. * - * Overrides wp_die(), pretends to be Ajax, and suppresses E_WARNINGs. + * Overrides wp_die(), pretends to be Ajax, and suppresses warnings. */ public function set_up() { parent::set_up(); @@ -164,7 +164,7 @@ public function tear_down() { $_GET = array(); unset( $GLOBALS['post'] ); unset( $GLOBALS['comment'] ); - remove_filter( 'wp_die_ajax_handler', array( $this, 'getDieHandler' ), 1, 1 ); + remove_filter( 'wp_die_ajax_handler', array( $this, 'getDieHandler' ), 1 ); remove_action( 'clear_auth_cookie', array( $this, 'logout' ) ); error_reporting( $this->_error_level ); set_current_screen( 'front' ); diff --git a/includes/core-phpunit/includes/testcase-rest-api.php b/includes/core-phpunit/includes/testcase-rest-api.php index 144bcc93c..a9b9f6e9e 100644 --- a/includes/core-phpunit/includes/testcase-rest-api.php +++ b/includes/core-phpunit/includes/testcase-rest-api.php @@ -3,19 +3,31 @@ namespace lucatume\WPBrowser\TestCase; abstract class WPRestApiTestCase extends WPTestCase { - protected function assertErrorResponse( $code, $response, $status = null ) { + + /** + * Asserts that the REST API response has the specified error. + * + * @since 4.4.0 + * @since 6.6.0 Added the `$message` parameter. + * + * @param string|int $code Expected error code. + * @param WP_REST_Response|WP_Error $response REST API response. + * @param int $status Optional. Status code. + * @param string $message Optional. Message to display when the assertion fails. + */ + protected function assertErrorResponse( $code, $response, $status = null, $message = '' ) { if ( $response instanceof \WP_REST_Response ) { $response = $response->as_error(); } - $this->assertWPError( $response ); - $this->assertSame( $code, $response->get_error_code() ); + $this->assertWPError( $response, $message . ' Passed $response is not a WP_Error object.' ); + $this->assertSame( $code, $response->get_error_code(), $message . ' The expected error code does not match.' ); if ( null !== $status ) { $data = $response->get_error_data(); - $this->assertArrayHasKey( 'status', $data ); - $this->assertSame( $status, $data['status'] ); + $this->assertArrayHasKey( 'status', $data, $message . ' Passed $response does not include a status code.' ); + $this->assertSame( $status, $data['status'], $message . ' The expected status code does not match.' ); } } } diff --git a/includes/sqlite-database-integration/admin-page.php b/includes/sqlite-database-integration/admin-page.php index f0528e886..6a44c0e7d 100644 --- a/includes/sqlite-database-integration/admin-page.php +++ b/includes/sqlite-database-integration/admin-page.php @@ -58,25 +58,25 @@ function sqlite_integration_admin_screen() {
-

+

+ ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' + ); + ?> +

+
+ ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' ); ?> -

- -
- ' . esc_html( basename( WP_CONTENT_DIR ) ) . '/db.php' - ); - ?> - +

diff --git a/includes/sqlite-database-integration/load.php b/includes/sqlite-database-integration/load.php index ddc5a9b6c..c80dcdd7b 100644 --- a/includes/sqlite-database-integration/load.php +++ b/includes/sqlite-database-integration/load.php @@ -3,7 +3,7 @@ * Plugin Name: SQLite Database Integration * Description: SQLite database driver drop-in. * Author: The WordPress Team - * Version: 2.1.7 + * Version: 2.1.10 * Requires PHP: 7.0 * Textdomain: sqlite-database-integration * @@ -16,6 +16,7 @@ define('SQLITE_MAIN_FILE', __FILE__); } +require_once __DIR__ . '/php-polyfills.php'; require_once __DIR__ . '/admin-page.php'; require_once __DIR__ . '/activate.php'; require_once __DIR__ . '/deactivate.php'; diff --git a/includes/sqlite-database-integration/php-polyfills.php b/includes/sqlite-database-integration/php-polyfills.php new file mode 100644 index 000000000..89d6d1a76 --- /dev/null +++ b/includes/sqlite-database-integration/php-polyfills.php @@ -0,0 +1,54 @@ + 'release_lock', 'ucase' => 'ucase', 'lcase' => 'lcase', + 'unhex' => 'unhex', 'inet_ntoa' => 'inet_ntoa', 'inet_aton' => 'inet_aton', 'datediff' => 'datediff', @@ -633,6 +634,21 @@ public function lcase( $content ) { return "lower($content)"; } + /** + * Method to emulate MySQL UNHEX() function. + * + * For a string argument str, UNHEX(str) interprets each pair of characters + * in the argument as a hexadecimal number and converts it to the byte represented + * by the number. The return value is a binary string. + * + * @param string $number Number to be unhexed. + * + * @return string Binary string + */ + public function unhex( $number ) { + return pack( 'H*', $number ); + } + /** * Method to emulate MySQL INET_NTOA() function. * diff --git a/includes/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php b/includes/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php index e5cc54c02..52bb16a3b 100644 --- a/includes/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/includes/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1040,7 +1040,7 @@ private function parse_mysql_create_table_field() { $result->name = ''; $result->sqlite_data_type = ''; $result->not_null = false; - $result->default = null; + $result->default = false; $result->auto_increment = false; $result->primary_key = false; @@ -1054,7 +1054,7 @@ private function parse_mysql_create_table_field() { $result->sqlite_data_type = $skip_mysql_data_type_parts[0]; $result->mysql_data_type = $skip_mysql_data_type_parts[1]; - // Look for the NOT NULL and AUTO_INCREMENT flags. + // Look for the NOT NULL, PRIMARY KEY, DEFAULT, and AUTO_INCREMENT flags. while ( true ) { $token = $this->rewriter->skip(); if ( ! $token ) { @@ -1123,8 +1123,30 @@ private function make_sqlite_field_definition( $field ) { if ( $field->not_null ) { $definition .= ' NOT NULL'; } - if ( null !== $field->default ) { + /** + * WPDB removes the STRICT_TRANS_TABLES mode from MySQL queries. + * This mode allows the use of `NULL` when NOT NULL is set on a column that falls back to DEFAULT. + * SQLite does not support this behavior, so we need to add the `ON CONFLICT REPLACE` clause to the column definition. + */ + if ( $field->not_null ) { + $definition .= ' ON CONFLICT REPLACE'; + } + /** + * The value of DEFAULT can be NULL. PHP would print this as an empty string, so we need a special case for it. + */ + if ( null === $field->default ) { + $definition .= ' DEFAULT NULL'; + } elseif ( false !== $field->default ) { $definition .= ' DEFAULT ' . $field->default; + } elseif ( $field->not_null ) { + /** + * If the column is NOT NULL, we need to provide a default value to match WPDB behavior caused by removing the STRICT_TRANS_TABLES mode. + */ + if ( 'text' === $field->sqlite_data_type ) { + $definition .= ' DEFAULT \'\''; + } elseif ( in_array( $field->sqlite_data_type, array( 'integer', 'real' ), true ) ) { + $definition .= ' DEFAULT 0'; + } } /* @@ -1416,6 +1438,10 @@ private function execute_select() { continue; } + if ( $this->skip_index_hint() ) { + continue; + } + $this->rewriter->consume(); } $this->rewriter->consume_all(); @@ -1427,8 +1453,9 @@ private function execute_select() { $updated_query = $this->get_information_schema_query( $updated_query ); $params = array(); } elseif ( - strpos( $updated_query, '@@SESSION.sql_mode' ) !== false - || strpos( $updated_query, 'CONVERT( ' ) !== false + // Examples: @@SESSION.sql_mode, @@GLOBAL.max_allowed_packet, @@character_set_client + preg_match( '/@@((SESSION|GLOBAL)\s*\.\s*)?\w+\b/i', $updated_query ) === 1 || + strpos( $updated_query, 'CONVERT( ' ) !== false ) { /* * If the query contains a function that is not supported by SQLite, @@ -1468,6 +1495,71 @@ private function execute_select() { } } + /** + * Ignores the FORCE INDEX clause + * + * USE {INDEX|KEY} + * [FOR {JOIN|ORDER BY|GROUP BY}] ([index_list]) + * | {IGNORE|FORCE} {INDEX|KEY} + * [FOR {JOIN|ORDER BY|GROUP BY}] (index_list) + * + * @see https://dev.mysql.com/doc/refman/8.3/en/index-hints.html + * @return bool + */ + private function skip_index_hint() { + $force = $this->rewriter->peek(); + if ( ! $force || ! $force->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'USE', 'FORCE', 'IGNORE' ) + ) ) { + return false; + } + + $index = $this->rewriter->peek_nth( 2 ); + if ( ! $index || ! $index->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'INDEX', 'KEY' ) + ) ) { + return false; + } + + $this->rewriter->skip(); // USE, FORCE, IGNORE. + $this->rewriter->skip(); // INDEX, KEY. + + $maybe_for = $this->rewriter->peek(); + if ( $maybe_for && $maybe_for->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'FOR' ) + ) ) { + $this->rewriter->skip(); // FOR. + + $token = $this->rewriter->peek(); + if ( $token && $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED, + array( 'JOIN', 'ORDER', 'GROUP' ) + ) ) { + $this->rewriter->skip(); // JOIN, ORDER, GROUP. + if ( 'BY' === strtoupper( $this->rewriter->peek()->value ) ) { + $this->rewriter->skip(); // BY. + } + } + } + + // Skip everything until the closing parenthesis. + $this->rewriter->skip( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ')', + ) + ); + + return true; + } + /** * Executes a TRUNCATE statement. */ @@ -1493,7 +1585,23 @@ private function execute_truncate() { private function execute_describe() { $this->rewriter->skip(); $this->table_name = $this->rewriter->consume()->value; - $stmt = $this->execute_sqlite_query( + $this->set_results_from_fetched_data( + $this->describe( $this->table_name ) + ); + if ( ! $this->results ) { + throw new PDOException( 'Table not found' ); + } + } + + /** + * Executes a SELECT statement. + * + * @param string $table_name The table name. + * + * @return array + */ + private function describe( $table_name ) { + return $this->execute_sqlite_query( "SELECT `name` as `Field`, ( @@ -1502,7 +1610,7 @@ private function execute_describe() { WHEN 1 THEN 'NO' END ) as `Null`, - IFNULL( + COALESCE( d.`mysql_type`, ( CASE `type` @@ -1522,34 +1630,70 @@ private function execute_describe() { ELSE 'PRI' END ) as `Key` - FROM pragma_table_info(\"$this->table_name\") p + FROM pragma_table_info(\"$table_name\") p LEFT JOIN " . self::DATA_TYPES_CACHE_TABLE . " d - ON d.`table` = \"$this->table_name\" + ON d.`table` = \"$table_name\" AND d.`column_or_index` = p.`name` ; " - ); - $this->set_results_from_fetched_data( - $stmt->fetchAll( $this->pdo_fetch_mode ) - ); - if ( ! $this->results ) { - throw new PDOException( 'Table not found' ); - } + ) + ->fetchAll( $this->pdo_fetch_mode ); } /** * Executes an UPDATE statement. + * Supported syntax: + * + * UPDATE [LOW_PRIORITY] [IGNORE] table_reference + * SET assignment_list + * [WHERE where_condition] + * [ORDER BY ...] + * [LIMIT row_count] + * + * @see https://dev.mysql.com/doc/refman/8.0/en/update.html */ private function execute_update() { - $this->rewriter->consume(); // Update. - - $params = array(); + $this->rewriter->consume(); // Consume the UPDATE keyword. + $has_where = false; + $needs_closing_parenthesis = false; + $params = array(); while ( true ) { $token = $this->rewriter->peek(); if ( ! $token ) { break; } + /* + * If the query contains a WHERE clause, + * we need to rewrite the query to use a nested SELECT statement. + * eg: + * - UPDATE table SET column = value WHERE condition LIMIT 1; + * will be rewritten to: + * - UPDATE table SET column = value WHERE rowid IN (SELECT rowid FROM table WHERE condition LIMIT 1); + */ + if ( 0 === $this->rewriter->depth ) { + if ( ( 'LIMIT' === $token->value || 'ORDER' === $token->value ) && ! $has_where ) { + $this->rewriter->add( + new WP_SQLite_Token( 'WHERE', WP_SQLite_Token::TYPE_KEYWORD ) + ); + $needs_closing_parenthesis = true; + $this->preface_where_clause_with_a_subquery(); + } elseif ( 'WHERE' === $token->value ) { + $has_where = true; + $needs_closing_parenthesis = true; + $this->rewriter->consume(); + $this->preface_where_clause_with_a_subquery(); + $this->rewriter->add( + new WP_SQLite_Token( 'WHERE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) + ); + } + } + + // Ignore the semicolon in case of rewritten query as it breaks the query. + if ( ';' === $this->rewriter->peek()->value && $this->rewriter->peek()->type === WP_SQLite_Token::TYPE_DELIMITER ) { + break; + } + // Record the table name. if ( ! $this->table_name && @@ -1572,6 +1716,12 @@ private function execute_update() { $this->rewriter->consume(); } + + // Wrap up the WHERE clause with the nested SELECT statement. + if ( $needs_closing_parenthesis ) { + $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); + } + $this->rewriter->consume_all(); $updated_query = $this->rewriter->get_updated_query(); @@ -1579,6 +1729,39 @@ private function execute_update() { $this->set_result_from_affected_rows(); } + /** + * Injects `rowid IN (SELECT rowid FROM table WHERE ...` into the WHERE clause at the current + * position in the query. + * + * This is necessary to emulate the behavior of MySQL's UPDATE LIMIT and DELETE LIMIT statement + * as SQLite does not support LIMIT in UPDATE and DELETE statements. + * + * The WHERE clause is wrapped in a subquery that selects the rowid of the rows that match the original + * WHERE clause. + * + * @return void + */ + private function preface_where_clause_with_a_subquery() { + $this->rewriter->add_many( + array( + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'IN', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'FROM', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( $this->table_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + ) + ); + } + /** * Executes a INSERT or REPLACE statement. */ @@ -1824,6 +2007,7 @@ private function translate_expression( $token ) { || $this->capture_group_by( $token ) || $this->translate_ungrouped_having( $token ) || $this->translate_like_escape( $token ) + || $this->translate_left_function( $token ) ); } @@ -2025,6 +2209,41 @@ private function translate_date_add_sub( $token ) { return true; } + /** + * Translate the LEFT() function. + * + * > Returns the leftmost len characters from the string str, or NULL if any argument is NULL. + * + * https://dev.mysql.com/doc/refman/8.3/en/string-functions.html#function_left + * + * @param WP_SQLite_Token $token The token to translate. + * + * @return bool + */ + private function translate_left_function( $token ) { + if ( + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_FUNCTION, + array( 'LEFT' ) + ) + ) { + return false; + } + + $this->rewriter->skip(); + $this->rewriter->add( new WP_SQLite_Token( 'SUBSTRING', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_FUNCTION ) ); + $this->rewriter->consume( + array( + 'type' => WP_SQLite_Token::TYPE_OPERATOR, + 'value' => ',', + ) + ); + $this->rewriter->add( new WP_SQLite_Token( 1, WP_SQLite_Token::TYPE_NUMBER ) ); + $this->rewriter->add( new WP_SQLite_Token( ',', WP_SQLite_Token::TYPE_OPERATOR ) ); + return true; + } + /** * Convert function aliases. * @@ -3026,46 +3245,23 @@ private function execute_show() { $this->results = true; return; + case 'GRANTS FOR': + $this->set_results_from_fetched_data( + array( + (object) array( + 'Grants for root@localhost' => 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON *.* TO `root`@`localhost` WITH GRANT OPTION', + ), + ) + ); + return; + case 'FULL COLUMNS': $this->rewriter->consume(); // Fall through. case 'COLUMNS FROM': $table_name = $this->rewriter->consume()->token; - $stmt = $this->execute_sqlite_query( - "PRAGMA table_info(\"$table_name\");" - ); - /* @todo we may need to add the Extra column if anybdy needs it. 'auto_increment' is the value */ - $name_map = array( - 'name' => 'Field', - 'type' => 'Type', - 'dflt_value' => 'Default', - 'cid' => null, - 'notnull' => null, - 'pk' => null, - ); - $columns = $stmt->fetchAll( $this->pdo_fetch_mode ); - $columns = array_map( - function ( $row ) use ( $name_map ) { - $new = array(); - $is_object = is_object( $row ); - $row = $is_object ? (array) $row : $row; - foreach ( $row as $k => $v ) { - $k = array_key_exists( $k, $name_map ) ? $name_map [ $k ] : $k; - if ( $k ) { - $new[ $k ] = $v; - } - } - if ( array_key_exists( 'notnull', $row ) ) { - $new['Null'] = ( '1' === $row ['notnull'] ) ? 'NO' : 'YES'; - } - if ( array_key_exists( 'pk', $row ) ) { - $new['Key'] = ( '1' === $row ['pk'] ) ? 'PRI' : ''; - } - return $is_object ? (object) $new : $new; - }, - $columns - ); - $this->set_results_from_fetched_data( $columns ); + + $this->set_results_from_fetched_data( $this->get_columns_from( $table_name ) ); return; case 'INDEX FROM': @@ -3140,14 +3336,100 @@ function ( $row ) use ( $name_map ) { return; + case 'CREATE TABLE': + $table_name = $this->rewriter->consume()->token; + $columns = $this->get_columns_from( $table_name ); + $keys = $this->get_keys( $table_name ); + + foreach ( $columns as $column ) { + $column = (array) $column; + $definition = ''; + $definition .= '`' . $column['Field'] . '` '; + $definition .= $this->get_cached_mysql_data_type( + $table_name, + $column['Field'] + ) ?? $column['Type']; + $definition .= 'PRI' === $column['Key'] ? ' PRIMARY KEY' : ''; + $definition .= 'PRI' === $column['Key'] && 'INTEGER' === $column['Type'] ? ' AUTO_INCREMENT' : ''; + $definition .= 'NO' === $column['Null'] ? ' NOT NULL' : ''; + $definition .= $column['Default'] ? ' DEFAULT ' . $column['Default'] : ''; + $entries[] = $definition; + } + foreach ( $keys as $key ) { + $key = (array) $key; + $definition = ''; + $definition .= '1' === $key['index']['unique'] ? 'UNIQUE ' : ''; + $definition .= 'KEY '; + $definition .= $key['index']['name']; + $definition .= ' ('; + $definition .= implode( + ', ', + array_column( $key['columns'], 'name' ) + ); + $definition .= ')'; + $entries[] = $definition; + } + $create_table = "CREATE TABLE $table_name (\n\t"; + $create_table .= implode( ",\n\t", $entries ); + $create_table .= "\n);"; + $this->set_results_from_fetched_data( + array( + (object) array( + 'Create Table' => $create_table, + ), + ) + ); + return; + case 'TABLE STATUS': // FROM `database`. - $this->rewriter->skip(); + // Match the optional [{FROM | IN} db_name]. + $database_expression = $this->rewriter->consume(); + if ( 'FROM' === $database_expression->token || 'IN' === $database_expression->token ) { + $this->rewriter->consume(); + $database_expression = $this->rewriter->consume(); + } + + $pattern = '%'; + // [LIKE 'pattern' | WHERE expr] + if ( 'LIKE' === $database_expression->token ) { + $pattern = $this->rewriter->consume()->value; + } elseif ( 'WHERE' === $database_expression->token ) { + // @TODO Support me please. + } elseif ( ';' !== $database_expression->token ) { + throw new Exception( 'Syntax error: Unexpected token ' . $database_expression->token . ' in query ' . $this->mysql_query ); + } + $database_expression = $this->rewriter->skip(); $stmt = $this->execute_sqlite_query( - "SELECT name as `Name`, 'myisam' as `Engine`, 0 as `Data_length`, 0 as `Index_length`, 0 as `Data_free` FROM sqlite_master WHERE type='table' ORDER BY name" + "SELECT + name as `Name`, + 'myisam' as `Engine`, + 10 as `Version`, + 'Fixed' as `Row_format`, + 0 as `Rows`, + 0 as `Avg_row_length`, + 0 as `Data_length`, + 0 as `Max_data_length`, + 0 as `Index_length`, + 0 as `Data_free` , + 0 as `Auto_increment`, + '2024-03-20 15:33:20' as `Create_time`, + '2024-03-20 15:33:20' as `Update_time`, + null as `Check_time`, + null as `Collation`, + null as `Checksum`, + '' as `Create_options`, + '' as `Comment` + FROM sqlite_master + WHERE + type='table' + AND name LIKE :pattern + ORDER BY name", + array( + ':pattern' => $pattern, + ) ); - - $tables = $this->strip_sqlite_system_tables( $stmt->fetchAll( $this->pdo_fetch_mode ) ); + $tables = $this->strip_sqlite_system_tables( $stmt->fetchAll( $this->pdo_fetch_mode ) ); foreach ( $tables as $table ) { $table_name = $table->Name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $stmt = $this->execute_sqlite_query( "SELECT COUNT(1) as `Rows` FROM $table_name" ); @@ -3196,6 +3478,51 @@ function ( $row ) use ( $name_map ) { } } + /** + * Gets the columns from a table. + * + * @param string $table_name The table name. + * + * @return array The columns. + */ + private function get_columns_from( $table_name ) { + $stmt = $this->execute_sqlite_query( + "PRAGMA table_info(\"$table_name\");" + ); + /* @todo we may need to add the Extra column if anybdy needs it. 'auto_increment' is the value */ + $name_map = array( + 'name' => 'Field', + 'type' => 'Type', + 'dflt_value' => 'Default', + 'cid' => null, + 'notnull' => null, + 'pk' => null, + ); + $columns = $stmt->fetchAll( $this->pdo_fetch_mode ); + $columns = array_map( + function ( $row ) use ( $name_map ) { + $new = array(); + $is_object = is_object( $row ); + $row = $is_object ? (array) $row : $row; + foreach ( $row as $k => $v ) { + $k = array_key_exists( $k, $name_map ) ? $name_map [ $k ] : $k; + if ( $k ) { + $new[ $k ] = $v; + } + } + if ( array_key_exists( 'notnull', $row ) ) { + $new['Null'] = ( '1' === $row ['notnull'] ) ? 'NO' : 'YES'; + } + if ( array_key_exists( 'pk', $row ) ) { + $new['Key'] = ( '1' === $row ['pk'] ) ? 'PRI' : ''; + } + return $is_object ? (object) $new : $new; + }, + $columns + ); + return $columns; + } + /** * Consumes data types from the query. * diff --git a/includes/sqlite-database-integration/wp-includes/sqlite/install-functions.php b/includes/sqlite-database-integration/wp-includes/sqlite/install-functions.php index ffd75abf9..43bfd10f5 100644 --- a/includes/sqlite-database-integration/wp-includes/sqlite/install-functions.php +++ b/includes/sqlite-database-integration/wp-includes/sqlite/install-functions.php @@ -31,7 +31,7 @@ function sqlite_make_db_sqlite() { wp_die( $message, 'Database Error!' ); } - $translator = new WP_SQLite_Translator( $pdo, $GLOBALS['table_prefix'] ); + $translator = new WP_SQLite_Translator( $pdo ); $query = null; try {