Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/wp-tests-phpunit-run.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,6 @@ const expectedFailures = [
'Tests_DB_Charset::test_strip_invalid_text with data set #32',
'Tests_DB_Charset::test_strip_invalid_text with data set #33',
'Tests_DB_Charset::test_strip_invalid_text with data set #34',
'Tests_DB_Charset::test_strip_invalid_text with data set #35',
'Tests_DB_Charset::test_strip_invalid_text with data set #36',
'Tests_DB_Charset::test_strip_invalid_text with data set #37',
'Tests_DB_Charset::test_strip_invalid_text with data set #39',
'Tests_DB_Charset::test_strip_invalid_text with data set #40',
'Tests_DB_Charset::test_strip_invalid_text with data set #41',
Expand Down
6 changes: 3 additions & 3 deletions grammar-tools/MySQLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -2961,13 +2961,13 @@ bitExpr: simpleExpr %bitExpr_rr*;

/*
* @CHANGED:
* Factored left recursion.
* Factored left recursion and introduced "simpleExprBody" for easier processing.
*/
simpleExpr: %simpleExpr_collate (CONCAT_PIPES_SYMBOL %simpleExpr_collate)*;

%simpleExpr_collate: %simpleExpr_factored (COLLATE_SYMBOL textOrIdentifier)?;
%simpleExpr_collate: simpleExprBody (COLLATE_SYMBOL textOrIdentifier)?;

%simpleExpr_factored:
simpleExprBody:
literal # simpleExprLiteral
| sumExpr # simpleExprSum
| variable (equal expr)? # simpleExprVariable
Expand Down
2 changes: 1 addition & 1 deletion packages/mysql-on-sqlite/src/mysql/mysql-grammar.php

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3691,8 +3691,8 @@ private function translate( $node ): ?string {
return null;
}
return $this->translate_sequence( $node->get_children() );
case 'simpleExpr':
return $this->translate_simple_expr( $node );
case 'simpleExprBody':
return $this->translate_simple_expr_body( $node );
case 'predicateOperations':
$token = $node->get_first_child_token();
if ( WP_MySQL_Lexer::LIKE_SYMBOL === $token->id ) {
Expand Down Expand Up @@ -4204,13 +4204,13 @@ private function translate_query_specification( WP_Parser_Node $node ): string {
}

/**
* Translate a MySQL simple expression to SQLite.
* Translate a MySQL simple expression body to SQLite.
*
* @param WP_Parser_Node $node The "simpleExpr" AST node.
* @param WP_Parser_Node $node The "simpleExprBody" AST node.
* @return string The translated value.
* @throws WP_SQLite_Driver_Exception When the translation fails.
*/
private function translate_simple_expr( WP_Parser_Node $node ): string {
private function translate_simple_expr_body( WP_Parser_Node $node ): string {
$token = $node->get_first_child_token();

// Translate "VALUES(col)" to "excluded.col" in ON DUPLICATE KEY UPDATE.
Expand All @@ -4221,6 +4221,27 @@ private function translate_simple_expr( WP_Parser_Node $node ): string {
);
}

/**
* Translate MySQL CONVERT() expression.
*
* MySQL supports two forms of CONVERT():
* 1. CONVERT(expr, type): Equivalent to CAST(expr AS type).
* 2. CONVERT(expr USING charset): Converts the character set.
*/
if ( null !== $token && WP_MySQL_Lexer::CONVERT_SYMBOL === $token->id ) {
$expr = $this->translate( $node->get_first_child_node( 'expr' ) );
$cast_type = $node->get_first_child_node( 'castType' );

if ( null !== $cast_type ) {
// CONVERT(expr, type): Translate to cast expression.
return sprintf( 'CAST(%s AS %s)', $expr, $this->translate( $cast_type ) );
} else {
// CONVERT(expr USING charset): Keep "expr" as is (no SQLite support).
// TODO: Consider rejecting UTF-8-incompatible charasets.
return $expr;
}
}

return $this->translate_sequence( $node->get_children() );
}

Expand Down Expand Up @@ -5350,12 +5371,12 @@ private function store_last_column_meta_from_statement( PDOStatement $stmt ): vo
private function unnest_parenthesized_expression( WP_Parser_Node $node ): WP_Parser_Node {
$children = $node->get_children();

// Descend the "expr -> boolPri -> predicate -> bitExpr -> simpleExpr" tree,
// when on each level we have only a single child node (expression nesting).
// Descend the "expr -> boolPri -> predicate -> bitExpr -> simpleExpr" -> "simpleExprBody"
// tree, when on each level we have only a single child node (expression nesting).
if (
1 === count( $children )
&& $children[0] instanceof WP_Parser_Node
&& in_array( $children[0]->rule_name, array( 'expr', 'boolPri', 'predicate', 'bitExpr', 'simpleExpr' ), true )
&& in_array( $children[0]->rule_name, array( 'expr', 'boolPri', 'predicate', 'bitExpr', 'simpleExpr', 'simpleExprBody' ), true )
) {
$unnested = $this->unnest_parenthesized_expression( $children[0] );
return $unnested === $children[0] ? $node : $unnested;
Expand Down
83 changes: 83 additions & 0 deletions packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -9986,6 +9986,89 @@ public function testCastExpression(): void {
);
}

public function testConvertExpression(): void {
// CONVERT(expr, type) should behave like CAST(expr AS type).
$result = $this->assertQuery(
"SELECT
CONVERT('abc', BINARY) AS expr_1,
CONVERT('abc', CHAR) AS expr_2,
CONVERT('-10', SIGNED) AS expr_3,
CONVERT('-10', UNSIGNED) AS expr_4,
CONVERT('123.456', DECIMAL) AS expr_5,
CONVERT('2025-10-05', DATE) AS expr_6
"
);

$this->assertEquals(
array(
(object) array(
'expr_1' => 'abc',
'expr_2' => 'abc',
'expr_3' => '-10',
'expr_4' => '-10',
'expr_5' => '123.456',
'expr_6' => '2025-10-05',
),
),
$result
);
}

public function testConvertUsingExpression(): void {
// CONVERT(expr USING charset) converts character set.
// In SQLite, all text is UTF-8 — the conversion is a no-op.
$result = $this->assertQuery(
"SELECT
CONVERT('Customer' USING utf8mb4) AS expr_1,
CONVERT('test' USING utf8) AS expr_2,
CONVERT('data' USING latin1) AS expr_3
"
);

$this->assertEquals(
array(
(object) array(
'expr_1' => 'Customer',
'expr_2' => 'test',
'expr_3' => 'data',
),
),
$result
);
}

public function testConvertUsingWithCollate(): void {
$result = $this->assertQuery(
"SELECT CONVERT('Customer' USING utf8mb4) COLLATE utf8mb4_bin AS val"
);

$this->assertEquals(
array(
(object) array( 'val' => 'Customer' ),
),
$result
);
}

public function testConvertWithColumnReferences(): void {
$this->assertQuery( 'CREATE TABLE t (val VARCHAR(255), num VARCHAR(255))' );
$this->assertQuery( "INSERT INTO t (val, num) VALUES ('hello', '-42')" );

$result = $this->assertQuery(
'SELECT CONVERT(val, BINARY) AS v1, CONVERT(val USING utf8mb4) AS v2
FROM t WHERE CONVERT(num, SIGNED) < 0 ORDER BY CONVERT(val USING utf8mb4)'
);
$this->assertEquals(
array(
(object) array(
'v1' => 'hello',
'v2' => 'hello',
),
),
$result
);
}

public function testInsertWithoutInto(): void {
$this->assertQuery( 'CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))' );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,41 @@ public function testSelect(): void {
);
}

public function testConvert(): void {
// CONVERT(expr, type) → CAST(expr AS type)
$this->assertQuery(
"SELECT CAST('abc' AS BLOB) AS `CONVERT('abc', BINARY)`",
"SELECT CONVERT('abc', BINARY)"
);

$this->assertQuery(
"SELECT CAST('abc' AS TEXT) AS `CONVERT('abc', CHAR)`",
"SELECT CONVERT('abc', CHAR)"
);

$this->assertQuery(
"SELECT CAST('-10' AS INTEGER) AS `CONVERT('-10', SIGNED)`",
"SELECT CONVERT('-10', SIGNED)"
);

// CONVERT(expr USING charset) → expr
$this->assertQuery(
"SELECT 'Customer' AS `Customer`",
"SELECT CONVERT('Customer' USING utf8mb4)"
);

$this->assertQuery(
"SELECT 'test' AS `test`",
"SELECT CONVERT('test' USING utf8)"
);

// CONVERT(expr USING charset) COLLATE collation → expr COLLATE collation
$this->assertQuery(
"SELECT 'Customer' COLLATE `utf8mb4_bin` AS `CONVERT('Customer' USING utf8mb4) COLLATE utf8mb4_bin`",
"SELECT CONVERT('Customer' USING utf8mb4) COLLATE utf8mb4_bin"
);
}

public function testInsert(): void {
$this->driver->query( 'CREATE TABLE t (c INT, c1 INT, c2 INT)' );
$this->driver->query( 'CREATE TABLE t1 (c1 INT, c2 INT)' );
Expand Down
Loading