Skip to content

Commit a8a5604

Browse files
committed
Refactor and document key definition inference
1 parent 1121fcc commit a8a5604

3 files changed

+123
-29
lines changed

tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php

+58-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
use PHPUnit\Framework\TestCase;
44

55
class WP_SQLite_Information_Schema_Reconstructor_Tests extends TestCase {
6+
const CREATE_DATA_TYPES_CACHE_TABLE_SQL = '
7+
CREATE TABLE _mysql_data_types_cache (
8+
`table` TEXT NOT NULL,
9+
`column_or_index` TEXT NOT NULL,
10+
`mysql_type` TEXT NOT NULL,
11+
PRIMARY KEY(`table`, `column_or_index`)
12+
)';
13+
614
/** @var WP_SQLite_Driver */
715
private $engine;
816

@@ -107,8 +115,8 @@ public function testReconstructInformationSchemaTable(): void {
107115
' PRIMARY KEY (`id`),',
108116
' KEY `idx_role_score` (`role`(100), `priority`),',
109117
' KEY `idx_score` (`score`),',
110-
' UNIQUE KEY `sqlite_autoindex_t_2` (`name`(100)),',
111-
' UNIQUE KEY `sqlite_autoindex_t_1` (`email`(100))',
118+
' UNIQUE KEY `name` (`name`(100)),',
119+
' UNIQUE KEY `email` (`email`(100))',
112120
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
113121
)
114122
),
@@ -167,6 +175,54 @@ public function testReconstructInformationSchemaTableWithWpTables(): void {
167175
);
168176
}
169177

178+
public function testReconstructInformationSchemaFromMysqlDataTypesCache(): void {
179+
$pdo = $this->engine->get_pdo();
180+
181+
$pdo->exec( self::CREATE_DATA_TYPES_CACHE_TABLE_SQL );
182+
$pdo->exec( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'id', 'int unsigned')" );
183+
$pdo->exec( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'name', 'varchar(255)')" );
184+
$pdo->exec( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'description', 'text')" );
185+
$pdo->exec( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 'shape', 'geomcollection')" );
186+
$pdo->exec( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 't__idx_name', 'KEY')" );
187+
$pdo->exec( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 't__idx_description', 'FULLTEXT')" );
188+
$pdo->exec( "INSERT INTO _mysql_data_types_cache (`table`, column_or_index, mysql_type) VALUES ('t', 't__idx_shape', 'SPATIAL')" );
189+
190+
$this->engine->get_pdo()->exec(
191+
'
192+
CREATE TABLE t (
193+
id INTEGER PRIMARY KEY AUTOINCREMENT,
194+
name TEXT,
195+
description TEXT,
196+
shape TEXT NOT NULL
197+
)
198+
'
199+
);
200+
$this->engine->get_pdo()->exec( 'CREATE INDEX t__idx_name ON t (name)' );
201+
$this->engine->get_pdo()->exec( 'CREATE INDEX t__idx_description ON t (description)' );
202+
$this->engine->get_pdo()->exec( 'CREATE INDEX t__idx_shape ON t (shape)' );
203+
204+
$this->reconstructor->ensure_correct_information_schema();
205+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
206+
$this->assertSame(
207+
implode(
208+
"\n",
209+
array(
210+
'CREATE TABLE `t` (',
211+
' `id` int unsigned NOT NULL AUTO_INCREMENT,',
212+
' `name` varchar(255) DEFAULT NULL,',
213+
' `description` text DEFAULT NULL,',
214+
' `shape` geomcollection NOT NULL,',
215+
' PRIMARY KEY (`id`),',
216+
' SPATIAL KEY `idx_shape` (`shape`(32)),',
217+
' FULLTEXT KEY `idx_description` (`description`(100)),',
218+
' KEY `idx_name` (`name`(100))',
219+
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
220+
)
221+
),
222+
$result[0]->{'Create Table'}
223+
);
224+
}
225+
170226
private function assertQuery( $sql ) {
171227
$retval = $this->engine->query( $sql );
172228
$this->assertNotFalse( $retval );

wp-includes/sqlite-ast/class-wp-sqlite-driver.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -3459,7 +3459,12 @@ function ( $column ) {
34593459
} else {
34603460
$is_unique = '0' === $info['NON_UNIQUE'];
34613461

3462-
$sql = sprintf( ' %sKEY ', $is_unique ? 'UNIQUE ' : '' );
3462+
$sql = sprintf(
3463+
' %s%s%sKEY ',
3464+
$is_unique ? 'UNIQUE ' : '',
3465+
'FULLTEXT' === $info['INDEX_TYPE'] ? 'FULLTEXT ' : '',
3466+
'SPATIAL' === $info['INDEX_TYPE'] ? 'SPATIAL ' : ''
3467+
);
34633468
$sql .= $this->quote_mysql_identifier( $info['INDEX_NAME'] );
34643469
$sql .= ' (';
34653470
$sql .= implode(

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

+59-26
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,13 @@ private function get_existing_table_names(): array {
125125
SELECT name
126126
FROM sqlite_schema
127127
WHERE type = 'table'
128+
AND name != ?
128129
AND name NOT LIKE ? ESCAPE '\'
129130
AND name NOT LIKE ? ESCAPE '\'
130131
ORDER BY name
131132
",
132133
array(
134+
'_mysql_data_types_cache',
133135
'sqlite\_%',
134136
str_replace( '_', '\_', WP_SQLite_Driver::RESERVED_PREFIX ) . '%',
135137
)
@@ -160,15 +162,15 @@ private function generate_create_table_statement( string $table_name ): string {
160162
sprintf( 'PRAGMA table_xinfo("%s")', $table_name )
161163
)->fetchAll( PDO::FETCH_ASSOC );
162164

163-
$definitions = array();
164-
$data_types = array();
165+
$definitions = array();
166+
$column_types = array();
165167
foreach ( $columns as $column ) {
166168
$mysql_type = $this->get_cached_mysql_data_type( $table_name, $column['name'] );
167169
if ( null === $mysql_type ) {
168170
$mysql_type = $this->get_mysql_data_type( $column['type'] );
169171
}
170-
$definitions[] = $this->get_column_definition( $table_name, $column );
171-
$data_types[ $column['name'] ] = $mysql_type;
172+
$definitions[] = $this->get_column_definition( $table_name, $column );
173+
$column_types[ $column['name'] ] = $mysql_type;
172174
}
173175

174176
// Primary key.
@@ -199,18 +201,12 @@ private function generate_create_table_statement( string $table_name ): string {
199201
)->fetchAll( PDO::FETCH_ASSOC );
200202

201203
foreach ( $keys as $key ) {
202-
$key_columns = $this->driver->execute_sqlite_query(
203-
'SELECT * FROM pragma_index_info("' . $key['name'] . '")'
204-
)->fetchAll( PDO::FETCH_ASSOC );
205-
206-
// If the PK columns are the same as the UK columns, skip the key.
207-
// This is because a primary key is already unique in MySQL.
208-
$key_equals_pk = ! array_diff( $pk_columns, array_column( $key_columns, 'name' ) );
209-
$is_auto_index = strpos( $key['name'], 'sqlite_autoindex_' ) === 0;
210-
if ( $is_auto_index && $key['unique'] && $key_equals_pk ) {
204+
// Skip the internal index that SQLite may create for a primary key.
205+
// In MySQL, no explicit index needs to be defined for a primary key.
206+
if ( 'pk' === $key['origin'] ) {
211207
continue;
212208
}
213-
$definitions[] = $this->get_key_definition( $key, $key_columns, $data_types );
209+
$definitions[] = $this->get_key_definition( $table_name, $key, $column_types );
214210
}
215211

216212
return sprintf(
@@ -271,25 +267,52 @@ private function get_column_definition( string $table_name, array $column_info )
271267
*
272268
* This method generates a MySQL key definition from SQLite key data.
273269
*
270+
* @param string $table_name The name of the table.
274271
* @param array $key The SQLite key information.
275-
* @param array $key_columns The SQLite key column information.
276-
* @param array $data_types The MySQL data types of the columns.
272+
* @param array $column_type The MySQL data types of the columns.
277273
* @return string The MySQL key definition.
278274
*/
279-
private function get_key_definition( array $key, array $key_columns, array $data_types ): string {
280-
// Key definition.
275+
private function get_key_definition( string $table_name, array $key, array $column_type ): string {
281276
$definition = array();
282-
if ( $key['unique'] ) {
283-
$definition[] = 'UNIQUE';
277+
278+
// Key type.
279+
$cached_type = $this->get_cached_mysql_data_type( $table_name, $key['name'] );
280+
if ( 'FULLTEXT' === $cached_type ) {
281+
$definition[] = 'FULLTEXT KEY';
282+
} elseif ( 'SPATIAL' === $cached_type ) {
283+
$definition[] = 'SPATIAL KEY';
284+
} elseif ( 'UNIQUE' === $cached_type || '1' === $key['unique'] ) {
285+
$definition[] = 'UNIQUE KEY';
286+
} else {
287+
$definition[] = 'KEY';
288+
}
289+
290+
// Key name.
291+
$name = $key['name'];
292+
293+
/*
294+
* The SQLite driver prefixes index names with "{$table_name}__" to avoid
295+
* naming conflicts among tables in SQLite. We need to remove the prefix.
296+
*/
297+
if ( str_starts_with( $name, "{$table_name}__" ) ) {
298+
$name = substr( $name, strlen( "{$table_name}__" ) );
284299
}
285-
$definition[] = 'KEY';
286300

287-
// Remove the prefix from the index name if there is any. We use __ as a separator.
288-
$index_name = explode( '__', $key['name'], 2 )[1] ?? $key['name'];
289-
$definition[] = $this->quote_sqlite_identifier( $index_name );
301+
/**
302+
* SQLite creates automatic internal indexes for primary and unique keys,
303+
* naming them in format "sqlite_autoindex_{$table_name}_{$index_id}".
304+
* For these internal indexes, we need to skip their name, so that in
305+
* the generated MySQL definition, they follow implicit MySQL naming.
306+
*/
307+
if ( ! str_starts_with( $name, 'sqlite_autoindex_' ) ) {
308+
$definition[] = $this->quote_sqlite_identifier( $name );
309+
}
290310

291311
// Key columns.
292-
$cols = array();
312+
$key_columns = $this->driver->execute_sqlite_query(
313+
'SELECT * FROM pragma_index_info("' . $key['name'] . '")'
314+
)->fetchAll( PDO::FETCH_ASSOC );
315+
$cols = array();
293316
foreach ( $key_columns as $column ) {
294317
/*
295318
* Extract type and length from column data type definition.
@@ -299,7 +322,7 @@ private function get_key_definition( array $key, array $key_columns, array $data
299322
* the format "type(length)", such as "varchar(255)".
300323
*/
301324
$max_prefix_length = 100;
302-
$type = strtolower( $data_types[ $column['name'] ] );
325+
$type = strtolower( $column_type[ $column['name'] ] );
303326
$parts = explode( '(', $type );
304327
$column_type = $parts[0];
305328
$column_length = isset( $parts[1] ) ? (int) $parts[1] : null;
@@ -362,6 +385,12 @@ private function column_has_default( string $mysql_type, ?string $default_value
362385
* that was used by an old version of the SQLite driver and that is otherwise
363386
* no longer needed. This is more precise than direct inference from SQLite.
364387
*
388+
* For columns, it returns full column type, including prefix length, e.g.:
389+
* int(11), bigint(20) unsigned, varchar(255), longtext
390+
*
391+
* For indexes, it returns one of:
392+
* KEY, PRIMARY, UNIQUE, FULLTEXT, SPATIAL
393+
*
365394
* @param string $table_name The table name.
366395
* @param string $column_or_index_name The column or index name.
367396
* @return string|null The MySQL definition, or null when not found.
@@ -378,6 +407,10 @@ private function get_cached_mysql_data_type( string $table_name, string $column_
378407
}
379408
throw $e;
380409
}
410+
411+
// Normalize index type for backward compatibility. Some older versions
412+
// of the SQLite driver stored index types with a " KEY" suffix, e.g.,
413+
// "PRIMARY KEY" or "UNIQUE KEY". More recent versions omit the suffix.
381414
if ( str_ends_with( $mysql_type, ' KEY' ) ) {
382415
$mysql_type = substr( $mysql_type, 0, strlen( $mysql_type ) - strlen( ' KEY' ) );
383416
}

0 commit comments

Comments
 (0)