Skip to content

Commit 21476a5

Browse files
committed
Refactor default value handling, add escaping and tests
1 parent baa22c3 commit 21476a5

File tree

2 files changed

+157
-9
lines changed

2 files changed

+157
-9
lines changed

tests/WP_SQLite_Information_Schema_Reconstructor_Tests.php

+60
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,66 @@ public function testReconstructTableFromMysqlDataTypesCache(): void {
223223
);
224224
}
225225

226+
public function testDefaultValues(): void {
227+
$this->engine->get_pdo()->exec(
228+
"
229+
CREATE TABLE t (
230+
col1 text DEFAULT abc,
231+
col2 text DEFAULT 'abc',
232+
col3 text DEFAULT \"abc\",
233+
col4 text DEFAULT NULL,
234+
col5 int DEFAULT TRUE,
235+
col6 int DEFAULT FALSE,
236+
col7 int DEFAULT 123,
237+
col8 real DEFAULT 1.23,
238+
col9 real DEFAULT -1.23,
239+
col10 real DEFAULT 1e3,
240+
col11 real DEFAULT 1.2e-3,
241+
col12 real DEFAULT 1_000_000,
242+
col13 int DEFAULT 0x1a2f,
243+
col14 int DEFAULT 0X1A2f,
244+
col15 blob DEFAULT x'4142432E',
245+
col16 blob DEFAULT x'4142432E',
246+
col17 text DEFAULT CURRENT_TIMESTAMP,
247+
col18 text DEFAULT CURRENT_DATE,
248+
col19 text DEFAULT CURRENT_TIME
249+
)
250+
"
251+
);
252+
253+
$this->reconstructor->ensure_correct_information_schema();
254+
$result = $this->assertQuery( 'SHOW CREATE TABLE t' );
255+
$this->assertSame(
256+
implode(
257+
"\n",
258+
array(
259+
'CREATE TABLE `t` (',
260+
" `col1` varchar(65535) DEFAULT 'abc',",
261+
" `col2` varchar(65535) DEFAULT 'abc',",
262+
" `col3` varchar(65535) DEFAULT 'abc',",
263+
' `col4` text DEFAULT NULL,',
264+
" `col5` int DEFAULT '1',",
265+
" `col6` int DEFAULT '0',",
266+
" `col7` int DEFAULT '123',",
267+
" `col8` float DEFAULT '1.23',",
268+
" `col9` float DEFAULT '-1.23',",
269+
" `col10` float DEFAULT '1e3',",
270+
" `col11` float DEFAULT '1.2e-3',",
271+
" `col12` float DEFAULT '1000000',",
272+
" `col13` int DEFAULT '6703',",
273+
" `col14` int DEFAULT '6703',",
274+
" `col15` varbinary(65535) DEFAULT 'ABC.',",
275+
" `col16` varbinary(65535) DEFAULT 'ABC.',",
276+
' `col17` datetime DEFAULT CURRENT_TIMESTAMP,',
277+
' `col18` text DEFAULT NULL,',
278+
' `col19` text DEFAULT NULL',
279+
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci',
280+
)
281+
),
282+
$result[0]->{'Create Table'}
283+
);
284+
}
285+
226286
private function assertQuery( $sql ) {
227287
$retval = $this->engine->query( $sql );
228288
$this->assertNotFalse( $retval );

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

+97-9
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,23 @@ private function generate_column_definition( string $table_name, array $column_i
267267
if ( null === $mysql_type ) {
268268
$mysql_type = $this->get_mysql_column_type( $column_info['type'] );
269269
}
270+
271+
/**
272+
* Correct some column types based on their default values:
273+
* 1. In MySQL, non-datetime columns can't have a timestamp default.
274+
* Let's use DATETIME when default is set to CURRENT_TIMESTAMP.
275+
* 2. In MySQL, TEXT and BLOB columns can't have a default value.
276+
* Let's use VARCHAR(65535) and VARBINARY(65535) when default is set.
277+
*/
278+
$default = $this->generate_column_default( $mysql_type, $column_info['dflt_value'] );
279+
if ( 'CURRENT_TIMESTAMP' === $default ) {
280+
$mysql_type = 'datetime';
281+
} elseif ( 'text' === $mysql_type && null !== $default ) {
282+
$mysql_type = 'varchar(65535)';
283+
} elseif ( 'blob' === $mysql_type && null !== $default ) {
284+
$mysql_type = 'varbinary(65535)';
285+
}
286+
270287
$definition[] = $mysql_type;
271288

272289
// NULL/NOT NULL.
@@ -288,8 +305,8 @@ private function generate_column_definition( string $table_name, array $column_i
288305
}
289306

290307
// Default value.
291-
if ( $this->column_has_default( $mysql_type, $column_info['dflt_value'] ) && ! $is_auto_increment ) {
292-
$definition[] = 'DEFAULT ' . $column_info['dflt_value'];
308+
if ( null !== $default && ! $is_auto_increment ) {
309+
$definition[] = 'DEFAULT ' . $default;
293310
}
294311

295312
return implode( ' ', $definition );
@@ -392,15 +409,15 @@ private function generate_key_definition( string $table_name, array $key_info, a
392409
}
393410

394411
/**
395-
* Determine if a column has a default value.
412+
* Generate a MySQL default value from an SQLite default value.
396413
*
397414
* @param string $mysql_type The MySQL data type of the column.
398415
* @param string|null $default_value The default value of the SQLite column.
399-
* @return bool True if the column has a default value, false otherwise.
416+
* @return string|null The default value, or null if the column has no default value.
400417
*/
401-
private function column_has_default( string $mysql_type, ?string $default_value ): bool {
418+
private function generate_column_default( string $mysql_type, ?string $default_value ): ?string {
402419
if ( null === $default_value || '' === $default_value ) {
403-
return false;
420+
return null;
404421
}
405422
$mysql_type = strtolower( $mysql_type );
406423

@@ -411,7 +428,7 @@ private function column_has_default( string $mysql_type, ?string $default_value
411428
* of the SQLite driver, TEXT columns were assigned a default value of ''.
412429
*/
413430
if ( 'geomcollection' === $mysql_type || 'geometrycollection' === $mysql_type ) {
414-
return false;
431+
return null;
415432
}
416433

417434
/*
@@ -424,9 +441,69 @@ private function column_has_default( string $mysql_type, ?string $default_value
424441
"''" === $default_value
425442
&& in_array( $mysql_type, array( 'datetime', 'date', 'time', 'timestamp', 'year' ), true )
426443
) {
427-
return false;
444+
return null;
445+
}
446+
447+
/**
448+
* Convert SQLite default values to MySQL default values.
449+
*
450+
* See:
451+
* - https://www.sqlite.org/syntax/column-constraint.html
452+
* - https://www.sqlite.org/syntax/literal-value.html
453+
* - https://www.sqlite.org/lang_expr.html#literal_values_constants_
454+
*/
455+
456+
// Quoted string literal. E.g.: 'abc', "abc", `abc`
457+
$first_byte = $default_value[0] ?? null;
458+
if ( '"' === $first_byte || "'" === $first_byte || '`' === $first_byte ) {
459+
return $this->escape_mysql_string_literal( substr( $default_value, 1, -1 ) );
460+
}
461+
462+
// Normalize the default value to for easier comparison.
463+
$uppercase_default_value = strtoupper( $default_value );
464+
465+
// NULL, TRUE, FALSE.
466+
if ( 'NULL' === $uppercase_default_value ) {
467+
// DEFAULT NULL is the same as no default value.
468+
return null;
469+
} elseif ( 'TRUE' === $uppercase_default_value ) {
470+
return '1';
471+
} elseif ( 'FALSE' === $uppercase_default_value ) {
472+
return '0';
473+
}
474+
475+
// Date/time values.
476+
if ( 'CURRENT_TIMESTAMP' === $uppercase_default_value ) {
477+
return 'CURRENT_TIMESTAMP';
478+
} elseif ( 'CURRENT_DATE' === $uppercase_default_value ) {
479+
return null; // Not supported in MySQL.
480+
} elseif ( 'CURRENT_TIME' === $uppercase_default_value ) {
481+
return null; // Not supported in MySQL.
482+
}
483+
484+
// SQLite supports underscores in all numeric literals.
485+
$no_underscore_default_value = str_replace( '_', '', $default_value );
486+
487+
// Numeric literals. E.g.: 123, 1.23, -1.23, 1e3, 1.2e-3
488+
if ( is_numeric( $no_underscore_default_value ) ) {
489+
return $no_underscore_default_value;
490+
}
491+
492+
// HEX literals (numeric). E.g.: 0x1a2f, 0X1A2F
493+
$value = filter_var( $no_underscore_default_value, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX );
494+
if ( false !== $value ) {
495+
return $value;
496+
}
497+
498+
// BLOB literals (string). E.g.: x'1a2f', X'1A2F'
499+
// Checking the prefix is enough as SQLite doesn't allow malformed values.
500+
if ( str_starts_with( $uppercase_default_value, "X'" ) ) {
501+
// Convert the hex string to ASCII bytes.
502+
return "'" . pack( 'H*', substr( $default_value, 2, -1 ) ) . "'";
428503
}
429-
return true;
504+
505+
// Unquoted string literal. E.g.: abc
506+
return $this->escape_mysql_string_literal( $default_value );
430507
}
431508

432509
/**
@@ -525,6 +602,17 @@ private function get_mysql_column_type( string $column_type ): string {
525602
return 'text';
526603
}
527604

605+
/**
606+
* Escape a string literal for MySQL DEFAULT values.
607+
*
608+
* @param string $literal The string literal to escape.
609+
* @return string The escaped string literal.
610+
*/
611+
private function escape_mysql_string_literal( string $literal ): string {
612+
// See: https://www.php.net/manual/en/mysqli.real-escape-string.php
613+
return "'" . addcslashes( $literal, "\0\n\r'\"\Z" ) . "'";
614+
}
615+
528616
/**
529617
* Quote an SQLite identifier.
530618
*

0 commit comments

Comments
 (0)