Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support UPDATE LIMIT #93

Merged
merged 13 commits into from
Apr 16, 2024
40 changes: 40 additions & 0 deletions tests/WP_SQLite_Translator_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,46 @@ 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');"
kozer marked this conversation as resolved.
Show resolved Hide resolved
);
$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;"
);
$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"
);
$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() {
$this->assertQuery(
// Use a confusing alias to make sure it replaces only the correct token
Expand Down
77 changes: 75 additions & 2 deletions wp-includes/sqlite/class-wp-sqlite-translator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1547,17 +1547,64 @@ 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
*/
kozer marked this conversation as resolved.
Show resolved Hide resolved
private function execute_update() {
$this->rewriter->consume(); // Update.

$params = array();
$limit = $this->rewriter->peek(
adamziel marked this conversation as resolved.
Show resolved Hide resolved
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();
if ( ! $token ) {
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 ) ) {
kozer marked this conversation as resolved.
Show resolved Hide resolved
$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.
*/
kozer marked this conversation as resolved.
Show resolved Hide resolved
if ( $token->value === ';' && $token->type === WP_SQLite_Token::TYPE_DELIMITER && ( $limit || $order_by ) ) {
adamziel marked this conversation as resolved.
Show resolved Hide resolved
$this->rewriter->skip();
}

// Record the table name.
if (
! $this->table_name &&
Expand All @@ -1580,13 +1627,39 @@ 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();
$this->execute_sqlite_query( $updated_query, $params );
$this->set_result_from_affected_rows();
}

private function prepare_update_for_limit_or_order() {
$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.
*/
Expand Down