Skip to content

Commit c087051

Browse files
authored
Handle incorrectly stored MySQL metadata in the legacy-to-AST driver migrator (#312)
When migrating from the legacy SQLite driver to the new AST-based driver, the information schema reconstructor reads MySQL column type metadata from the `_mysql_data_types_cache` table. Some older versions of the legacy driver stored invalid type definitions in this table, causing migration failures. This PR detects and handles two types of invalid data: **1. Truncated decimal definitions** — Before [PR #126](#126), columns with multiple type arguments like `decimal(26, 8)` were incorrectly stored as `decimal(26,` (missing the second argument and closing parenthesis). For WooCommerce tables, the fix restores the correct decimal precision based on known WooCommerce column patterns. For other tables, these invalid definitions are discarded and the column type is inferred from SQLite. **2. Index definitions mistaken for columns** — Before [commit b5a9fba](b5a9fba), index definitions like `KEY timestamp (timestamp)` were incorrectly parsed and stored as column `KEY` with type `timestamp(timestamp)`. The fix validates that type arguments are numeric, rejecting these malformed entries and falling back to the SQLite type inference. Fixes WordPress/wordpress-playground#3050.
1 parent eeabf9f commit c087051

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,88 @@ public function testDefaultValueEscaping(): void {
310310
);
311311
}
312312

313+
public function testInvalidDataTypeCacheDataForDecimalDefinition(): void {
314+
// Recreate the invalid database state before the following fix:
315+
// https://github.com/WordPress/sqlite-database-integration/pull/126
316+
$connection = $this->engine->get_connection();
317+
$connection->query( self::CREATE_DATA_TYPES_CACHE_TABLE_SQL );
318+
$connection->query( 'CREATE TABLE t ( dec_col REAL )' );
319+
$connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'decimal', 'decimal(26,')" );
320+
321+
// Ensure the information schema is reconstructed correctly.
322+
$this->reconstructor->ensure_correct_information_schema();
323+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
324+
$this->assertSame(
325+
implode(
326+
"\n",
327+
array(
328+
'CREATE TABLE `t` (',
329+
' `dec_col` float DEFAULT NULL',
330+
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
331+
)
332+
),
333+
$result[0]->{'Create Table'}
334+
);
335+
}
336+
337+
public function testInvalidDataTypeCacheDataForDecimalDefinitionOnWooCommerceTable(): void {
338+
// Recreate the invalid database state before the following fix:
339+
// https://github.com/WordPress/sqlite-database-integration/pull/126
340+
$connection = $this->engine->get_connection();
341+
$connection->query( self::CREATE_DATA_TYPES_CACHE_TABLE_SQL );
342+
$connection->query( 'CREATE TABLE wc_testing_table ( col1 REAL, col2 REAL, col3 REAL, col4 REAL )' );
343+
$connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('wc_testing_table', 'col1', 'decimal(26,')" );
344+
$connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('wc_testing_table', 'col2', 'decimal(19,')" );
345+
$connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('wc_testing_table', 'col3', 'decimal(3,')" );
346+
$connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('wc_testing_table', 'col4', 'decimal(5,')" );
347+
348+
// Ensure the information schema is reconstructed correctly.
349+
$this->reconstructor->ensure_correct_information_schema();
350+
$result = $this->assertQuery( 'SHOW CREATE TABLE wc_testing_table' );
351+
$this->assertSame(
352+
implode(
353+
"\n",
354+
array(
355+
'CREATE TABLE `wc_testing_table` (',
356+
' `col1` decimal(26,8) DEFAULT NULL,',
357+
' `col2` decimal(19,4) DEFAULT NULL,',
358+
' `col3` decimal(3,2) DEFAULT NULL,',
359+
' `col4` float DEFAULT NULL',
360+
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
361+
)
362+
),
363+
$result[0]->{'Create Table'}
364+
);
365+
}
366+
367+
public function testInvalidDataTypeCacheDataForIndexDefinition(): void {
368+
$connection = $this->engine->get_connection();
369+
370+
// Recreate the invalid database state before the following fix:
371+
// https://github.com/WordPress/sqlite-database-integration/commit/b5a9fbaed4d0d843f792aaa959e3d00f193ff1ee
372+
// https://github.com/Automattic/sqlite-database-integration/pull/2
373+
$connection->query( self::CREATE_DATA_TYPES_CACHE_TABLE_SQL );
374+
$connection->query( 'CREATE TABLE t ( `timestamp` TEXT, `KEY` TEXT)' );
375+
$connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'timestamp', 'datetime')" );
376+
$connection->query( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'KEY', 'timestamp(timestamp)')" );
377+
378+
// Ensure the information schema is reconstructed correctly.
379+
$this->reconstructor->ensure_correct_information_schema();
380+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
381+
$this->assertSame(
382+
implode(
383+
"\n",
384+
array(
385+
'CREATE TABLE `t` (',
386+
' `timestamp` datetime DEFAULT NULL,',
387+
' `KEY` text DEFAULT NULL',
388+
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
389+
)
390+
),
391+
$result[0]->{'Create Table'}
392+
);
393+
}
394+
313395
private function assertQuery( $sql ) {
314396
$retval = $this->engine->query( $sql );
315397
$this->assertNotFalse( $retval );

wp-includes/sqlite-ast/class-wp-sqlite-information-schema-reconstructor.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,72 @@ private function get_cached_mysql_data_type( string $table_name, string $column_
623623
return null;
624624
}
625625

626+
/**
627+
* Check whether the stored type value is a valid MySQL column type.
628+
*
629+
* Some older versions of the legacy SQLite driver might have stored
630+
* invalid MySQL column types in some scenarios:
631+
*
632+
* 1. Before https://github.com/WordPress/sqlite-database-integration/pull/126,
633+
* the legacy SQLite driver incorrectly stored MySQL column types
634+
* for columns with multiple type arguments.
635+
*
636+
* E.g., a column definition like "col_name decimal(26, 8)" would
637+
* be stored with invalid type "decimal(26,".
638+
*
639+
* 2. Before https://github.com/WordPress/sqlite-database-integration/commit/b5a9fbaed4d0d843f792aaa959e3d00f193ff1ee
640+
* (see also https://github.com/Automattic/sqlite-database-integration/pull/2),
641+
* the legacy SQLite driver incorrectly recognized indexes on columns
642+
* with type keywords as additional table column definitions.
643+
*
644+
* E.g., an index definition like "KEY timestamp (timestamp)" would
645+
* be stored as column "KEY" with invalid type "timestamp(timestamp)".
646+
*
647+
* To address these issues, we need to check whether the stored type looks
648+
* like a valid MySQL column type definition.
649+
*/
650+
$open_par_index = strpos( $mysql_type, '(' );
651+
$close_par_index = strpos( $mysql_type, ')' );
652+
if ( false !== $open_par_index ) {
653+
$end = false !== $close_par_index ? $close_par_index : strlen( $mysql_type );
654+
$parts = explode( '(', substr( $mysql_type, 0, $end ) );
655+
$type = strtolower( trim( $parts[0] ) );
656+
$args = array();
657+
foreach ( explode( ',', $parts[1] ) as $arg ) {
658+
$args[] = strtolower( trim( $arg ) );
659+
}
660+
661+
// WooCommerce uses decimal(26,8), decimal(19,4), and decimal(3,2)
662+
// column types, so we can can fix the invalid column definitions.
663+
$looks_like_wc_table = str_contains( $table_name, 'wc_' ) || str_contains( $table_name, 'woocommerce_' );
664+
$is_invalid_decimal = 'decimal' === $type && count( $args ) === 2 && '' === $args[1];
665+
if ( $looks_like_wc_table && $is_invalid_decimal ) {
666+
if ( '26' === $args[0] ) {
667+
// Fix "decimal(26,".
668+
return 'decimal(26,8)';
669+
} elseif ( '19' === $args[0] ) {
670+
// Fix "decimal(19,".
671+
return 'decimal(19,4)';
672+
} elseif ( '3' === $args[0] ) {
673+
// Fix "decimal(3,".
674+
return 'decimal(3,2)';
675+
}
676+
}
677+
678+
// Only numeric arguments are allowed for MySQL column types.
679+
// This handles the incorrectly stored index definition case.
680+
foreach ( $args as $arg ) {
681+
if ( ! is_numeric( $arg ) ) {
682+
return null;
683+
}
684+
}
685+
686+
// If there is no closing parenthesis, the type is invalid.
687+
if ( false === $close_par_index ) {
688+
return null;
689+
}
690+
}
691+
626692
// Normalize index type for backward compatibility. Some older versions
627693
// of the SQLite driver stored index types with a " KEY" suffix, e.g.,
628694
// "PRIMARY KEY" or "UNIQUE KEY". More recent versions omit the suffix.

0 commit comments

Comments
 (0)