Skip to content

Commit dd5eb3b

Browse files
committed
Implement select item translation to provide correct result column names
1 parent d14d596 commit dd5eb3b

4 files changed

Lines changed: 102 additions & 24 deletions

File tree

tests/WP_SQLite_Driver_Metadata_Tests.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,10 @@ public function testCountTables() {
2323
$this->assertQuery( 'CREATE TABLE t2 (id INT)' );
2424

2525
$result = $this->assertQuery( "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'wp'" );
26-
$this->assertEquals( array( (object) array( 'COUNT ( * )' => '2' ) ), $result );
26+
$this->assertEquals( array( (object) array( 'COUNT(*)' => '2' ) ), $result );
2727

2828
$result = $this->assertQuery( "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'other'" );
29-
$this->assertEquals( array( (object) array( 'COUNT ( * )' => '0' ) ), $result );
30-
31-
// @TODO: The result key should be "COUNT(*)" instead of "COUNT ( * )".
32-
// The spacing was probably inserted by the translator.
29+
$this->assertEquals( array( (object) array( 'COUNT(*)' => '0' ) ), $result );
3330
}
3431

3532
public function testInformationSchemaTables() {

tests/WP_SQLite_Driver_Tests.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6020,4 +6020,33 @@ public function testDatabaseNameMismatchWithExistingInformationSchemaTableData()
60206020
$this->expectExceptionMessage( "Incorrect database name. The database was created with name 'db-one', but 'db-two' is used in the current session." );
60216021
new WP_SQLite_Driver( $connection, 'db-two' );
60226022
}
6023+
6024+
public function testSelectColumnNames(): void {
6025+
$this->assertQuery( 'CREATE TABLE t (id INT, name VARCHAR(255))' );
6026+
$this->assertQuery( 'INSERT INTO t (id, name) VALUES (1, "John"), (2, "Jane")' );
6027+
6028+
// Columns (no explicit alias).
6029+
$result = $this->assertQuery( 'SELECT id, name FROM t' );
6030+
$this->assertSame( array( 'id', 'name' ), array_keys( (array) $result[0] ) );
6031+
6032+
// Columns with an explicit alias.
6033+
$result = $this->assertQuery( 'SELECT id AS alias_id, name AS alias_name FROM t' );
6034+
$this->assertSame( array( 'alias_id', 'alias_name' ), array_keys( (array) $result[0] ) );
6035+
6036+
// Expressions (no explicit alias).
6037+
$result = $this->assertQuery( 'SELECT id + 1, (2 + 3) FROM t' );
6038+
$this->assertSame( array( 'id + 1', '(2 + 3)' ), array_keys( (array) $result[0] ) );
6039+
6040+
// Expressions with an explicit alias.
6041+
$result = $this->assertQuery( 'SELECT id + 1 AS alias_id, (2 + 3) AS alias_numbers FROM t' );
6042+
$this->assertSame( array( 'alias_id', 'alias_numbers' ), array_keys( (array) $result[0] ) );
6043+
6044+
// Function calls (no explicit alias).
6045+
$result = $this->assertQuery( "SELECT CONCAT('a', 'b')" );
6046+
$this->assertSame( array( "CONCAT('a', 'b')" ), array_keys( (array) $result[0] ) );
6047+
6048+
// Function calls with an explicit alias.
6049+
$result = $this->assertQuery( "SELECT CONCAT('a', 'b') AS alias_concat" );
6050+
$this->assertSame( array( 'alias_concat' ), array_keys( (array) $result[0] ) );
6051+
}
60236052
}

tests/WP_SQLite_Driver_Translation_Tests.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1261,7 +1261,7 @@ public function testSystemVariables(): void {
12611261

12621262
public function testConcatFunction(): void {
12631263
$this->assertQuery(
1264-
"SELECT ('a' || 'b' || 'c')",
1264+
"SELECT ('a' || 'b' || 'c') AS `CONCAT(\"a\", \"b\", \"c\")`",
12651265
'SELECT CONCAT("a", "b", "c")'
12661266
);
12671267
}

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

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2365,6 +2365,8 @@ private function translate( $node ): ?string {
23652365
throw $this->new_not_supported_exception(
23662366
sprintf( 'data type: %s', $child->get_value() )
23672367
);
2368+
case 'selectItem':
2369+
return $this->translate_select_item( $node );
23682370
case 'fromClause':
23692371
// FROM DUAL is MySQL-specific syntax that means "FROM no tables"
23702372
// and it is equivalent to omitting the FROM clause entirely.
@@ -2411,19 +2413,7 @@ private function translate( $node ): ?string {
24112413
// @TODO: Emulate more system variables, or use reasonable defaults.
24122414
// See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variable-reference.html
24132415
// See: https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html
2414-
2415-
// TODO: Original name should come from the original MySQL input,
2416-
// exactly as it was written by the user, and not translated.
2417-
2418-
// TODO: The '% AS %' syntax is compatible with SELECT lists only.
2419-
// We need to translate it differently when used as a value.
2420-
return sprintf(
2421-
'%s AS %s',
2422-
$value,
2423-
$this->quote_sqlite_identifier(
2424-
'@@' . ( $type_token ? "{$type_token->get_value()}." : '' ) . $original_name
2425-
)
2426-
);
2416+
return $value;
24272417
case 'castType':
24282418
// Translate "CAST(... AS BINARY)" to "CAST(... AS BLOB)".
24292419
if ( $node->has_child_token( WP_MySQL_Lexer::BINARY_SYMBOL ) ) {
@@ -2880,15 +2870,11 @@ private function translate_function_call( WP_Parser_Node $node ): string {
28802870
case 'CONCAT':
28812871
return '(' . implode( ' || ', $args ) . ')';
28822872
case 'FOUND_ROWS':
2883-
// @TODO: The following implementation with an alias assumes
2884-
// that the function is used in the SELECT field list.
2885-
// For compatibility with more complex use cases, it may
2886-
// be better to register it as a custom SQLite function.
28872873
$found_rows = $this->last_sql_calc_found_rows;
28882874
if ( null === $found_rows && is_array( $this->last_result ) ) {
28892875
$found_rows = count( $this->last_result );
28902876
}
2891-
return sprintf( "(SELECT %d) AS 'FOUND_ROWS()'", $found_rows );
2877+
return $found_rows;
28922878
default:
28932879
return $this->translate_sequence( $node->get_children() );
28942880
}
@@ -2959,6 +2945,72 @@ private function translate_datetime_literal( string $value ): string {
29592945
return $value;
29602946
}
29612947

2948+
/**
2949+
* Translate a select item to SQLite.
2950+
*
2951+
* In some cases, an explicit alias will be added to the select item, so that
2952+
* the returned column name is always the same as it would be in MySQL.
2953+
*
2954+
* @param WP_Parser_Node $node The "selectItem" AST node.
2955+
* @return string The translated value.
2956+
*/
2957+
public function translate_select_item( WP_Parser_Node $node ): string {
2958+
/*
2959+
* First, let's translate the select item subtree.
2960+
*
2961+
* [GRAMMAR]
2962+
* selectItem: tableWild | (expr selectAlias?)
2963+
*/
2964+
$item = $this->translate_sequence( $node->get_children() );
2965+
2966+
// A table wildcard (e.g., "SELECT *, t.*, ...") never has an alias.
2967+
if ( $node->has_child_node( 'tableWild' ) ) {
2968+
return $item;
2969+
}
2970+
2971+
// When an explicit alias is provided, we can use it as is.
2972+
$alias = $node->get_first_child_node( 'selectAlias' );
2973+
if ( $alias ) {
2974+
return $item;
2975+
}
2976+
2977+
/*
2978+
* When the select item contains only a column definition, we need to use
2979+
* it without change, so that the returned column name reflects the real
2980+
* column name in all cases, including when using a fully qualified name.
2981+
*
2982+
* For example, for "SELECT t.id", the column name in the result set will
2983+
* only be "id", not "t.id", as it may appear based on the original query.
2984+
*
2985+
* In this case, SQLite uses the same logic as MySQL, so using the value
2986+
* as is without adding an explicit alias will produce the correct result.
2987+
*/
2988+
$column_ref = $node->get_first_descendant_node( 'columnRef' );
2989+
$is_column_ref = $column_ref && $item === $this->translate( $column_ref );
2990+
if ( $is_column_ref ) {
2991+
return $item;
2992+
}
2993+
2994+
/*
2995+
* When the select item has no explicit alias, we need to ensure that the
2996+
* returned column name is equivalent to what MySQL infers from the input.
2997+
*
2998+
* For example, if we translate "CONCAT('a', 'b')" to "('a' || 'b')", we
2999+
* need to use the original "CONCAT('a', 'b')" string as the column name.
3000+
* To achieve this, the select item will be translated as follows:
3001+
*
3002+
* SELECT CONCAT('a', 'b') -> SELECT ('a' || 'b') AS `CONCAT('a', 'b')`
3003+
*/
3004+
$raw_alias = substr( $this->last_mysql_query, $node->get_start(), $node->get_length() );
3005+
$alias = $this->quote_sqlite_identifier( $raw_alias );
3006+
if ( $alias === $item || $raw_alias === $item ) {
3007+
// For the simple case of selecting only columns ("SELECT id FROM t"),
3008+
// let's avoid unnecessary aliases ("SELECT `id` AS `id` FROM t").
3009+
return $item;
3010+
}
3011+
return sprintf( '%s AS %s', $item, $alias );
3012+
}
3013+
29623014
/**
29633015
* Recreate an existing table using data in the information schema.
29643016
*

0 commit comments

Comments
 (0)