Skip to content
This repository was archived by the owner on Jun 2, 2025. It is now read-only.

Commit 822464a

Browse files
committed
Implement ON DUPLICATE KEY UPDATE
1 parent 32c72bf commit 822464a

File tree

4 files changed

+74
-41
lines changed

4 files changed

+74
-41
lines changed

grammar-tools/MySQLParser.g4

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2965,9 +2965,9 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
29652965
literal # simpleExprLiteral
29662966
| sumExpr # simpleExprSum
29672967
| variable (equal expr)? # simpleExprVariable
2968-
| functionCall # simpleExprFunction
2969-
| runtimeFunctionCall # simpleExprRuntimeFunction
2970-
| columnRef jsonOperator? # simpleExprColumnRef
2968+
/*| functionCall # simpleExprFunction*/
2969+
/*| runtimeFunctionCall # simpleExprRuntimeFunction*/
2970+
/*| columnRef jsonOperator? # simpleExprColumnRef*/
29712971
| PARAM_MARKER # simpleExprParamMarker
29722972
| {serverVersion >= 80000}? groupingOperation # simpleExprGroupingOperation
29732973
| {serverVersion >= 80000}? windowFunctionCall # simpleExprWindowingFunction
@@ -2991,6 +2991,10 @@ simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;
29912991
| DEFAULT_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprDefault
29922992
| VALUES_SYMBOL OPEN_PAR_SYMBOL simpleIdentifier CLOSE_PAR_SYMBOL # simpleExprValues
29932993
| INTERVAL_SYMBOL expr interval PLUS_OPERATOR expr # simpleExprInterval
2994+
/* @FIX: Move function calls and ref to the end to avoid conflicts with the above expressions. */
2995+
| functionCall # simpleExprFunction
2996+
| runtimeFunctionCall # simpleExprRuntimeFunction
2997+
| columnRef jsonOperator? # simpleExprColumnRef
29942998
;
29952999

29963000
arrayCast:
@@ -3175,7 +3179,7 @@ runtimeFunctionCall:
31753179
| name = WEEK_SYMBOL OPEN_PAR_SYMBOL expr (COMMA_SYMBOL expr)? CLOSE_PAR_SYMBOL
31763180
| name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr AS_SYMBOL CHAR_SYMBOL CLOSE_PAR_SYMBOL
31773181
| name = WEIGHT_STRING_SYMBOL OPEN_PAR_SYMBOL expr (
3178-
/* @FIX: Move "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */
3182+
/* @FIX: Move "AS BINARY(...)" before "AS CHAR(...)" to solve conflict. */
31793183
AS_SYMBOL BINARY_SYMBOL wsNumCodepoints
31803184
| (AS_SYMBOL CHAR_SYMBOL wsNumCodepoints)? (
31813185
{serverVersion < 80000}? weightStringLevels

tests/WP_SQLite_Driver_Tests.php

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2062,9 +2062,9 @@ public function testOnDuplicateUpdate() {
20622062
);"
20632063
);
20642064

2065-
// $result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
2066-
// $this->assertEquals( '', $this->engine->get_error_message() );
2067-
// $this->assertEquals( 1, $result1 );
2065+
$result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
2066+
$this->assertEquals( '', $this->engine->get_error_message() );
2067+
$this->assertEquals( 1, $result1 );
20682068

20692069
$result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);" );
20702070
$this->assertEquals( 1, $result2 );
@@ -2452,7 +2452,7 @@ public function testInsertOnDuplicateKey() {
24522452
$result1 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('first');" );
24532453
$this->assertEquals( 1, $result1 );
24542454

2455-
$result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY SET name=VALUES(`name`);" );
2455+
$result2 = $this->assertQuery( "INSERT INTO _tmp_table (name) VALUES ('FIRST') ON DUPLICATE KEY UPDATE name=VALUES(`name`);" );
24562456
$this->assertEquals( 1, $result2 );
24572457

24582458
$this->assertQuery( 'SELECT COUNT(*) as cnt FROM _tmp_table' );
@@ -2775,7 +2775,7 @@ public function testInsertOnDuplicateKeyCompositePk() {
27752775
$this->assertEquals( '', $this->engine->get_error_message() );
27762776
$this->assertEquals( 2, $result1 );
27772777

2778-
$result2 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1) ON DUPLICATE KEY SET term_order = VALUES(term_order);' );
2778+
$result2 = $this->assertQuery( 'INSERT INTO wptests_term_relationships VALUES (1,2,2),(1,3,1) ON DUPLICATE KEY UPDATE term_order = VALUES(term_order);' );
27792779
$this->assertEquals( '', $this->engine->get_error_message() );
27802780
$this->assertEquals( 2, $result2 );
27812781

@@ -3267,21 +3267,20 @@ public function testTranslateLikeBinary() {
32673267
$this->assertCount( 2, $result ); // Should match both 'first' and 'FIRST'
32683268
}
32693269

3270-
public function testOnConflictReplace() {
3270+
public function testUniqueConstraints() {
32713271
$this->assertQuery(
32723272
"CREATE TABLE _tmp_table (
32733273
ID INTEGER PRIMARY KEY AUTO_INCREMENT NOT NULL,
32743274
name varchar(20) NOT NULL default 'default-value',
32753275
unique_name varchar(20) NOT NULL default 'unique-default-value',
3276-
inline_unique_name varchar(20) NOT NULL default 'inline-unique-default-value',
3277-
no_default varchar(20) NOT NULL,
3278-
UNIQUE KEY unique_name (unique_name)
3276+
inline_unique_name varchar(30) NOT NULL default 'inline-unique-default-value' UNIQUE,
3277+
UNIQUE KEY unique_name (unique_name),
3278+
UNIQUE KEY compound_name (name, unique_name)
32793279
);"
32803280
);
32813281

3282-
$this->assertQuery(
3283-
"INSERT INTO _tmp_table VALUES (1, null, null, null, '');"
3284-
);
3282+
// Insert a row with default values.
3283+
$this->assertQuery( 'INSERT INTO _tmp_table (ID) VALUES (1)' );
32853284
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 1' );
32863285
$this->assertEquals(
32873286
array(
@@ -3290,45 +3289,47 @@ public function testOnConflictReplace() {
32903289
'name' => 'default-value',
32913290
'unique_name' => 'unique-default-value',
32923291
'inline_unique_name' => 'inline-unique-default-value',
3293-
'no_default' => '',
32943292
),
32953293
),
32963294
$result
32973295
);
32983296

3297+
// Insert another row.
3298+
$this->assertQuery(
3299+
"INSERT INTO _tmp_table VALUES (2, 'ANOTHER-VALUE', 'ANOTHER-UNIQUE-VALUE', 'ANOTHER-INLINE-UNIQUE-VALUE')"
3300+
);
3301+
3302+
// This should fail because of the UNIQUE constraints.
32993303
$this->assertQuery(
3300-
"INSERT INTO _tmp_table VALUES (2, '1', '2', '3', '4');"
3304+
"UPDATE _tmp_table SET unique_name = 'unique-default-value' WHERE ID = 2",
3305+
'UNIQUE constraint failed: _tmp_table.unique_name'
33013306
);
3307+
33023308
$this->assertQuery(
3303-
'UPDATE _tmp_table SET name = null WHERE ID = 2;'
3309+
"UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2",
3310+
'UNIQUE constraint failed: _tmp_table.inline_unique_name'
33043311
);
33053312

3306-
$result = $this->assertQuery( 'SELECT name FROM _tmp_table WHERE ID = 2' );
3313+
// Updating "name" to the same value as the first row should pass.
3314+
$this->assertQuery(
3315+
"UPDATE _tmp_table SET name = 'default-value' WHERE ID = 2"
3316+
);
33073317
$this->assertEquals(
33083318
array(
33093319
(object) array(
3310-
'name' => 'default-value',
3320+
'ID' => '2',
3321+
'name' => 'default-value',
3322+
'unique_name' => 'ANOTHER-UNIQUE-VALUE',
3323+
'inline_unique_name' => 'ANOTHER-INLINE-UNIQUE-VALUE',
33113324
),
33123325
),
3313-
$result
3326+
$this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' )
33143327
);
33153328

3316-
// This should fail because of the UNIQUE constraint
3329+
// Updating also "unique_name" should fail on the compound UNIQUE key.
33173330
$this->assertQuery(
3318-
'UPDATE _tmp_table SET unique_name = NULL WHERE ID = 2;',
3319-
'UNIQUE constraint failed: _tmp_table.unique_name'
3320-
);
3321-
3322-
// Inline unique constraint aren't supported currently, so this should pass
3323-
$this->assertQuery(
3324-
'UPDATE _tmp_table SET inline_unique_name = NULL WHERE ID = 2;',
3325-
''
3326-
);
3327-
3328-
// WPDB allows for NULL values in columns that don't have a default value and a NOT NULL constraint
3329-
$this->assertQuery(
3330-
'UPDATE _tmp_table SET no_default = NULL WHERE ID = 2;',
3331-
''
3331+
"UPDATE _tmp_table SET inline_unique_name = 'inline-unique-default-value' WHERE ID = 2",
3332+
'UNIQUE constraint failed: _tmp_table.inline_unique_name'
33323333
);
33333334

33343335
$result = $this->assertQuery( 'SELECT * FROM _tmp_table WHERE ID = 2' );
@@ -3337,9 +3338,8 @@ public function testOnConflictReplace() {
33373338
(object) array(
33383339
'ID' => '2',
33393340
'name' => 'default-value',
3340-
'unique_name' => '2',
3341-
'inline_unique_name' => 'inline-unique-default-value',
3342-
'no_default' => '',
3341+
'unique_name' => 'ANOTHER-UNIQUE-VALUE',
3342+
'inline_unique_name' => 'ANOTHER-INLINE-UNIQUE-VALUE',
33433343
),
33443344
),
33453345
$result

wp-includes/mysql/mysql-grammar.php

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,14 @@ private function translate( $ast ) {
15131513
return null;
15141514
}
15151515
return $this->translate_sequence( $ast->get_children() );
1516+
case 'insertUpdateList':
1517+
// Translate "ON DUPLICATE KEY UPDATE" to "ON CONFLICT DO UPDATE SET".
1518+
return sprintf(
1519+
'ON CONFLICT DO UPDATE SET %s',
1520+
$this->translate( $ast->get_child_node( 'updateList' ) )
1521+
);
1522+
case 'simpleExpr':
1523+
return $this->translate_simple_expr( $ast );
15161524
case 'predicateOperations':
15171525
$token = $ast->get_child_token();
15181526
if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) {
@@ -1601,6 +1609,20 @@ private function translate_sequence( array $nodes, string $separator = ' ' ): ?s
16011609
return implode( $separator, $parts );
16021610
}
16031611

1612+
private function translate_simple_expr( WP_Parser_Node $node ): string {
1613+
$token = $node->get_child_token();
1614+
1615+
// Translate "VALUES(col)" to "excluded.col" in ON DUPLICATE KEY UPDATE.
1616+
if ( null !== $token && WP_MySQL_Lexer::VALUES_SYMBOL === $token->id ) {
1617+
return sprintf(
1618+
'"excluded".%s',
1619+
$this->translate( $node->get_child_node( 'simpleIdentifier' ) )
1620+
);
1621+
}
1622+
1623+
return $this->translate_sequence( $node->get_children() );
1624+
}
1625+
16041626
private function translate_like( WP_Parser_Node $node ): string {
16051627
$tokens = $node->get_descendant_tokens();
16061628
$is_binary = isset( $tokens[1] ) && WP_MySQL_Lexer::BINARY_SYMBOL === $tokens[1]->id;
@@ -1893,6 +1915,13 @@ private function get_sqlite_create_table_statement( string $table_name, ?string
18931915
}
18941916

18951917
$sql .= ' ' . $type;
1918+
1919+
// In MySQL, text fields are case-insensitive by default.
1920+
// COLLATE NOCASE emulates the same behavior in SQLite.
1921+
// @TODO: Respect the actual column and index collation.
1922+
if ( 'TEXT' === $type ) {
1923+
$sql .= ' COLLATE NOCASE';
1924+
}
18961925
if ( 'NO' === $column['IS_NULLABLE'] ) {
18971926
$sql .= ' NOT NULL';
18981927
}

0 commit comments

Comments
 (0)