From a6baded501ed9ff1918b8c36a063a19af3069f6d Mon Sep 17 00:00:00 2001 From: Antony Agrios Date: Thu, 11 Apr 2024 13:11:00 +0300 Subject: [PATCH 01/13] Support UPDATE LIMIT --- .../sqlite/class-wp-sqlite-translator.php | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 34ad86b7..0d92c0a9 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1550,7 +1550,24 @@ private function execute_describe() { */ private function execute_update() { $this->rewriter->consume(); // Update. - + $limit = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'LIMIT', + ) + ); + $order_by = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'ORDER BY', + ) + ); + $where = $this->rewriter->peek( + array( + 'type' => WP_SQLite_Token::TYPE_KEYWORD, + 'value' => 'WHERE', + ) + ); $params = array(); while ( true ) { $token = $this->rewriter->peek(); @@ -1558,6 +1575,12 @@ private function execute_update() { break; } + if ( $token->value === 'WHERE' && ( $limit || $order_by ) ) { + $this->remember_last_reserved_keyword( $token ); + $this->rewriter->consume(); + $this->prepare_update_for_limit_or_order(); + } + // Record the table name. if ( ! $this->table_name && @@ -1580,6 +1603,11 @@ private function execute_update() { $this->rewriter->consume(); } + + if ( $where && ( $limit || $order_by ) ) { + $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR )); + } + $this->rewriter->consume_all(); $updated_query = $this->rewriter->get_updated_query(); @@ -1587,6 +1615,23 @@ private function execute_update() { $this->set_result_from_affected_rows(); } + private function prepare_update_for_limit_or_order() { + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'IN', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); + $this->rewriter->add( new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'FROM', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( $this->table_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); + $this->rewriter->add( new WP_SQLite_Token( 'WHERE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + } /** * Executes a INSERT or REPLACE statement. */ From e90500aeedd3343b907c2523f6c072d029f582cd Mon Sep 17 00:00:00 2001 From: Antony Agrios Date: Thu, 11 Apr 2024 15:31:55 +0300 Subject: [PATCH 02/13] Update: Add tests --- tests/WP_SQLite_Translator_Tests.php | 30 +++++++++++++++++++ .../sqlite/class-wp-sqlite-translator.php | 14 +++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index cfebc685..cf80e361 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -129,6 +129,36 @@ public function testInsertDateNow() { $this->assertEquals( gmdate( 'Y' ), $results[0]->y ); } + public function testUpdateWithLimit() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45');" + ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1;" + ); + $results = $this->engine->get_query_results(); + + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); + + $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); + } + + public function testUpdateWithLimitNoEndToken() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45')" + ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1" + ); + $results = $this->engine->get_query_results(); + + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + + $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); + } + public function testCastAsBinary() { $this->assertQuery( // Use a confusing alias to make sure it replaces only the correct token diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 0d92c0a9..aefdabec 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1550,7 +1550,7 @@ private function execute_describe() { */ private function execute_update() { $this->rewriter->consume(); // Update. - $limit = $this->rewriter->peek( + $limit = $this->rewriter->peek( array( 'type' => WP_SQLite_Token::TYPE_KEYWORD, 'value' => 'LIMIT', @@ -1562,13 +1562,13 @@ private function execute_update() { 'value' => 'ORDER BY', ) ); - $where = $this->rewriter->peek( + $where = $this->rewriter->peek( array( 'type' => WP_SQLite_Token::TYPE_KEYWORD, 'value' => 'WHERE', ) ); - $params = array(); + $params = array(); while ( true ) { $token = $this->rewriter->peek(); if ( ! $token ) { @@ -1581,6 +1581,10 @@ private function execute_update() { $this->prepare_update_for_limit_or_order(); } + if ( $token->value === ';' && ( $limit || $order_by ) ) { + $this->rewriter->skip(); + } + // Record the table name. if ( ! $this->table_name && @@ -1604,8 +1608,8 @@ private function execute_update() { $this->rewriter->consume(); } - if ( $where && ( $limit || $order_by ) ) { - $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR )); + if ( $where && ( $limit || $order_by ) ) { + $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); } $this->rewriter->consume_all(); From e1c20b75f28ff1676bf95ade758fc667d2b4c8c2 Mon Sep 17 00:00:00 2001 From: Antony Agrios Date: Thu, 11 Apr 2024 15:38:20 +0300 Subject: [PATCH 03/13] Update: Ensure more that ; is a delimiter --- wp-includes/sqlite/class-wp-sqlite-translator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index aefdabec..22787a66 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1581,7 +1581,7 @@ private function execute_update() { $this->prepare_update_for_limit_or_order(); } - if ( $token->value === ';' && ( $limit || $order_by ) ) { + if ( $token->value === ';' && $token->type === WP_SQLite_Token::TYPE_DELIMITER && ( $limit || $order_by ) ) { $this->rewriter->skip(); } From 0c17566ba759ac4b6e06dfc7be03afb02eddb0a6 Mon Sep 17 00:00:00 2001 From: Antony Agrios Date: Mon, 15 Apr 2024 10:41:20 +0300 Subject: [PATCH 04/13] Update: Add more test cases and switch to add_many function --- tests/WP_SQLite_Translator_Tests.php | 10 ++++++ .../sqlite/class-wp-sqlite-translator.php | 34 +++++++++++-------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index cf80e361..d7859169 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -133,6 +133,9 @@ public function testUpdateWithLimit() { $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45');" ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45');" + ); $this->assertQuery( "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1;" @@ -140,14 +143,19 @@ public function testUpdateWithLimit() { $results = $this->engine->get_query_results(); $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" ); $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); + $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value ); } public function testUpdateWithLimitNoEndToken() { $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 00:00:45')" ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-28 00:00:45')" + ); $this->assertQuery( "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1" @@ -155,8 +163,10 @@ public function testUpdateWithLimitNoEndToken() { $results = $this->engine->get_query_results(); $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); + $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value ); } public function testCastAsBinary() { diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 22787a66..865e5856 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1620,21 +1620,25 @@ private function execute_update() { } private function prepare_update_for_limit_or_order() { - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( 'IN', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ) ); - $this->rewriter->add( new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( 'FROM', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( $this->table_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); - $this->rewriter->add( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ) ); - $this->rewriter->add( new WP_SQLite_Token( 'WHERE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ) ); + $this->rewriter->add_many( + array( + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'IN', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( '(', WP_SQLite_Token::TYPE_OPERATOR ), + new WP_SQLite_Token( 'SELECT', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'rowid', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_KEY ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'FROM', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( $this->table_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), + new WP_SQLite_Token( 'WHERE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), + ) + ); } /** * Executes a INSERT or REPLACE statement. From 704a2d0705423794cebdb91aa4c926b2553ef01f Mon Sep 17 00:00:00 2001 From: Antony Agrios Date: Mon, 15 Apr 2024 10:42:19 +0300 Subject: [PATCH 05/13] Update wp-includes/sqlite/class-wp-sqlite-translator.php Co-authored-by: Adam Zielinski --- wp-includes/sqlite/class-wp-sqlite-translator.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 865e5856..1d3fb6ce 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1547,6 +1547,15 @@ private function execute_describe() { /** * Executes an UPDATE statement. + * Supported syntax: + * + * UPDATE [LOW_PRIORITY] [IGNORE] table_reference + * SET assignment_list + * [WHERE where_condition] + * [ORDER BY ...] + * [LIMIT row_count] + * + * @see https://dev.mysql.com/doc/refman/8.0/en/update.html */ private function execute_update() { $this->rewriter->consume(); // Update. From c1440780dc110fbaeaddde7cb6f0d05542d87ff9 Mon Sep 17 00:00:00 2001 From: Antony Agrios Date: Mon, 15 Apr 2024 11:00:58 +0300 Subject: [PATCH 06/13] Update: Add comments to explain the row id transformation further --- wp-includes/sqlite/class-wp-sqlite-translator.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 1d3fb6ce..3acb6785 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1584,12 +1584,23 @@ private function execute_update() { break; } + /* + * If the query contains a WHERE clause, and either a LIMIT or ORDER BY clause, + * we need to rewrite the query to use a nested SELECT statement. + * eg: + * - UPDATE table SET column = value WHERE condition LIMIT 1; + * will be rewritten to: + * - UPDATE table SET column = value WHERE rowid IN (SELECT rowid FROM table WHERE condition LIMIT 1); + */ if ( $token->value === 'WHERE' && ( $limit || $order_by ) ) { $this->remember_last_reserved_keyword( $token ); $this->rewriter->consume(); $this->prepare_update_for_limit_or_order(); } - + /* + * In case we rewrite the query, we need to skip the semicolon. + * This is because the semicolon becomes part of the nested SELECT statement, and it breaks the query. + */ if ( $token->value === ';' && $token->type === WP_SQLite_Token::TYPE_DELIMITER && ( $limit || $order_by ) ) { $this->rewriter->skip(); } From c2a8b633af7fcad461da852e26a93391fc3b471b Mon Sep 17 00:00:00 2001 From: Antony Agrios Date: Mon, 15 Apr 2024 12:13:01 +0300 Subject: [PATCH 07/13] Update wp-includes/sqlite/class-wp-sqlite-translator.php Co-authored-by: Mukesh Panchal --- wp-includes/sqlite/class-wp-sqlite-translator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 3acb6785..d7140969 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1598,9 +1598,9 @@ private function execute_update() { $this->prepare_update_for_limit_or_order(); } /* - * In case we rewrite the query, we need to skip the semicolon. - * This is because the semicolon becomes part of the nested SELECT statement, and it breaks the query. - */ + * In case we rewrite the query, we need to skip the semicolon. + * This is because the semicolon becomes part of the nested SELECT statement, and it breaks the query. + */ if ( $token->value === ';' && $token->type === WP_SQLite_Token::TYPE_DELIMITER && ( $limit || $order_by ) ) { $this->rewriter->skip(); } From 1510b6a8d8dd4d3cac4aa0383484e64fe47dc35b Mon Sep 17 00:00:00 2001 From: Antony Agrios Date: Mon, 15 Apr 2024 18:04:20 +0300 Subject: [PATCH 08/13] Update: wrap where in select on update --- tests/WP_SQLite_Translator_Tests.php | 29 ++++++++++- .../sqlite/class-wp-sqlite-translator.php | 49 +++++++------------ 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index d7859169..928289eb 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -140,13 +140,26 @@ public function testUpdateWithLimit() { $this->assertQuery( "UPDATE _dates SET option_value = '2001-05-27 10:08:48' WHERE option_name = 'first' ORDER BY option_name LIMIT 1;" ); - $results = $this->engine->get_query_results(); $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" ); $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:49' WHERE option_name = 'first';" + ); + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); + $this->assertEquals( '2001-05-27 10:08:49', $result1[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-12 10:00:40' WHERE option_name in ( SELECT option_name from _dates );" + ); + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first';" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second';" ); + $this->assertEquals( '2001-05-12 10:00:40', $result1[0]->option_value ); + $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value ); } public function testUpdateWithLimitNoEndToken() { @@ -167,6 +180,20 @@ public function testUpdateWithLimitNoEndToken() { $this->assertEquals( '2001-05-27 10:08:48', $result1[0]->option_value ); $this->assertEquals( '2003-05-28 00:00:45', $result2[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-27 10:08:49' WHERE option_name = 'first'" + ); + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $this->assertEquals( '2001-05-27 10:08:49', $result1[0]->option_value ); + + $this->assertQuery( + "UPDATE _dates SET option_value = '2001-05-12 10:00:40' WHERE option_name in ( SELECT option_name from _dates )" + ); + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); + $this->assertEquals( '2001-05-12 10:00:40', $result1[0]->option_value ); + $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value ); } public function testCastAsBinary() { diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index d7140969..23e04bce 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1558,26 +1558,14 @@ private function execute_describe() { * @see https://dev.mysql.com/doc/refman/8.0/en/update.html */ private function execute_update() { - $this->rewriter->consume(); // Update. - $limit = $this->rewriter->peek( - array( - 'type' => WP_SQLite_Token::TYPE_KEYWORD, - 'value' => 'LIMIT', - ) - ); - $order_by = $this->rewriter->peek( - array( - 'type' => WP_SQLite_Token::TYPE_KEYWORD, - 'value' => 'ORDER BY', - ) - ); - $where = $this->rewriter->peek( + $this->rewriter->consume(); // Consume the UPDATE keyword. + $where = $this->rewriter->peek( array( 'type' => WP_SQLite_Token::TYPE_KEYWORD, 'value' => 'WHERE', ) ); - $params = array(); + $params = array(); while ( true ) { $token = $this->rewriter->peek(); if ( ! $token ) { @@ -1585,33 +1573,31 @@ private function execute_update() { } /* - * If the query contains a WHERE clause, and either a LIMIT or ORDER BY clause, + * If the query contains a WHERE clause, * we need to rewrite the query to use a nested SELECT statement. * eg: * - UPDATE table SET column = value WHERE condition LIMIT 1; * will be rewritten to: * - UPDATE table SET column = value WHERE rowid IN (SELECT rowid FROM table WHERE condition LIMIT 1); */ - if ( $token->value === 'WHERE' && ( $limit || $order_by ) ) { + if ( $token->value === 'WHERE' ) { $this->remember_last_reserved_keyword( $token ); $this->rewriter->consume(); - $this->prepare_update_for_limit_or_order(); + $this->prepare_update_nested_query(); } - /* - * In case we rewrite the query, we need to skip the semicolon. - * This is because the semicolon becomes part of the nested SELECT statement, and it breaks the query. - */ - if ( $token->value === ';' && $token->type === WP_SQLite_Token::TYPE_DELIMITER && ( $limit || $order_by ) ) { - $this->rewriter->skip(); + + // Ignore the semicolon in case of rewritten query as it breaks the query. + if ( ';' === $this->rewriter->peek()->value && $this->rewriter->peek()->type === WP_SQLite_Token::TYPE_DELIMITER ) { + break; } // Record the table name. if ( - ! $this->table_name && - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_RESERVED - ) + ! $this->table_name && + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED + ) ) { $this->table_name = $token->value; } @@ -1628,7 +1614,8 @@ private function execute_update() { $this->rewriter->consume(); } - if ( $where && ( $limit || $order_by ) ) { + // Wrap up the WHERE clause with the nested SELECT statement + if ( $where ) { $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); } @@ -1639,7 +1626,7 @@ private function execute_update() { $this->set_result_from_affected_rows(); } - private function prepare_update_for_limit_or_order() { + private function prepare_update_nested_query() { $this->rewriter->add_many( array( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), From c0596ec94f3c68621bbd6a96300098ba0be97566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 16 Apr 2024 12:38:52 +0200 Subject: [PATCH 09/13] Add two more test cases --- tests/WP_SQLite_Translator_Tests.php | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 928289eb..8b647780 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -196,6 +196,45 @@ public function testUpdateWithLimitNoEndToken() { $this->assertEquals( '2001-05-12 10:00:40', $result2[0]->option_value ); } + public function testUpdateWithoutWhereButWithSubSelect() { + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('User 0000019', 'second');" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48');" + ); + $return = $this->assertQuery( + "UPDATE _dates SET option_value = (SELECT option_value from _options WHERE option_name = 'User 0000019')" + ); + $this->assertSame( 2, $return, 'UPDATE query did not return 2 when two row were changed' ); + + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); + $this->assertEquals( 'second', $result1[0]->option_value ); + $this->assertEquals( 'second', $result2[0]->option_value ); + } + + public function testUpdateWithoutWhereButWithLimit() { + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('first', '2003-05-27 10:08:48');" + ); + $this->assertQuery( + "INSERT INTO _dates (option_name, option_value) VALUES ('second', '2003-05-27 10:08:48');" + ); + $return = $this->assertQuery( + "UPDATE _dates SET option_value = 'second' LIMIT 1" + ); + $this->assertSame( 2, $return, 'UPDATE query did not return 2 when two row were changed' ); + + $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); + $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); + $this->assertEquals( '2003-05-27 10:08:48', $result1[0]->option_value ); + $this->assertEquals( 'second', $result2[0]->option_value ); + } + public function testCastAsBinary() { $this->assertQuery( // Use a confusing alias to make sure it replaces only the correct token From 38fe052d814de8bdae4055b114917c73d3e5eaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 16 Apr 2024 12:49:05 +0200 Subject: [PATCH 10/13] Handle UPDATE with limit but no WHERE, handle UPDATE with a nested SELECT but no WHERE --- tests/WP_SQLite_Translator_Tests.php | 6 ++-- .../sqlite/class-wp-sqlite-translator.php | 31 ++++++++++++------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 8b647780..b1cdea7f 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -227,12 +227,12 @@ public function testUpdateWithoutWhereButWithLimit() { $return = $this->assertQuery( "UPDATE _dates SET option_value = 'second' LIMIT 1" ); - $this->assertSame( 2, $return, 'UPDATE query did not return 2 when two row were changed' ); + $this->assertSame( 1, $return, 'UPDATE query did not return 2 when two row were changed' ); $result1 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='first'" ); $result2 = $this->engine->query( "SELECT option_value FROM _dates WHERE option_name='second'" ); - $this->assertEquals( '2003-05-27 10:08:48', $result1[0]->option_value ); - $this->assertEquals( 'second', $result2[0]->option_value ); + $this->assertEquals( 'second', $result1[0]->option_value ); + $this->assertEquals( '2003-05-27 10:08:48', $result2[0]->option_value ); } public function testCastAsBinary() { diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 23e04bce..42e1276b 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1559,12 +1559,7 @@ private function execute_describe() { */ private function execute_update() { $this->rewriter->consume(); // Consume the UPDATE keyword. - $where = $this->rewriter->peek( - array( - 'type' => WP_SQLite_Token::TYPE_KEYWORD, - 'value' => 'WHERE', - ) - ); + $has_where = false; $params = array(); while ( true ) { $token = $this->rewriter->peek(); @@ -1580,10 +1575,23 @@ private function execute_update() { * will be rewritten to: * - UPDATE table SET column = value WHERE rowid IN (SELECT rowid FROM table WHERE condition LIMIT 1); */ - if ( $token->value === 'WHERE' ) { - $this->remember_last_reserved_keyword( $token ); - $this->rewriter->consume(); - $this->prepare_update_nested_query(); + if ($this->rewriter->depth === 0) { + if (($token->value === 'LIMIT' || $token->value === 'ORDER') && !$has_where) { + $this->rewriter->add( + new WP_SQLite_Token('WHERE', WP_SQLite_Token::TYPE_KEYWORD), + ); + $has_where = true; + $this->remember_last_reserved_keyword($token); + $this->prepare_update_nested_query(); + } else if ($token->value === 'WHERE') { + $has_where = true; + $this->remember_last_reserved_keyword($token); + $this->rewriter->consume(); + $this->prepare_update_nested_query(); + $this->rewriter->add( + new WP_SQLite_Token('WHERE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED) + ); + } } // Ignore the semicolon in case of rewritten query as it breaks the query. @@ -1615,7 +1623,7 @@ private function execute_update() { } // Wrap up the WHERE clause with the nested SELECT statement - if ( $where ) { + if ( $has_where ) { $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); } @@ -1643,7 +1651,6 @@ private function prepare_update_nested_query() { new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), new WP_SQLite_Token( $this->table_name, WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), - new WP_SQLite_Token( 'WHERE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED ), ) ); } From 81b3767f6763c22959772b6ccf74dcb5afbca4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 16 Apr 2024 12:51:31 +0200 Subject: [PATCH 11/13] Use `$needs_closing_parenthesis` as a variable name --- .../sqlite/class-wp-sqlite-translator.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 42e1276b..4b086226 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1560,6 +1560,7 @@ private function execute_describe() { private function execute_update() { $this->rewriter->consume(); // Consume the UPDATE keyword. $has_where = false; + $needs_closing_parenthesis = false; $params = array(); while ( true ) { $token = $this->rewriter->peek(); @@ -1580,11 +1581,12 @@ private function execute_update() { $this->rewriter->add( new WP_SQLite_Token('WHERE', WP_SQLite_Token::TYPE_KEYWORD), ); - $has_where = true; + $needs_closing_parenthesis = true; $this->remember_last_reserved_keyword($token); $this->prepare_update_nested_query(); } else if ($token->value === 'WHERE') { $has_where = true; + $needs_closing_parenthesis = true; $this->remember_last_reserved_keyword($token); $this->rewriter->consume(); $this->prepare_update_nested_query(); @@ -1601,11 +1603,11 @@ private function execute_update() { // Record the table name. if ( - ! $this->table_name && - ! $token->matches( - WP_SQLite_Token::TYPE_KEYWORD, - WP_SQLite_Token::FLAG_KEYWORD_RESERVED - ) + ! $this->table_name && + ! $token->matches( + WP_SQLite_Token::TYPE_KEYWORD, + WP_SQLite_Token::FLAG_KEYWORD_RESERVED + ) ) { $this->table_name = $token->value; } @@ -1623,7 +1625,7 @@ private function execute_update() { } // Wrap up the WHERE clause with the nested SELECT statement - if ( $has_where ) { + if ( $needs_closing_parenthesis ) { $this->rewriter->add( new WP_SQLite_Token( ')', WP_SQLite_Token::TYPE_OPERATOR ) ); } From 87a17ef8f1f47b2203befa8de1cdd5fdb4293bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 16 Apr 2024 12:52:22 +0200 Subject: [PATCH 12/13] Remove a call to $this->remember_last_reserved_keyword($token); --- wp-includes/sqlite/class-wp-sqlite-translator.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 4b086226..93b24349 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1582,12 +1582,10 @@ private function execute_update() { new WP_SQLite_Token('WHERE', WP_SQLite_Token::TYPE_KEYWORD), ); $needs_closing_parenthesis = true; - $this->remember_last_reserved_keyword($token); $this->prepare_update_nested_query(); } else if ($token->value === 'WHERE') { $has_where = true; $needs_closing_parenthesis = true; - $this->remember_last_reserved_keyword($token); $this->rewriter->consume(); $this->prepare_update_nested_query(); $this->rewriter->add( From a44cb392fd10b7dd32687ca80787e3f4d80e1992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 16 Apr 2024 12:54:18 +0200 Subject: [PATCH 13/13] Improve naming --- .../sqlite/class-wp-sqlite-translator.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-translator.php b/wp-includes/sqlite/class-wp-sqlite-translator.php index 93b24349..346b1ed1 100644 --- a/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1582,12 +1582,12 @@ private function execute_update() { new WP_SQLite_Token('WHERE', WP_SQLite_Token::TYPE_KEYWORD), ); $needs_closing_parenthesis = true; - $this->prepare_update_nested_query(); + $this->preface_WHERE_clause_with_a_subquery(); } else if ($token->value === 'WHERE') { $has_where = true; $needs_closing_parenthesis = true; $this->rewriter->consume(); - $this->prepare_update_nested_query(); + $this->preface_WHERE_clause_with_a_subquery(); $this->rewriter->add( new WP_SQLite_Token('WHERE', WP_SQLite_Token::TYPE_KEYWORD, WP_SQLite_Token::FLAG_KEYWORD_RESERVED) ); @@ -1634,7 +1634,19 @@ private function execute_update() { $this->set_result_from_affected_rows(); } - private function prepare_update_nested_query() { + /** + * Injects `rowid IN (SELECT rowid FROM table WHERE ...` into the WHERE clause at the current + * position in the query. + * + * This is necessary to emulate the behavior of MySQL's UPDATE LIMIT and DELETE LIMIT statement + * as SQLite does not support LIMIT in UPDATE and DELETE statements. + * + * The WHERE clause is wrapped in a subquery that selects the rowid of the rows that match the original + * WHERE clause. + * + * @return void + */ + private function preface_WHERE_clause_with_a_subquery() { $this->rewriter->add_many( array( new WP_SQLite_Token( ' ', WP_SQLite_Token::TYPE_WHITESPACE ), @@ -1654,6 +1666,7 @@ private function prepare_update_nested_query() { ) ); } + /** * Executes a INSERT or REPLACE statement. */