Skip to content

Commit 8afb8b4

Browse files
committed
Improve and document DEFAULT and CHECK expression formatting
1 parent 08ab6da commit 8afb8b4

2 files changed

Lines changed: 64 additions & 35 deletions

File tree

tests/WP_SQLite_Driver_Tests.php

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9219,13 +9219,13 @@ public function testCheckConstraints(): void {
92199219

92209220
// The of the check expressions below is not 100% matching MySQL,
92219221
// because in MySQL the expressions are parsed and normalized.
9222-
' CONSTRAINT `c1` CHECK ( id < 10 ),',
9223-
' CONSTRAINT `c2` CHECK ( start_timestamp < end_timestamp ),',
9224-
' CONSTRAINT `c3` CHECK ( length ( data ) < 20 ),',
9225-
' CONSTRAINT `t_chk_1` CHECK ( id > 0 ),',
9226-
" CONSTRAINT `t_chk_2` CHECK ( name != '' ),",
9227-
' CONSTRAINT `t_chk_3` CHECK ( score > 0 AND score < 100 ),',
9228-
' CONSTRAINT `t_chk_4` CHECK ( json_valid ( data ) )',
9222+
' CONSTRAINT `c1` CHECK (id < 10),',
9223+
' CONSTRAINT `c2` CHECK (start_timestamp < end_timestamp),',
9224+
' CONSTRAINT `c3` CHECK (length(data)< 20),',
9225+
' CONSTRAINT `t_chk_1` CHECK (id > 0),',
9226+
" CONSTRAINT `t_chk_2` CHECK (name != ''),",
9227+
' CONSTRAINT `t_chk_3` CHECK (score > 0 AND score < 100),',
9228+
' CONSTRAINT `t_chk_4` CHECK (json_valid(data))',
92299229
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
92309230
)
92319231
),
@@ -9251,8 +9251,8 @@ public function testAlterTableAddCheckConstraint(): void {
92519251
array(
92529252
'CREATE TABLE `t` (',
92539253
' `id` int DEFAULT NULL,',
9254-
' CONSTRAINT `c` CHECK ( id > 0 ),',
9255-
' CONSTRAINT `t_chk_1` CHECK ( id < 10 )',
9254+
' CONSTRAINT `c` CHECK (id > 0),',
9255+
' CONSTRAINT `t_chk_1` CHECK (id < 10)',
92569256
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
92579257
)
92589258
),
@@ -9321,7 +9321,7 @@ public function testCheckConstraintNotEnforced(): void {
93219321
array(
93229322
'CREATE TABLE `t` (',
93239323
' `id` int DEFAULT NULL,',
9324-
' CONSTRAINT `c` CHECK ( id > 0 ) /*!80016 NOT ENFORCED */',
9324+
' CONSTRAINT `c` CHECK (id > 0) /*!80016 NOT ENFORCED */',
93259325
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
93269326
)
93279327
),
@@ -11326,7 +11326,7 @@ public function testCreateTableWithDefaultNowFunction(): void {
1132611326
array(
1132711327
'CREATE TABLE `test_now_default` (',
1132811328
' `id` int NOT NULL,',
11329-
' `updated` timestamp NOT NULL DEFAULT ( now( ) ) ON UPDATE CURRENT_TIMESTAMP',
11329+
' `updated` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP',
1133011330
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci',
1133111331
)
1133211332
),
@@ -11351,7 +11351,7 @@ public function testCreateTableWithDefaultNowFunction(): void {
1135111351
'Type' => 'timestamp',
1135211352
'Null' => 'NO',
1135311353
'Key' => '',
11354-
'Default' => '( now( ) )',
11354+
'Default' => '(now())',
1135511355
'Extra' => 'DEFAULT_GENERATED on update CURRENT_TIMESTAMP',
1135611356
),
1135711357
),
@@ -11391,9 +11391,9 @@ public function testCreateTableWithDefaultExpressions(): void {
1139111391
array(
1139211392
'CREATE TABLE `t` (',
1139311393
' `id` int NOT NULL,',
11394-
' `col1` int NOT NULL DEFAULT ( 1 + 2 ),',
11395-
' `col2` datetime NOT NULL DEFAULT ( DATE_ADD( NOW( ) , INTERVAL 1 YEAR ) ),',
11396-
" `col3` varchar(255) NOT NULL DEFAULT ( CONCAT( 'a' , 'b' ) )",
11394+
' `col1` int NOT NULL DEFAULT (1 + 2),',
11395+
' `col2` datetime NOT NULL DEFAULT (DATE_ADD(NOW(), INTERVAL 1 YEAR)),',
11396+
" `col3` varchar(255) NOT NULL DEFAULT (CONCAT('a' , 'b'))",
1139711397
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci',
1139811398
)
1139911399
),
@@ -11418,23 +11418,23 @@ public function testCreateTableWithDefaultExpressions(): void {
1141811418
'Type' => 'int',
1141911419
'Null' => 'NO',
1142011420
'Key' => '',
11421-
'Default' => '( 1 + 2 )',
11421+
'Default' => '(1 + 2)',
1142211422
'Extra' => 'DEFAULT_GENERATED',
1142311423
),
1142411424
(object) array(
1142511425
'Field' => 'col2',
1142611426
'Type' => 'datetime',
1142711427
'Null' => 'NO',
1142811428
'Key' => '',
11429-
'Default' => '( DATE_ADD( NOW( ) , INTERVAL 1 YEAR ) )',
11429+
'Default' => '(DATE_ADD(NOW(), INTERVAL 1 YEAR))',
1143011430
'Extra' => 'DEFAULT_GENERATED',
1143111431
),
1143211432
(object) array(
1143311433
'Field' => 'col3',
1143411434
'Type' => 'varchar(255)',
1143511435
'Null' => 'NO',
1143611436
'Key' => '',
11437-
'Default' => "( CONCAT( 'a' , 'b' ) )",
11437+
'Default' => "(CONCAT('a' , 'b'))",
1143811438
'Extra' => 'DEFAULT_GENERATED',
1143911439
),
1144011440
),

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

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,10 +1912,7 @@ private function extract_check_constraint_data( WP_Parser_Node $node, string $ta
19121912
}
19131913

19141914
$expr = $check_constraint->get_first_child_node( 'exprWithParentheses' );
1915-
$check_clause = '';
1916-
foreach ( $expr->get_descendant_tokens() as $i => $token ) {
1917-
$check_clause .= ( $i > 0 ? ' ' : '' ) . $token->get_bytes();
1918-
}
1915+
$check_clause = $this->serialize_mysql_expression( $expr );
19191916

19201917
return array(
19211918
'constraint_schema' => self::SAVED_DATABASE_NAME,
@@ -2096,19 +2093,7 @@ private function get_column_default( WP_Parser_Node $node ): ?string {
20962093
// DEFAULT (expression) - MySQL 8.0.13+ supports exprWithParentheses
20972094
$expr_with_parens = $default_attr->get_first_child_node( 'exprWithParentheses' );
20982095
if ( $expr_with_parens ) {
2099-
$default_clause = '';
2100-
foreach ( $expr_with_parens->get_descendant_tokens() as $i => $token ) {
2101-
if ( WP_MySQL_Lexer::OPEN_PAR_SYMBOL === $token->id ) {
2102-
// TODO: This is just a quick fix to avoid inserting whitespace
2103-
// before '(', which would break function call expressions.
2104-
// The proper fix is to implement a "$node->get_bytes()" API.
2105-
// This same applies to the CHECK (expression) case as well.
2106-
$default_clause .= $token->get_bytes();
2107-
} else {
2108-
$default_clause .= ( $i > 0 ? ' ' : '' ) . $token->get_bytes();
2109-
}
2110-
}
2111-
return $default_clause;
2096+
return $this->serialize_mysql_expression( $expr_with_parens );
21122097
}
21132098

21142099
throw new Exception( 'DEFAULT value of this type is not supported.' );
@@ -3034,6 +3019,50 @@ private function get_value( WP_Parser_Node $node ): string {
30343019
return $full_value;
30353020
}
30363021

3022+
/**
3023+
* Serialize a MySQL expression for storing in the information schema.
3024+
*
3025+
* This is used for storing DEFAULT and CHECK expressions in the database.
3026+
*
3027+
* The current implementation is using a naive approach based on directly
3028+
* joining the original expression token bytes. This is safe, beacuase the
3029+
* original tokens must comprise a valid expression. While functionally
3030+
* equivalent, it is not strictly identical to what MySQL stores, because
3031+
* MySQL normalizes and prints the expression in a specific format.
3032+
*
3033+
* TODO: Consider implementing a full MySQL expression node -> string printer
3034+
* that would produce results that are identical to MySQL formatting.
3035+
* This gets tricky from MySQL 8, where a double-escaping regression
3036+
* was introduced, storing strings like "_utf8mb4\'abc\'" instead of
3037+
* "_utf8mb4'abc'", but displaying them correctly in SHOW statements.
3038+
* @see https://bugs.mysql.com/bug.php?id=100607
3039+
*
3040+
* @param WP_Parser_Node $node The AST node that needs to be serialized.
3041+
* @return string The serialized value of the node.
3042+
*/
3043+
private function serialize_mysql_expression( WP_Parser_Node $node ): string {
3044+
$value = '';
3045+
$last_token_id = null;
3046+
foreach ( $node->get_descendant_tokens() as $i => $token ) {
3047+
// Do not insert whitespace around parentheses. This is primarily to
3048+
// avoid inserting whitespace before '(', which may break function
3049+
// calls, depending on the value of the "IGNORE_SPACE" SQL mode.
3050+
if (
3051+
0 === $i
3052+
|| WP_MySQL_Lexer::OPEN_PAR_SYMBOL === $token->id
3053+
|| WP_MySQL_Lexer::CLOSE_PAR_SYMBOL === $token->id
3054+
|| WP_MySQL_Lexer::OPEN_PAR_SYMBOL === $last_token_id
3055+
|| WP_MySQL_Lexer::CLOSE_PAR_SYMBOL === $last_token_id
3056+
) {
3057+
$value .= $token->get_bytes();
3058+
} else {
3059+
$value .= ' ' . $token->get_bytes();
3060+
}
3061+
$last_token_id = $token->id;
3062+
}
3063+
return $value;
3064+
}
3065+
30373066
/**
30383067
* Insert values into an SQLite table.
30393068
*

0 commit comments

Comments
 (0)