Skip to content

Commit 3d05c34

Browse files
committed
Improve readability and docs, add tests
1 parent 4268dea commit 3d05c34

2 files changed

Lines changed: 76 additions & 30 deletions

File tree

tests/WP_SQLite_Driver_Translation_Tests.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,18 @@ public function testSelectOrderByAmbiguousColumnResolution(): void {
13801380
'SELECT t1.name AS t1_name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name DESC'
13811381
);
13821382

1383+
// When the SELECT item list is ambiguous, the ORDER BY column is not disambiguated (like in MySQL).
1384+
$this->assertQuery(
1385+
'SELECT `t1`.`name` , `t2`.`name` FROM `t1` JOIN `t2` ON `t2`.`id` = `t1`.`id` ORDER BY `name` DESC',
1386+
'SELECT t1.name, t2.name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name DESC'
1387+
);
1388+
1389+
// When the SELECT item list is ambiguous with an alias, the ORDER BY column is not disambiguated (like in MySQL).
1390+
$this->assertQuery(
1391+
"SELECT `t1`.`name` , 'test' AS `name` FROM `t1` JOIN `t2` ON `t2`.`id` = `t1`.`id` ORDER BY `name` DESC",
1392+
"SELECT t1.name, 'test' AS `name` FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY name DESC"
1393+
);
1394+
13831395
// When the ORDER BY item uses an alias, there is no ambiguity.
13841396
$this->assertQuery(
13851397
'SELECT `t1`.`name` AS `t1_name` FROM `t1` JOIN `t2` ON `t2`.`t1_id` = `t1`.`id` ORDER BY `t1_name` DESC',

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

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2946,56 +2946,90 @@ private function translate_query_expression( WP_Parser_Node $node ): string {
29462946
$order_list = $order_clause->get_first_child_node( 'orderList' );
29472947
$select_item_list = $node->get_first_descendant_node( 'selectItemList' );
29482948

2949-
// Get a list of column references used in the SELECT item list.
2950-
$select_column_refs = array();
2949+
// Create a map of SELECT item column names to their qualified values.
2950+
$disambiguation_map = array();
29512951
foreach ( $select_item_list->get_child_nodes() as $select_item ) {
2952-
// When the SELECT item uses an alias, the column is not disambiguated.
2953-
if ( $select_item->has_child_node( 'selectAlias' ) ) {
2954-
continue;
2955-
}
2952+
/*
2953+
* [GRAMMAR]
2954+
* selectItem: tableWild | (expr selectAlias?)
2955+
*/
29562956

2957+
// Skip when a "tableWild" node is used (no "expr" node).
29572958
$select_item_expr = $select_item->get_first_child_node( 'expr' );
29582959
if ( ! $select_item_expr ) {
29592960
continue;
29602961
}
29612962

2962-
$select_item_expr = $this->unnest_parenthesized_expression( $select_item_expr );
2963-
$select_column_ref = $select_item->get_first_descendant_node( 'columnRef' );
2964-
if (
2965-
$select_column_ref
2966-
&& $this->translate( $select_item_expr ) === $this->translate( $select_column_ref )
2967-
) {
2968-
$select_column_refs[] = $select_column_ref;
2963+
// A SELECT item alias always needs to be preserved as-is.
2964+
$alias = $select_item->get_first_child_node( 'selectAlias' );
2965+
if ( $alias ) {
2966+
$alias_value = $this->translate( $alias->get_first_child_node() );
2967+
$disambiguation_map[ $alias_value ] = array( $alias_value );
2968+
continue;
2969+
}
2970+
2971+
// Skip when there is no column listed (no "columnRef" node).
2972+
$select_column_ref = $select_item_expr->get_first_descendant_node( 'columnRef' );
2973+
if ( ! $select_column_ref ) {
2974+
continue;
2975+
}
2976+
2977+
// Skip when the column reference is not qualified (no "dotIdentifier" node).
2978+
$dot_identifiers = $select_column_ref->get_descendant_nodes( 'dotIdentifier' );
2979+
if ( 0 === count( $dot_identifiers ) ) {
2980+
continue;
2981+
}
2982+
2983+
// Support also parenthesized column references (e.g. "(t.id)").
2984+
$select_item_expr = $this->unnest_parenthesized_expression( $select_item_expr );
2985+
2986+
// Consider only simple and parenthesized column references.
2987+
$expr_value = $this->translate( $select_item_expr );
2988+
$column_value = $this->translate( $select_column_ref );
2989+
if ( $expr_value !== $column_value ) {
2990+
continue;
29692991
}
2992+
2993+
// The column name is the last "dotIdentifier" node.
2994+
$key = $this->translate( end( $dot_identifiers )->get_first_child_node() );
2995+
2996+
$disambiguation_map[ $key ] = $disambiguation_map[ $key ] ?? array();
2997+
$disambiguation_map[ $key ][] = $column_value;
29702998
}
29712999

29723000
// For each ORDER BY item, try to find a corresponding SELECT item.
29733001
foreach ( $order_list->get_child_nodes() as $order_item ) {
3002+
/*
3003+
* [GRAMMAR]
3004+
* orderExpression: expr direction?
3005+
*/
29743006
$order_expr = $order_item->get_first_child_node( 'expr' );
29753007
$order_column_ref = $order_expr->get_first_descendant_node( 'columnRef' );
29763008

2977-
$select_item_matches = array();
3009+
// Skip when there is no column in the ORDER BY item (no "columnRef" node),
3010+
// or when the item is already qualified (has a "dotIdentifier" node).
29783011
if (
2979-
$order_column_ref
2980-
&& $this->translate( $order_column_ref ) === $this->translate( $order_expr )
2981-
&& null === $order_column_ref->get_first_descendant_node( 'dotIdentifier' )
3012+
! $order_column_ref
3013+
|| null !== $order_column_ref->get_first_descendant_node( 'dotIdentifier' )
29823014
) {
2983-
// Look for select items that match the column reference.
2984-
foreach ( $select_column_refs as $select_column_ref ) {
2985-
$dot_identifiers = $select_column_ref->get_descendant_nodes( 'dotIdentifier' );
2986-
if ( count( $dot_identifiers ) === 0 ) {
2987-
continue;
2988-
}
3015+
$disambiguated_order_list[] = $this->translate( $order_item );
3016+
continue;
3017+
}
29893018

2990-
$last_dot_identifier = end( $dot_identifiers );
2991-
$select_column_name = $this->translate( $last_dot_identifier->get_first_child_node() );
2992-
$order_column_name = $this->translate( $order_column_ref );
2993-
if ( $select_column_name === $order_column_name ) {
2994-
$select_item_matches[] = $this->translate( $select_column_ref );
2995-
}
2996-
}
3019+
// Consider only simple and parenthesized order column references.
3020+
$order_expr_value = $this->translate( $order_expr );
3021+
$order_column_value = $this->translate( $order_column_ref );
3022+
if ( $order_expr_value !== $order_column_value ) {
3023+
$disambiguated_order_list[] = $this->translate( $order_item );
3024+
continue;
29973025
}
29983026

3027+
// Look for select items that match the column reference.
3028+
$order_column_name = $this->translate( $order_column_ref );
3029+
$select_item_matches = $disambiguation_map[ $order_column_name ] ?? array();
3030+
3031+
// When we find exactly one SELECT item, we can disambiguate the
3032+
// reference. Otherwise, fall back to the original ORDER BY item.
29993033
if ( 1 === count( $select_item_matches ) ) {
30003034
$direction = $order_item->get_first_child_node( 'direction' );
30013035
$translated_order_item = sprintf(

0 commit comments

Comments
 (0)