Skip to content

Commit 60fe936

Browse files
committed
Translate MySQL CONVERT() expressions to SQLite
SQLite doesn't support the CONVERT() function. Translate its two forms: - CONVERT(expr, type) is equivalent to CAST(expr AS type). - CONVERT(expr USING charset) is a character set conversion that is a no-op in SQLite, as all text is stored as UTF-8. Trailing modifiers like COLLATE are preserved in the translation. Fixes #344
1 parent ee854d2 commit 60fe936

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

packages/mysql-on-sqlite/src/sqlite/class-wp-pdo-mysql-on-sqlite.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4221,9 +4221,65 @@ private function translate_simple_expr( WP_Parser_Node $node ): string {
42214221
);
42224222
}
42234223

4224+
// Translate MySQL CONVERT() to SQLite equivalents.
4225+
if ( null !== $token && WP_MySQL_Lexer::CONVERT_SYMBOL === $token->id ) {
4226+
return $this->translate_convert_expr( $node );
4227+
}
4228+
42244229
return $this->translate_sequence( $node->get_children() );
42254230
}
42264231

4232+
/**
4233+
* Translate a MySQL CONVERT() expression to SQLite.
4234+
*
4235+
* MySQL supports two forms of CONVERT():
4236+
* - CONVERT(expr, type) — equivalent to CAST(expr AS type).
4237+
* - CONVERT(expr USING charset) — converts the character set.
4238+
*
4239+
* SQLite doesn't support CONVERT(). The type form is translated to CAST(),
4240+
* and the charset form is reduced to the expression, as SQLite stores all
4241+
* text as UTF-8 and charset conversions are not needed.
4242+
*
4243+
* @param WP_Parser_Node $node The "simpleExpr" AST node.
4244+
* @return string The translated value.
4245+
* @throws WP_SQLite_Driver_Exception When the translation fails.
4246+
*/
4247+
private function translate_convert_expr( WP_Parser_Node $node ): string {
4248+
$expr = $this->translate( $node->get_first_child_node( 'expr' ) );
4249+
$cast_type = $node->get_first_child_node( 'castType' );
4250+
4251+
if ( null !== $cast_type ) {
4252+
// CONVERT(expr, type) → CAST(expr AS type)
4253+
$parts = array(
4254+
sprintf( 'CAST ( %s AS %s )', $expr, $this->translate( $cast_type ) ),
4255+
);
4256+
} else {
4257+
// CONVERT(expr USING charset) → expr
4258+
$parts = array( $expr );
4259+
}
4260+
4261+
// Translate any trailing modifiers after the closing parenthesis,
4262+
// as the simpleExpr grammar can include COLLATE or CONCAT_PIPES.
4263+
$past_close_paren = false;
4264+
foreach ( $node->get_children() as $child ) {
4265+
if ( ! $past_close_paren ) {
4266+
if (
4267+
$child instanceof WP_Parser_Token
4268+
&& WP_MySQL_Lexer::CLOSE_PAR_SYMBOL === $child->id
4269+
) {
4270+
$past_close_paren = true;
4271+
}
4272+
continue;
4273+
}
4274+
$translated = $this->translate( $child );
4275+
if ( null !== $translated ) {
4276+
$parts[] = $translated;
4277+
}
4278+
}
4279+
4280+
return implode( ' ', $parts );
4281+
}
4282+
42274283
/**
42284284
* Translate a MySQL LIKE expression to SQLite.
42294285
*

packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Tests.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9986,6 +9986,73 @@ public function testCastExpression(): void {
99869986
);
99879987
}
99889988

9989+
public function testConvertExpression(): void {
9990+
// CONVERT(expr, type) should behave like CAST(expr AS type).
9991+
$result = $this->assertQuery(
9992+
"SELECT
9993+
CONVERT('abc', BINARY) AS expr_1,
9994+
CONVERT('abc', CHAR) AS expr_2,
9995+
CONVERT('-10', SIGNED) AS expr_3,
9996+
CONVERT('-10', UNSIGNED) AS expr_4,
9997+
CONVERT('123.456', DECIMAL) AS expr_5,
9998+
CONVERT('2025-10-05', DATE) AS expr_6
9999+
"
10000+
);
10001+
10002+
$this->assertEquals(
10003+
array(
10004+
(object) array(
10005+
'expr_1' => 'abc',
10006+
'expr_2' => 'abc',
10007+
'expr_3' => '-10',
10008+
'expr_4' => '-10',
10009+
'expr_5' => '123.456',
10010+
'expr_6' => '2025-10-05',
10011+
),
10012+
),
10013+
$result
10014+
);
10015+
}
10016+
10017+
public function testConvertUsingExpression(): void {
10018+
// CONVERT(expr USING charset) converts character set.
10019+
// In SQLite, all text is UTF-8 — the conversion is a no-op.
10020+
$result = $this->assertQuery(
10021+
"SELECT
10022+
CONVERT('Customer' USING utf8mb4) AS expr_1,
10023+
CONVERT('test' USING utf8) AS expr_2,
10024+
CONVERT('data' USING latin1) AS expr_3
10025+
"
10026+
);
10027+
10028+
$this->assertEquals(
10029+
array(
10030+
(object) array(
10031+
'expr_1' => 'Customer',
10032+
'expr_2' => 'test',
10033+
'expr_3' => 'data',
10034+
),
10035+
),
10036+
$result
10037+
);
10038+
}
10039+
10040+
/**
10041+
* @see https://github.com/WordPress/sqlite-database-integration/issues/344
10042+
*/
10043+
public function testConvertUsingWithCollate(): void {
10044+
$result = $this->assertQuery(
10045+
"SELECT CONVERT('Customer' USING utf8mb4) COLLATE utf8mb4_bin AS val"
10046+
);
10047+
10048+
$this->assertEquals(
10049+
array(
10050+
(object) array( 'val' => 'Customer' ),
10051+
),
10052+
$result
10053+
);
10054+
}
10055+
998910056
public function testInsertWithoutInto(): void {
999010057
$this->assertQuery( 'CREATE TABLE t (id INT PRIMARY KEY, name VARCHAR(255))' );
999110058

packages/mysql-on-sqlite/tests/WP_SQLite_Driver_Translation_Tests.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,43 @@ public function testSelect(): void {
101101
);
102102
}
103103

104+
public function testConvert(): void {
105+
// CONVERT(expr, type) → CAST(expr AS type)
106+
$this->assertQuery(
107+
"SELECT CAST ( 'abc' AS BLOB ) AS `CONVERT('abc', BINARY)`",
108+
"SELECT CONVERT('abc', BINARY)"
109+
);
110+
111+
$this->assertQuery(
112+
"SELECT CAST ( 'abc' AS TEXT ) AS `CONVERT('abc', CHAR)`",
113+
"SELECT CONVERT('abc', CHAR)"
114+
);
115+
116+
$this->assertQuery(
117+
"SELECT CAST ( '-10' AS INTEGER ) AS `CONVERT('-10', SIGNED)`",
118+
"SELECT CONVERT('-10', SIGNED)"
119+
);
120+
121+
// CONVERT(expr USING charset) → expr
122+
// When the result is a simple string literal, the auto-alias is the
123+
// unquoted string value (matching MySQL's column name behavior).
124+
$this->assertQuery(
125+
"SELECT 'Customer' AS `Customer`",
126+
"SELECT CONVERT('Customer' USING utf8mb4)"
127+
);
128+
129+
$this->assertQuery(
130+
"SELECT 'test' AS `test`",
131+
"SELECT CONVERT('test' USING utf8)"
132+
);
133+
134+
// CONVERT(expr USING charset) COLLATE collation → expr COLLATE collation
135+
$this->assertQuery(
136+
"SELECT 'Customer' COLLATE `utf8mb4_bin` AS `CONVERT('Customer' USING utf8mb4) COLLATE utf8mb4_bin`",
137+
"SELECT CONVERT('Customer' USING utf8mb4) COLLATE utf8mb4_bin"
138+
);
139+
}
140+
104141
public function testInsert(): void {
105142
$this->driver->query( 'CREATE TABLE t (c INT, c1 INT, c2 INT)' );
106143
$this->driver->query( 'CREATE TABLE t1 (c1 INT, c2 INT)' );

0 commit comments

Comments
 (0)