From 6a641f970419430b1c83ab9d439cdf7ed2b8562c Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Tue, 15 Apr 2025 16:08:18 +0200 Subject: [PATCH 1/6] Implement a database configurator service for DB initialization and migration --- load.php | 1 + tests/WP_SQLite_Driver_Metadata_Tests.php | 1 + tests/WP_SQLite_Driver_Query_Tests.php | 1 + tests/WP_SQLite_Driver_Tests.php | 1 + tests/WP_SQLite_Driver_Translation_Tests.php | 1 + tests/bootstrap.php | 6 + tests/tools/dump-sqlite-query.php | 1 + .../class-wp-sqlite-configurator.php | 103 ++++++++++++++++++ .../sqlite-ast/class-wp-sqlite-driver.php | 46 +++++++- wp-includes/sqlite/class-wp-sqlite-db.php | 1 + 10 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-configurator.php diff --git a/load.php b/load.php index 4e082ee..d2b647d 100644 --- a/load.php +++ b/load.php @@ -12,6 +12,7 @@ * @package wp-sqlite-integration */ +define( 'SQLITE_DRIVER_VERSION', '2.1.17-alpha' ); define( 'SQLITE_MAIN_FILE', __FILE__ ); require_once __DIR__ . '/php-polyfills.php'; diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index 33625b7..65bd157 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -1,5 +1,6 @@ driver = $driver; + $this->information_schema_builder = $information_schema_builder; + } + + /** + * Ensure that the SQLite database is configured. + * + * This method checks if the database is configured for the latest SQLite + * driver version, and if it is not, it will configure the database. + */ + public function ensure_database_configured(): void { + $version = SQLITE_DRIVER_VERSION; + $db_version = $this->driver->get_saved_driver_version(); + if ( version_compare( $version, $db_version ) > 0 ) { + $this->configure_database(); + } + } + + /** + * Configure the SQLite database. + * + * This method creates tables used for emulating MySQL behaviors in SQLite, + * and populates them with necessary data. When it is used with an already + * configured database, it will update the configuration as per the current + * SQLite driver version and attempt to repair any configuration corruption. + */ + public function configure_database(): void { + $this->ensure_global_variables_table(); + $this->information_schema_builder->ensure_information_schema_tables(); + $this->save_current_driver_version(); + } + + /** + * Ensure that the global variables table exists. + * + * This method configures a database table to store MySQL global variables + * and other internal configuration values. + */ + private function ensure_global_variables_table(): void { + $this->driver->execute_sqlite_query( + sprintf( + 'CREATE TABLE IF NOT EXISTS %s (name TEXT PRIMARY KEY, value TEXT)', + WP_SQLite_Driver::GLOBAL_VARIABLES_TABLE_NAME + ) + ); + } + + /** + * Save the current SQLite driver version. + * + * This method saves the current SQLite driver version to the database. + */ + private function save_current_driver_version(): void { + $this->driver->execute_sqlite_query( + sprintf( + 'INSERT INTO %s (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = ?', + WP_SQLite_Driver::GLOBAL_VARIABLES_TABLE_NAME + ), + array( + WP_SQLite_Driver::DRIVER_VERSION_VARIABLE_NAME, + SQLITE_DRIVER_VERSION, + SQLITE_DRIVER_VERSION, + ) + ); + } +} diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 361f278..7b000a8 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -40,6 +40,22 @@ class WP_SQLite_Driver { */ const RESERVED_PREFIX = '_wp_sqlite_'; + /** + * The name of a global variables table. + * + * This special table is used to emulate MySQL global variables and to store + * some internal configuration values. + */ + const GLOBAL_VARIABLES_TABLE_NAME = self::RESERVED_PREFIX . 'global_variables'; + + /** + * The name of the SQLite driver version variable. + * + * This internal variable is used to store the latest version of the SQLite + * driver that was used to initialize and configure the SQLite database. + */ + const DRIVER_VERSION_VARIABLE_NAME = self::RESERVED_PREFIX . 'driver_version'; + /** * A map of MySQL tokens to SQLite data types. * @@ -524,7 +540,10 @@ public function __construct( array $options ) { self::RESERVED_PREFIX, array( $this, 'execute_sqlite_query' ) ); - $this->information_schema_builder->ensure_information_schema_tables(); + + // Ensure that the database is configured. + $migrator = new WP_SQLite_Configurator( $this, $this->information_schema_builder ); + $migrator->ensure_database_configured(); } /** @@ -545,6 +564,31 @@ public function get_sqlite_version(): string { return $this->pdo->query( 'SELECT SQLITE_VERSION()' )->fetchColumn(); } + /** + * Get the SQLite driver version saved in the database. + * + * The saved driver version corresponds to the latest version of the SQLite + * driver that was used to initialize and configure the SQLite database. + * + * @return string SQLite driver version as a string. + * @throws PDOException When the query execution fails. + */ + public function get_saved_driver_version(): string { + $default_version = '0.0.0'; + try { + $stmt = $this->execute_sqlite_query( + sprintf( 'SELECT value FROM %s WHERE name = ?', self::GLOBAL_VARIABLES_TABLE_NAME ), + array( self::DRIVER_VERSION_VARIABLE_NAME ) + ); + return $stmt->fetchColumn() ?? $default_version; + } catch ( PDOException $e ) { + if ( str_contains( $e->getMessage(), 'no such table' ) ) { + return $default_version; + } + throw $e; + } + } + /** * Check if a specific SQL mode is active. * diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php index 914e430..4526c50 100644 --- a/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/wp-includes/sqlite/class-wp-sqlite-db.php @@ -300,6 +300,7 @@ public function db_connect( $allow_bail = true ) { require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-token.php'; require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-lexer.php'; require_once __DIR__ . '/../../wp-includes/mysql/class-wp-mysql-parser.php'; + require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-configurator.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; From 12d30633016489bcebec88b9f64b92c6fbd8fcdf Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Tue, 15 Apr 2025 17:10:18 +0200 Subject: [PATCH 2/6] Implement more robust plugin version definition and CI check --- .github/workflows/verify-version.yml | 35 ++++++++++++++++++++++++++++ load.php | 7 +++++- tests/bootstrap.php | 7 +----- version.php | 8 +++++++ wp-includes/sqlite/db.php | 6 +++++ 5 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/verify-version.yml create mode 100644 version.php diff --git a/.github/workflows/verify-version.yml b/.github/workflows/verify-version.yml new file mode 100644 index 0000000..a405c34 --- /dev/null +++ b/.github/workflows/verify-version.yml @@ -0,0 +1,35 @@ +name: Verify plugin version + +on: + push: + branches: + - main + pull_request: + +jobs: + verify-version: + name: Verify plugin version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Extract version from "load.php" + id: load_version + run: | + VERSION=$(grep "Version:" load.php | sed "s/.*Version: \([^ ]*\).*/\1/") + echo "load_version=$VERSION" >> $GITHUB_OUTPUT + + - name: Extract version from "version.php" + id: const_version + run: | + VERSION=$(php -r "require 'version.php'; echo SQLITE_DRIVER_VERSION;") + echo "const_version=$VERSION" >> $GITHUB_OUTPUT + + - name: Compare versions + run: | + if [ "${{ steps.load_version.outputs.load_version }}" != "${{ steps.const_version.outputs.const_version }}" ]; then + echo "Version mismatch detected!" + echo " load.php version: ${{ steps.load_version.outputs.load_version }}" + echo " version.php constant: ${{ steps.const_version.outputs.const_version }}" + exit 1 + fi diff --git a/load.php b/load.php index d2b647d..b041160 100644 --- a/load.php +++ b/load.php @@ -12,7 +12,12 @@ * @package wp-sqlite-integration */ -define( 'SQLITE_DRIVER_VERSION', '2.1.17-alpha' ); +/** + * Load the "SQLITE_DRIVER_VERSION" constant. + * This constant needs to be updated whenever the plugin version changes! + */ +require_once __DIR__ . '/version.php'; + define( 'SQLITE_MAIN_FILE', __FILE__ ); require_once __DIR__ . '/php-polyfills.php'; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a553fd1..b3aa234 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,6 +1,7 @@ Date: Wed, 16 Apr 2025 09:27:01 +0200 Subject: [PATCH 3/6] Load AST classes for tests in test bootstrap --- tests/WP_SQLite_Driver_Metadata_Tests.php | 5 ----- tests/WP_SQLite_Driver_Query_Tests.php | 5 ----- tests/WP_SQLite_Driver_Tests.php | 6 ------ tests/WP_SQLite_Driver_Translation_Tests.php | 5 ----- tests/bootstrap.php | 4 ++++ 5 files changed, 4 insertions(+), 21 deletions(-) diff --git a/tests/WP_SQLite_Driver_Metadata_Tests.php b/tests/WP_SQLite_Driver_Metadata_Tests.php index 65bd157..fde9968 100644 --- a/tests/WP_SQLite_Driver_Metadata_Tests.php +++ b/tests/WP_SQLite_Driver_Metadata_Tests.php @@ -1,10 +1,5 @@ Date: Wed, 16 Apr 2025 14:34:51 +0200 Subject: [PATCH 4/6] Implement information schema reconstructor --- ...Information_Schema_Reconstructor_Tests.php | 110 +++++ tests/bootstrap.php | 1 + tests/tools/dump-sqlite-query.php | 1 + .../class-wp-sqlite-configurator.php | 16 +- .../sqlite-ast/class-wp-sqlite-driver.php | 30 +- ...qlite-information-schema-reconstructor.php | 381 ++++++++++++++++++ wp-includes/sqlite/class-wp-sqlite-db.php | 1 + 7 files changed, 529 insertions(+), 11 deletions(-) create mode 100644 tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php create mode 100644 wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php diff --git a/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php b/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php new file mode 100644 index 0000000..baddc81 --- /dev/null +++ b/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php @@ -0,0 +1,110 @@ +suppress_errors = false; + $GLOBALS['wpdb']->show_errors = true; + } + } + + // Before each test, we create a new database + public function setUp(): void { + $this->sqlite = new PDO( 'sqlite::memory:' ); + $this->engine = new WP_SQLite_Driver( + array( + 'connection' => $this->sqlite, + 'database' => 'wp', + ) + ); + + $builder = new WP_SQLite_Information_Schema_Builder( + 'wp', + WP_SQLite_Driver::RESERVED_PREFIX, + array( $this->engine, 'execute_sqlite_query' ) + ); + + $this->reconstructor = new WP_SQLite_Information_Schema_Reconstructor( + $this->engine, + $builder + ); + } + + public function testReconstructInformationSchemaTable(): void { + $this->engine->get_pdo()->exec( + ' + CREATE TABLE t ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + role TEXT, + score REAL, + priority INTEGER DEFAULT 0, + data BLOB, + UNIQUE (name) + ) + ' + ); + $this->engine->get_pdo()->exec( 'CREATE INDEX idx_score ON t (score)' ); + $this->engine->get_pdo()->exec( 'CREATE INDEX idx_role_score ON t (role, priority)' ); + $result = $this->assertQuery( 'SELECT * FROM information_schema.tables WHERE table_name = "t"' ); + $this->assertEquals( 0, count( $result ) ); + + $this->reconstructor->ensure_correct_information_schema(); + $result = $this->assertQuery( 'SELECT * FROM information_schema.tables WHERE table_name = "t"' ); + $this->assertEquals( 1, count( $result ) ); + + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertSame( + implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `id` int NOT NULL AUTO_INCREMENT,', + ' `email` text NOT NULL,', + ' `name` text NOT NULL,', + ' `role` text DEFAULT NULL,', + ' `score` float DEFAULT NULL,', + " `priority` int DEFAULT '0',", + ' `data` blob DEFAULT NULL,', + ' PRIMARY KEY (`id`),', + ' KEY `idx_role_score` (`role`(100), `priority`),', + ' KEY `idx_score` (`score`),', + ' UNIQUE KEY `sqlite_autoindex_t_2` (`name`(100)),', + ' UNIQUE KEY `sqlite_autoindex_t_1` (`email`(100))', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + ) + ), + $result[0]->{'Create Table'} + ); + } + + private function assertQuery( $sql ) { + $retval = $this->engine->query( $sql ); + $this->assertNotFalse( $retval ); + return $retval; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2bf50a8..2f71bb9 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -18,6 +18,7 @@ require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; +require_once __DIR__ . '/../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php'; /** * Polyfills for WordPress functions diff --git a/tests/tools/dump-sqlite-query.php b/tests/tools/dump-sqlite-query.php index 69daf8c..b0f5453 100644 --- a/tests/tools/dump-sqlite-query.php +++ b/tests/tools/dump-sqlite-query.php @@ -12,6 +12,7 @@ require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; +require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php'; $driver = new WP_SQLite_Driver( array( diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php b/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php index b7c5a6e..5e0fecc 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php @@ -25,6 +25,13 @@ class WP_SQLite_Configurator { */ private $information_schema_builder; + /** + * A service for reconstructing the MySQL INFORMATION_SCHEMA tables in SQLite. + * + * @var WP_SQLite_Information_Schema_Reconstructor + */ + private $information_schema_reconstructor; + /** * Constructor. * @@ -35,8 +42,12 @@ public function __construct( WP_SQLite_Driver $driver, WP_SQLite_Information_Schema_Builder $information_schema_builder ) { - $this->driver = $driver; - $this->information_schema_builder = $information_schema_builder; + $this->driver = $driver; + $this->information_schema_builder = $information_schema_builder; + $this->information_schema_reconstructor = new WP_SQLite_Information_Schema_Reconstructor( + $driver, + $information_schema_builder + ); } /** @@ -64,6 +75,7 @@ public function ensure_database_configured(): void { public function configure_database(): void { $this->ensure_global_variables_table(); $this->information_schema_builder->ensure_information_schema_tables(); + $this->information_schema_reconstructor->ensure_correct_information_schema(); $this->save_current_driver_version(); } diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php index 7b000a8..d7c1a68 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-driver.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-driver.php @@ -655,15 +655,7 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo try { // Parse the MySQL query. - $lexer = new WP_MySQL_Lexer( $query ); - $tokens = $lexer->remaining_tokens(); - - $parser = new WP_MySQL_Parser( self::$mysql_grammar, $tokens ); - $ast = $parser->parse(); - - if ( null === $ast ) { - throw $this->new_driver_exception( 'Failed to parse the MySQL query.' ); - } + $ast = $this->parse_query( $query ); // Handle transaction commands. @@ -735,6 +727,26 @@ public function query( string $query, $fetch_mode = PDO::FETCH_OBJ, ...$fetch_mo } } + /** + * Parse a MySQL query into an AST. + * + * @param string $query The MySQL query to parse. + * @return WP_Parser_Node The AST representing the parsed query. + * @throws WP_SQLite_Driver_Exception When the query parsing fails. + */ + public function parse_query( string $query ): WP_Parser_Node { + $lexer = new WP_MySQL_Lexer( $query ); + $tokens = $lexer->remaining_tokens(); + + $parser = new WP_MySQL_Parser( self::$mysql_grammar, $tokens ); + $ast = $parser->parse(); + + if ( null === $ast ) { + throw $this->new_driver_exception( 'Failed to parse the MySQL query.' ); + } + return $ast; + } + /** * Get results of the last query. * diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php new file mode 100644 index 0000000..c2b4551 --- /dev/null +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php @@ -0,0 +1,381 @@ +driver = $driver; + $this->information_schema_builder = $information_schema_builder; + } + + /** + * Ensure that the MySQL INFORMATION_SCHEMA data in SQLite is correct. + * + * This method checks if the MySQL INFORMATION_SCHEMA data in SQLite is correct, + * and if it is not, it will reconstruct the data. + */ + public function ensure_correct_information_schema(): void { + $tables = $this->get_existing_table_names(); + $information_schema_tables = $this->get_information_schema_table_names(); + + // Reconstruct information schema records for tables that don't have them. + foreach ( $tables as $table ) { + if ( ! in_array( $table, $information_schema_tables, true ) ) { + $sql = $this->generate_create_table_statement( $table ); + $ast = $this->driver->parse_query( $sql ); + $this->information_schema_builder->record_create_table( $ast ); + } + } + + // Remove information schema records for tables that don't exist. + foreach ( $information_schema_tables as $table ) { + if ( ! in_array( $table, $tables, true ) ) { + $sql = sprintf( 'DROP %s', $this->quote_sqlite_identifier( $table ) ); + $ast = $this->driver->parse_query( $sql ); + $this->information_schema_builder->record_drop_table( $ast ); + } + } + } + + /** + * Get the names of all existing tables in the SQLite database. + * + * @return string[] The names of tables in the SQLite database. + */ + private function get_existing_table_names(): array { + $all_tables = $this->driver->execute_sqlite_query( + 'SELECT name FROM sqlite_schema WHERE type = "table" ORDER BY name' + )->fetchAll( PDO::FETCH_COLUMN ); + + // Filter out internal tables. + $tables = array(); + foreach ( $all_tables as $table ) { + if ( str_starts_with( $table, 'sqlite_' ) ) { + continue; + } + if ( str_starts_with( $table, WP_SQLite_Driver::RESERVED_PREFIX ) ) { + continue; + } + $tables[] = $table; + } + return $tables; + } + + /** + * Get the names of all tables recorded in the information schema. + * + * @return string[] The names of tables in the information schema. + */ + private function get_information_schema_table_names(): array { + $tables_table = $this->information_schema_builder->get_table_name( false, 'tables' ); + return $this->driver->execute_sqlite_query( + "SELECT table_name FROM $tables_table ORDER BY table_name" + )->fetchAll( PDO::FETCH_COLUMN ); + } + + /** + * Generate a MySQL CREATE TABLE statement from an SQLite table definition. + * + * This method generates a MySQL CREATE TABLE statement for a given table name. + * It retrieves the column information from the SQLite database and generates + * a CREATE TABLE statement that can be used to create the table in MySQL. + * + * @param string $table_name The name of the table. + * @return string The CREATE TABLE statement. + */ + private function generate_create_table_statement( string $table_name ): string { + // Columns. + $columns = $this->driver->execute_sqlite_query( + sprintf( 'PRAGMA table_xinfo("%s")', $table_name ) + )->fetchAll( PDO::FETCH_ASSOC ); + + $definitions = array(); + $data_types = array(); + foreach ( $columns as $column ) { + $mysql_type = $this->get_cached_mysql_data_type( $table_name, $column['name'] ); + if ( null === $mysql_type ) { + $mysql_type = $this->get_mysql_data_type( $column['type'] ); + } + $definitions[] = $this->get_column_definition( $table_name, $column ); + $data_types[ $column['name'] ] = $mysql_type; + } + + // Primary key. + $pk_columns = array(); + foreach ( $columns as $column ) { + if ( '0' !== $column['pk'] ) { + $pk_columns[ $column['pk'] ] = $column['name']; + } + } + ksort( $pk_columns ); + + if ( count( $pk_columns ) > 0 ) { + $definitions[] = sprintf( + 'PRIMARY KEY (%s)', + implode( ', ', array_map( array( $this, 'quote_sqlite_identifier' ), $pk_columns ) ) + ); + } + + // Indexes and keys. + $keys = $this->driver->execute_sqlite_query( + 'SELECT * FROM pragma_index_list("' . $table_name . '")' + )->fetchAll( PDO::FETCH_ASSOC ); + + foreach ( $keys as $key ) { + $key_columns = $this->driver->execute_sqlite_query( + 'SELECT * FROM pragma_index_info("' . $key['name'] . '")' + )->fetchAll( PDO::FETCH_ASSOC ); + + // If the PK columns are the same as the UK columns, skip the key. + // This is because a primary key is already unique in MySQL. + $key_equals_pk = ! array_diff( $pk_columns, array_column( $key_columns, 'name' ) ); + $is_auto_index = strpos( $key['name'], 'sqlite_autoindex_' ) === 0; + if ( $is_auto_index && $key['unique'] && $key_equals_pk ) { + continue; + } + $definitions[] = $this->get_key_definition( $key, $key_columns, $data_types ); + } + + return sprintf( + "CREATE TABLE %s (\n %s\n)", + $this->quote_sqlite_identifier( $table_name ), + implode( ",\n ", $definitions ) + ); + } + + /** + * Generate a MySQL column definition from an SQLite column information. + * + * This method generates a MySQL column definition from SQLite column data. + * + * @param string $table_name The name of the table. + * @param array $column_info The SQLite column information. + * @return string The MySQL column definition. + */ + private function get_column_definition( string $table_name, array $column_info ): string { + $definition = array(); + $definition[] = $this->quote_sqlite_identifier( $column_info['name'] ); + + // Data type. + $mysql_type = $this->get_cached_mysql_data_type( $table_name, $column_info['name'] ); + if ( null === $mysql_type ) { + $mysql_type = $this->get_mysql_data_type( $column_info['type'] ); + } + $definition[] = $mysql_type; + + // NULL/NOT NULL. + if ( '1' === $column_info['notnull'] ) { + $definition[] = 'NOT NULL'; + } + + // Auto increment. + $is_auto_increment = false; + if ( '0' !== $column_info['pk'] ) { + $is_auto_increment = $this->driver->execute_sqlite_query( + 'SELECT 1 FROM sqlite_schema WHERE tbl_name = ? AND sql LIKE ?', + array( $table_name, '%AUTOINCREMENT%' ) + )->fetchColumn(); + + if ( $is_auto_increment ) { + $definition[] = 'AUTO_INCREMENT'; + } + } + + // Default value. + if ( $this->column_has_default( $mysql_type, $column_info['dflt_value'] ) && ! $is_auto_increment ) { + $definition[] = 'DEFAULT ' . $column_info['dflt_value']; + } + + return implode( ' ', $definition ); + } + + /** + * Generate a MySQL key definition from an SQLite key information. + * + * This method generates a MySQL key definition from SQLite key data. + * + * @param array $key The SQLite key information. + * @param array $key_columns The SQLite key column information. + * @param array $data_types The MySQL data types of the columns. + * @return string The MySQL key definition. + */ + private function get_key_definition( array $key, array $key_columns, array $data_types ): string { + $key_length_limit = 100; + + // Key definition. + $definition = array(); + if ( $key['unique'] ) { + $definition[] = 'UNIQUE'; + } + $definition[] = 'KEY'; + + // Remove the prefix from the index name if there is any. We use __ as a separator. + $index_name = explode( '__', $key['name'], 2 )[1] ?? $key['name']; + $definition[] = $this->quote_sqlite_identifier( $index_name ); + + // Key columns. + $cols = array(); + foreach ( $key_columns as $column ) { + // Get data type and length. + $data_type = strtolower( $data_types[ $column['name'] ] ); + if ( 1 === preg_match( '/^(\w+)\s*\(\s*(\d+)\s*\)/', $data_type, $matches ) ) { + $data_type = $matches[1]; // "varchar" + $data_length = min( $matches[2], $key_length_limit ); // "255" + } + + // Apply max length if needed. + if ( + str_contains( $data_type, 'char' ) + || str_starts_with( $data_type, 'var' ) + || str_ends_with( $data_type, 'text' ) + || str_ends_with( $data_type, 'blob' ) + ) { + $cols[] = sprintf( + '%s(%d)', + $this->quote_sqlite_identifier( $column['name'] ), + $data_length ?? $key_length_limit + ); + } else { + $cols[] = $this->quote_sqlite_identifier( $column['name'] ); + } + } + + $definition[] = '(' . implode( ', ', $cols ) . ')'; + return implode( ' ', $definition ); + } + + /** + * Determine if a column has a default value. + * + * @param string $mysql_type The MySQL data type of the column. + * @param string|null $default_value The default value of the SQLite column. + * @return bool True if the column has a default value, false otherwise. + */ + private function column_has_default( string $mysql_type, ?string $default_value ): bool { + if ( null === $default_value || '' === $default_value ) { + return false; + } + if ( + "''" === $default_value + && in_array( strtolower( $mysql_type ), array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true ) + ) { + return false; + } + return true; + } + + /** + * Get a MySQL column or index data type from legacy data types cache table. + * + * This method retrieves MySQL column or index data types from a special table + * that was used by an old version of the SQLite driver and that is otherwise + * no longer needed. This is more precise than direct inference from SQLite. + * + * @param string $table_name The table name. + * @param string $column_or_index_name The column or index name. + * @return string|null The MySQL definition, or null when not found. + */ + private function get_cached_mysql_data_type( string $table_name, string $column_or_index_name ): ?string { + try { + $mysql_type = $this->driver->execute_sqlite_query( + 'SELECT mysql_type FROM _mysql_data_types_cache WHERE `table` = ? AND column_or_index = ?', + array( $table_name, $column_or_index_name ) + )->fetchColumn(); + } catch ( PDOException $e ) { + if ( str_contains( $e->getMessage(), 'no such table' ) ) { + return null; + } + throw $e; + } + if ( str_ends_with( $mysql_type, ' KEY' ) ) { + $mysql_type = substr( $mysql_type, 0, strlen( $mysql_type ) - strlen( ' KEY' ) ); + } + return $mysql_type; + } + + /** + * Get a MySQL column type from an SQLite column type. + * + * This method converts an SQLite column type to a MySQL column type as per + * the SQLite column type affinity rules: + * https://sqlite.org/datatype3.html#determination_of_column_affinity + * + * @param string $column_type The SQLite column type. + * @return string The MySQL column type. + */ + private function get_mysql_data_type( string $column_type ): string { + $type = strtoupper( $column_type ); + if ( str_contains( $type, 'INT' ) ) { + return 'int'; + } + if ( str_contains( $type, 'TEXT' ) || str_contains( $type, 'CHAR' ) || str_contains( $type, 'CLOB' ) ) { + return 'text'; + } + if ( str_contains( $type, 'BLOB' ) || '' === $type ) { + return 'blob'; + } + if ( str_contains( $type, 'REAL' ) || str_contains( $type, 'FLOA' ) ) { + return 'float'; + } + if ( str_contains( $type, 'DOUB' ) ) { + return 'double'; + } + + /** + * While SQLite defaults to a NUMERIC column affinity, it's better to use + * TEXT in this case, because numeric SQLite columns in non-strict tables + * can contain any text data as well, when it is not a well-formed number. + * + * See: https://sqlite.org/datatype3.html#type_affinity + */ + return 'text'; + } + + /** + * Quote an SQLite identifier. + * + * Wrap the identifier in backticks and escape backtick values within. + * + * @param string $unquoted_identifier The unquoted identifier value. + * @return string The quoted identifier value. + */ + private function quote_sqlite_identifier( string $unquoted_identifier ): string { + return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`'; + } +} diff --git a/wp-includes/sqlite/class-wp-sqlite-db.php b/wp-includes/sqlite/class-wp-sqlite-db.php index 4526c50..3dc28ac 100644 --- a/wp-includes/sqlite/class-wp-sqlite-db.php +++ b/wp-includes/sqlite/class-wp-sqlite-db.php @@ -304,6 +304,7 @@ public function db_connect( $allow_bail = true ) { require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-driver-exception.php'; require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-builder.php'; + require_once __DIR__ . '/../../wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php'; $this->ensure_database_directory( FQDB ); try { From 23331b36c3b0753478318968fbf9c80b382475d1 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Thu, 17 Apr 2025 11:56:34 +0200 Subject: [PATCH 5/6] Reconstruct WordPress tables using "wp_get_db_schema" function --- ...Information_Schema_Reconstructor_Tests.php | 68 +++++++++++++++++++ ...qlite-information-schema-reconstructor.php | 48 ++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php b/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php index baddc81..9609452 100644 --- a/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php +++ b/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php @@ -29,6 +29,23 @@ public static function setUpBeforeClass(): void { $GLOBALS['wpdb']->suppress_errors = false; $GLOBALS['wpdb']->show_errors = true; } + + // Mock symols that are used for WordPress table reconstruction. + if ( ! defined( 'ABSPATH' ) ) { + define( 'ABSPATH', __DIR__ ); + } + if ( ! function_exists( 'wp_installing' ) ) { + function wp_installing() { + return false; + } + } + if ( ! function_exists( 'wp_get_db_schema' ) ) { + function wp_get_db_schema() { + // Output from "wp_get_db_schema" as of WordPress 6.8.0. + // See: https://github.com/WordPress/wordpress-develop/blob/6.8.0/src/wp-admin/includes/schema.php#L36 + return "CREATE TABLE wp_users ( ID bigint(20) unsigned NOT NULL auto_increment, user_login varchar(60) NOT NULL default '', user_pass varchar(255) NOT NULL default '', user_nicename varchar(50) NOT NULL default '', user_email varchar(100) NOT NULL default '', user_url varchar(100) NOT NULL default '', user_registered datetime NOT NULL default '0000-00-00 00:00:00', user_activation_key varchar(255) NOT NULL default '', user_status int(11) NOT NULL default '0', display_name varchar(250) NOT NULL default '', PRIMARY KEY (ID), KEY user_login_key (user_login), KEY user_nicename (user_nicename), KEY user_email (user_email) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_usermeta ( umeta_id bigint(20) unsigned NOT NULL auto_increment, user_id bigint(20) unsigned NOT NULL default '0', meta_key varchar(255) default NULL, meta_value longtext, PRIMARY KEY (umeta_id), KEY user_id (user_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_termmeta ( meta_id bigint(20) unsigned NOT NULL auto_increment, term_id bigint(20) unsigned NOT NULL default '0', meta_key varchar(255) default NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY term_id (term_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_terms ( term_id bigint(20) unsigned NOT NULL auto_increment, name varchar(200) NOT NULL default '', slug varchar(200) NOT NULL default '', term_group bigint(10) NOT NULL default 0, PRIMARY KEY (term_id), KEY slug (slug(191)), KEY name (name(191)) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_term_taxonomy ( term_taxonomy_id bigint(20) unsigned NOT NULL auto_increment, term_id bigint(20) unsigned NOT NULL default 0, taxonomy varchar(32) NOT NULL default '', description longtext NOT NULL, parent bigint(20) unsigned NOT NULL default 0, count bigint(20) NOT NULL default 0, PRIMARY KEY (term_taxonomy_id), UNIQUE KEY term_id_taxonomy (term_id,taxonomy), KEY taxonomy (taxonomy) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_term_relationships ( object_id bigint(20) unsigned NOT NULL default 0, term_taxonomy_id bigint(20) unsigned NOT NULL default 0, term_order int(11) NOT NULL default 0, PRIMARY KEY (object_id,term_taxonomy_id), KEY term_taxonomy_id (term_taxonomy_id) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_commentmeta ( meta_id bigint(20) unsigned NOT NULL auto_increment, comment_id bigint(20) unsigned NOT NULL default '0', meta_key varchar(255) default NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY comment_id (comment_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_comments ( comment_ID bigint(20) unsigned NOT NULL auto_increment, comment_post_ID bigint(20) unsigned NOT NULL default '0', comment_author tinytext NOT NULL, comment_author_email varchar(100) NOT NULL default '', comment_author_url varchar(200) NOT NULL default '', comment_author_IP varchar(100) NOT NULL default '', comment_date datetime NOT NULL default '0000-00-00 00:00:00', comment_date_gmt datetime NOT NULL default '0000-00-00 00:00:00', comment_content text NOT NULL, comment_karma int(11) NOT NULL default '0', comment_approved varchar(20) NOT NULL default '1', comment_agent varchar(255) NOT NULL default '', comment_type varchar(20) NOT NULL default 'comment', comment_parent bigint(20) unsigned NOT NULL default '0', user_id bigint(20) unsigned NOT NULL default '0', PRIMARY KEY (comment_ID), KEY comment_post_ID (comment_post_ID), KEY comment_approved_date_gmt (comment_approved,comment_date_gmt), KEY comment_date_gmt (comment_date_gmt), KEY comment_parent (comment_parent), KEY comment_author_email (comment_author_email(10)) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_links ( link_id bigint(20) unsigned NOT NULL auto_increment, link_url varchar(255) NOT NULL default '', link_name varchar(255) NOT NULL default '', link_image varchar(255) NOT NULL default '', link_target varchar(25) NOT NULL default '', link_description varchar(255) NOT NULL default '', link_visible varchar(20) NOT NULL default 'Y', link_owner bigint(20) unsigned NOT NULL default '1', link_rating int(11) NOT NULL default '0', link_updated datetime NOT NULL default '0000-00-00 00:00:00', link_rel varchar(255) NOT NULL default '', link_notes mediumtext NOT NULL, link_rss varchar(255) NOT NULL default '', PRIMARY KEY (link_id), KEY link_visible (link_visible) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_options ( option_id bigint(20) unsigned NOT NULL auto_increment, option_name varchar(191) NOT NULL default '', option_value longtext NOT NULL, autoload varchar(20) NOT NULL default 'yes', PRIMARY KEY (option_id), UNIQUE KEY option_name (option_name), KEY autoload (autoload) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_postmeta ( meta_id bigint(20) unsigned NOT NULL auto_increment, post_id bigint(20) unsigned NOT NULL default '0', meta_key varchar(255) default NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY post_id (post_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4; CREATE TABLE wp_posts ( ID bigint(20) unsigned NOT NULL auto_increment, post_author bigint(20) unsigned NOT NULL default '0', post_date datetime NOT NULL default '0000-00-00 00:00:00', post_date_gmt datetime NOT NULL default '0000-00-00 00:00:00', post_content longtext NOT NULL, post_title text NOT NULL, post_excerpt text NOT NULL, post_status varchar(20) NOT NULL default 'publish', comment_status varchar(20) NOT NULL default 'open', ping_status varchar(20) NOT NULL default 'open', post_password varchar(255) NOT NULL default '', post_name varchar(200) NOT NULL default '', to_ping text NOT NULL, pinged text NOT NULL, post_modified datetime NOT NULL default '0000-00-00 00:00:00', post_modified_gmt datetime NOT NULL default '0000-00-00 00:00:00', post_content_filtered longtext NOT NULL, post_parent bigint(20) unsigned NOT NULL default '0', guid varchar(255) NOT NULL default '', menu_order int(11) NOT NULL default '0', post_type varchar(20) NOT NULL default 'post', post_mime_type varchar(100) NOT NULL default '', comment_count bigint(20) NOT NULL default '0', PRIMARY KEY (ID), KEY post_name (post_name(191)), KEY type_status_date (post_type,post_status,post_date,ID), KEY post_parent (post_parent), KEY post_author (post_author) ) DEFAULT CHARACTER SET utf8mb4;"; + } + } } // Before each test, we create a new database @@ -102,6 +119,57 @@ public function testReconstructInformationSchemaTable(): void { ); } + public function testReconstructInformationSchemaTableWithWpTables(): void { + // Create a WP table with any columns. + $this->engine->get_pdo()->exec( 'CREATE TABLE wp_posts ( id INTEGER )' ); + + // Reconstruct the information schema. + $this->reconstructor->ensure_correct_information_schema(); + $result = $this->assertQuery( 'SELECT * FROM information_schema.tables WHERE table_name = "wp_posts"' ); + $this->assertEquals( 1, count( $result ) ); + + // The reconstructed schema should correspond to the original WP table definition. + $result = $this->assertQuery( 'SHOW CREATE TABLE wp_posts' ); + $this->assertSame( + implode( + "\n", + array( + 'CREATE TABLE `wp_posts` (', + ' `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT,', + " `post_author` bigint(20) unsigned NOT NULL DEFAULT '0',", + " `post_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',", + " `post_date_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',", + ' `post_content` longtext NOT NULL,', + ' `post_title` text NOT NULL,', + ' `post_excerpt` text NOT NULL,', + " `post_status` varchar(20) NOT NULL DEFAULT 'publish',", + " `comment_status` varchar(20) NOT NULL DEFAULT 'open',", + " `ping_status` varchar(20) NOT NULL DEFAULT 'open',", + " `post_password` varchar(255) NOT NULL DEFAULT '',", + " `post_name` varchar(200) NOT NULL DEFAULT '',", + ' `to_ping` text NOT NULL,', + ' `pinged` text NOT NULL,', + " `post_modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',", + " `post_modified_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',", + ' `post_content_filtered` longtext NOT NULL,', + " `post_parent` bigint(20) unsigned NOT NULL DEFAULT '0',", + " `guid` varchar(255) NOT NULL DEFAULT '',", + " `menu_order` int(11) NOT NULL DEFAULT '0',", + " `post_type` varchar(20) NOT NULL DEFAULT 'post',", + " `post_mime_type` varchar(100) NOT NULL DEFAULT '',", + " `comment_count` bigint(20) NOT NULL DEFAULT '0',", + ' PRIMARY KEY (`ID`),', + ' KEY `post_name` (`post_name`(191)),', + ' KEY `type_status_date` (`post_type`, `post_status`, `post_date`, `ID`),', + ' KEY `post_parent` (`post_parent`),', + ' KEY `post_author` (`post_author`)', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci', + ) + ), + $result[0]->{'Create Table'} + ); + } + private function assertQuery( $sql ) { $retval = $this->engine->query( $sql ); $this->assertNotFalse( $retval ); diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php index c2b4551..5d7cbc3 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php @@ -54,10 +54,38 @@ public function ensure_correct_information_schema(): void { $tables = $this->get_existing_table_names(); $information_schema_tables = $this->get_information_schema_table_names(); + // In WordPress, use "wp_get_db_schema()" to reconstruct WordPress tables. + $wp_tables = array(); + if ( defined( 'ABSPATH' ) ) { + if ( wp_installing() ) { + // Avoid interfering with WordPress installation. + return; + } + if ( file_exists( ABSPATH . 'wp-admin/includes/schema.php' ) ) { + require_once ABSPATH . 'wp-admin/includes/schema.php'; + } + if ( function_exists( 'wp_get_db_schema' ) ) { + $schema = wp_get_db_schema(); + $parts = preg_split( '/(CREATE\s+TABLE)/', $schema, -1, PREG_SPLIT_NO_EMPTY ); + foreach ( $parts as $part ) { + $name = $this->unquote_mysql_identifier( + preg_split( '/\s+/', $part, 2, PREG_SPLIT_NO_EMPTY )[0] + ); + $wp_tables[ $name ] = 'CREATE TABLE' . $part; + } + } + } + // Reconstruct information schema records for tables that don't have them. foreach ( $tables as $table ) { if ( ! in_array( $table, $information_schema_tables, true ) ) { - $sql = $this->generate_create_table_statement( $table ); + if ( isset( $wp_tables[ $table ] ) ) { + // WordPress table. + $sql = $wp_tables[ $table ]; + } else { + // Non-WordPress table. + $sql = $this->generate_create_table_statement( $table ); + } $ast = $this->driver->parse_query( $sql ); $this->information_schema_builder->record_create_table( $ast ); } @@ -378,4 +406,22 @@ private function get_mysql_data_type( string $column_type ): string { private function quote_sqlite_identifier( string $unquoted_identifier ): string { return '`' . str_replace( '`', '``', $unquoted_identifier ) . '`'; } + + /** + * Unquote a quoted MySQL identifier. + * + * Remove bounding quotes and replace escaped quotes with their values. + * + * @param string $quoted_identifier The quoted identifier value. + * @return string The unquoted identifier value. + */ + private function unquote_mysql_identifier( string $quoted_identifier ): string { + $first_byte = $quoted_identifier[0] ?? null; + if ( '"' === $first_byte || '`' === $first_byte ) { + $unquoted = substr( $quoted_identifier, 1, -1 ); + } else { + $unquoted = $quoted_identifier; + } + return str_replace( $first_byte . $first_byte, $first_byte, $unquoted ); + } } From ed959fb299654f7455a4b11d565f7ae48f63e568 Mon Sep 17 00:00:00 2001 From: Jan Jakes Date: Thu, 17 Apr 2025 15:42:34 +0200 Subject: [PATCH 6/6] Avoid race conditions when configuring the SQLite database --- .../class-wp-sqlite-configurator.php | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php b/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php index 5e0fecc..4cc9477 100644 --- a/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php +++ b/wp-includes/sqlite-ast/class-wp-sqlite-configurator.php @@ -57,11 +57,20 @@ public function __construct( * driver version, and if it is not, it will configure the database. */ public function ensure_database_configured(): void { - $version = SQLITE_DRIVER_VERSION; - $db_version = $this->driver->get_saved_driver_version(); - if ( version_compare( $version, $db_version ) > 0 ) { - $this->configure_database(); + // Use an EXCLUSIVE transaction to prevent multiple connections + // from attempting to configure the database at the same time. + $this->driver->execute_sqlite_query( 'BEGIN EXCLUSIVE TRANSACTION' ); + try { + $version = SQLITE_DRIVER_VERSION; + $db_version = $this->driver->get_saved_driver_version(); + if ( version_compare( $version, $db_version ) > 0 ) { + $this->run_database_configuration(); + } + } catch ( Throwable $e ) { + $this->driver->execute_sqlite_query( 'ROLLBACK' ); + throw $e; } + $this->driver->execute_sqlite_query( 'COMMIT' ); } /** @@ -73,6 +82,25 @@ public function ensure_database_configured(): void { * SQLite driver version and attempt to repair any configuration corruption. */ public function configure_database(): void { + // Use an EXCLUSIVE transaction to prevent multiple connections + // from attempting to configure the database at the same time. + $this->driver->execute_sqlite_query( 'BEGIN EXCLUSIVE TRANSACTION' ); + try { + $this->run_database_configuration(); + } catch ( Throwable $e ) { + $this->driver->execute_sqlite_query( 'ROLLBACK' ); + throw $e; + } + $this->driver->execute_sqlite_query( 'COMMIT' ); + } + + /** + * Run the SQLite database configuration. + * + * This method executes the database configuration steps, ensuring that all + * tables required for MySQL emulation in SQLite are created and populated. + */ + private function run_database_configuration(): void { $this->ensure_global_variables_table(); $this->information_schema_builder->ensure_information_schema_tables(); $this->information_schema_reconstructor->ensure_correct_information_schema();