diff --git a/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php b/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php index cfc6840c..af14aecf 100644 --- a/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php +++ b/tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php @@ -310,6 +310,88 @@ public function testDefaultValueEscaping(): void { ); } + public function testInvalidDataTypeCacheDataForDecimalDefinition(): void { + // Recreate the invalid database state before the following fix: + // https://github.com/WordPress/sqlite-database-integration/pull/126 + $connection = $this->engine->get_connection(); + $connection->query( self::CREATE_DATA_TYPES_CACHE_TABLE_SQL ); + $connection->query( 'CREATE TABLE t ( dec_col REAL )' ); + $connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'decimal', 'decimal(26,')" ); + + // Ensure the information schema is reconstructed correctly. + $this->reconstructor->ensure_correct_information_schema(); + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertSame( + implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `dec_col` float DEFAULT NULL', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + $result[0]->{'Create Table'} + ); + } + + public function testInvalidDataTypeCacheDataForDecimalDefinitionOnWooCommerceTable(): void { + // Recreate the invalid database state before the following fix: + // https://github.com/WordPress/sqlite-database-integration/pull/126 + $connection = $this->engine->get_connection(); + $connection->query( self::CREATE_DATA_TYPES_CACHE_TABLE_SQL ); + $connection->query( 'CREATE TABLE wc_testing_table ( col1 REAL, col2 REAL, col3 REAL, col4 REAL )' ); + $connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('wc_testing_table', 'col1', 'decimal(26,')" ); + $connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('wc_testing_table', 'col2', 'decimal(19,')" ); + $connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('wc_testing_table', 'col3', 'decimal(3,')" ); + $connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('wc_testing_table', 'col4', 'decimal(5,')" ); + + // Ensure the information schema is reconstructed correctly. + $this->reconstructor->ensure_correct_information_schema(); + $result = $this->assertQuery( 'SHOW CREATE TABLE wc_testing_table' ); + $this->assertSame( + implode( + "\n", + array( + 'CREATE TABLE `wc_testing_table` (', + ' `col1` decimal(26,8) DEFAULT NULL,', + ' `col2` decimal(19,4) DEFAULT NULL,', + ' `col3` decimal(3,2) DEFAULT NULL,', + ' `col4` float DEFAULT NULL', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci', + ) + ), + $result[0]->{'Create Table'} + ); + } + + public function testInvalidDataTypeCacheDataForIndexDefinition(): void { + $connection = $this->engine->get_connection(); + + // Recreate the invalid database state before the following fix: + // https://github.com/WordPress/sqlite-database-integration/commit/b5a9fbaed4d0d843f792aaa959e3d00f193ff1ee + // https://github.com/Automattic/sqlite-database-integration/pull/2 + $connection->query( self::CREATE_DATA_TYPES_CACHE_TABLE_SQL ); + $connection->query( 'CREATE TABLE t ( `timestamp` TEXT, `KEY` TEXT)' ); + $connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'timestamp', 'datetime')" ); + $connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'KEY', 'timestamp(timestamp)')" ); + + // Ensure the information schema is reconstructed correctly. + $this->reconstructor->ensure_correct_information_schema(); + $result = $this->assertQuery( 'SHOW CREATE TABLE t' ); + $this->assertSame( + implode( + "\n", + array( + 'CREATE TABLE `t` (', + ' `timestamp` datetime DEFAULT NULL,', + ' `KEY` text DEFAULT NULL', + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_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 ff91e0ec..103bb8c1 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 @@ -623,6 +623,72 @@ private function get_cached_mysql_data_type( string $table_name, string $column_ return null; } + /** + * Check whether the stored type value is a valid MySQL column type. + * + * Some older versions of the legacy SQLite driver might have stored + * invalid MySQL column types in some scenarios: + * + * 1. Before https://github.com/WordPress/sqlite-database-integration/pull/126, + * the legacy SQLite driver incorrectly stored MySQL column types + * for columns with multiple type arguments. + * + * E.g., a column definition like "col_name decimal(26, 8)" would + * be stored with invalid type "decimal(26,". + * + * 2. Before https://github.com/WordPress/sqlite-database-integration/commit/b5a9fbaed4d0d843f792aaa959e3d00f193ff1ee + * (see also https://github.com/Automattic/sqlite-database-integration/pull/2), + * the legacy SQLite driver incorrectly recognized indexes on columns + * with type keywords as additional table column definitions. + * + * E.g., an index definition like "KEY timestamp (timestamp)" would + * be stored as column "KEY" with invalid type "timestamp(timestamp)". + * + * To address these issues, we need to check whether the stored type looks + * like a valid MySQL column type definition. + */ + $open_par_index = strpos( $mysql_type, '(' ); + $close_par_index = strpos( $mysql_type, ')' ); + if ( false !== $open_par_index ) { + $end = false !== $close_par_index ? $close_par_index : strlen( $mysql_type ); + $parts = explode( '(', substr( $mysql_type, 0, $end ) ); + $type = strtolower( trim( $parts[0] ) ); + $args = array(); + foreach ( explode( ',', $parts[1] ) as $arg ) { + $args[] = strtolower( trim( $arg ) ); + } + + // WooCommerce uses decimal(26,8), decimal(19,4), and decimal(3,2) + // column types, so we can can fix the invalid column definitions. + $looks_like_wc_table = str_contains( $table_name, 'wc_' ) || str_contains( $table_name, 'woocommerce_' ); + $is_invalid_decimal = 'decimal' === $type && count( $args ) === 2 && '' === $args[1]; + if ( $looks_like_wc_table && $is_invalid_decimal ) { + if ( '26' === $args[0] ) { + // Fix "decimal(26,". + return 'decimal(26,8)'; + } elseif ( '19' === $args[0] ) { + // Fix "decimal(19,". + return 'decimal(19,4)'; + } elseif ( '3' === $args[0] ) { + // Fix "decimal(3,". + return 'decimal(3,2)'; + } + } + + // Only numeric arguments are allowed for MySQL column types. + // This handles the incorrectly stored index definition case. + foreach ( $args as $arg ) { + if ( ! is_numeric( $arg ) ) { + return null; + } + } + + // If there is no closing parenthesis, the type is invalid. + if ( false === $close_par_index ) { + return null; + } + } + // Normalize index type for backward compatibility. Some older versions // of the SQLite driver stored index types with a " KEY" suffix, e.g., // "PRIMARY KEY" or "UNIQUE KEY". More recent versions omit the suffix.