Skip to content

Commit 9d2c536

Browse files
committed
Fix disambiguation behavior with CTEs, UNION, EXCEPT, and INTERSECT
1 parent 946320a commit 9d2c536

File tree

2 files changed

+109
-3
lines changed

2 files changed

+109
-3
lines changed

tests/WP_SQLite_Driver_Translation_Tests.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,57 @@ public function testSelectOrderByAmbiguousColumnResolution(): void {
14091409
'SELECT `t1`.`name` AS `t1_name` FROM `t1` JOIN `t2` ON `t2`.`id` = `t1`.`id` ORDER BY `t1_name` DESC',
14101410
'SELECT t1.name AS t1_name FROM t1 JOIN t2 ON t2.id = t1.id ORDER BY `t1_name` DESC'
14111411
);
1412+
1413+
// With a parenthesized query body, the column should be disambiguated.
1414+
$this->assertQuery(
1415+
'( ( ( SELECT `t1`.`name` FROM `t1` ) ) ) ORDER BY `t1`.`name`',
1416+
'(((SELECT t1.name FROM t1))) ORDER BY name'
1417+
);
1418+
1419+
// The root query should not disambiguate from a nested SELECT item.
1420+
$this->assertQuery(
1421+
'SELECT ( SELECT `t1`.`name` FROM `t1` ) AS `t1_name` ORDER BY `name`',
1422+
'SELECT (SELECT t1.name FROM t1) AS t1_name ORDER BY name'
1423+
);
1424+
1425+
// With UNION, the column should not be disambiguated.
1426+
$this->assertQuery(
1427+
'SELECT `t1`.`name` FROM `t1` UNION ALL SELECT `t2`.`name` FROM `t2` ORDER BY `name`',
1428+
'SELECT t1.name FROM t1 UNION ALL SELECT t2.name FROM t2 ORDER BY name'
1429+
);
1430+
1431+
// With EXCEPT, the column should not be disambiguated.
1432+
$this->assertQuery(
1433+
'SELECT `t1`.`name` FROM `t1` EXCEPT SELECT `t2`.`name` FROM `t2` ORDER BY `name`',
1434+
'SELECT t1.name FROM t1 EXCEPT SELECT t2.name FROM t2 ORDER BY name'
1435+
);
1436+
1437+
// With INTERSECT, the column should not be disambiguated.
1438+
$this->assertQuery(
1439+
'SELECT `t1`.`name` FROM `t1` INTERSECT SELECT `t2`.`name` FROM `t2` ORDER BY `name`',
1440+
'SELECT t1.name FROM t1 INTERSECT SELECT t2.name FROM t2 ORDER BY name'
1441+
);
1442+
1443+
// Test a complex query with CTEs.
1444+
$this->assertQuery(
1445+
'WITH'
1446+
. " `cte1` ( `name` ) AS ( SELECT 'a' UNION ALL SELECT 'b' ) ,"
1447+
. ' `cte2` ( `name` ) AS ( SELECT `t2`.`name` FROM `t2` JOIN `t1` ON `t1`.`id` = `t2`.`id` ORDER BY `t2`.`name` )'
1448+
. ' SELECT `t1`.`name` , ( SELECT `name` FROM `cte1` WHERE `id` = 1 ) AS `cte1_name` , ( SELECT `name` FROM `cte2` WHERE `id` = 2 ) AS `cte2_name`'
1449+
. ' FROM `t1`'
1450+
. ' ORDER BY `t1`.`name`',
1451+
"
1452+
WITH
1453+
cte1(name) AS (SELECT 'a' UNION ALL SELECT 'b'),
1454+
cte2(name) AS (SELECT t2.name FROM t2 JOIN t1 ON t1.id = t2.id ORDER BY name)
1455+
SELECT
1456+
t1.name,
1457+
(SELECT name FROM cte1 WHERE id = 1) AS cte1_name,
1458+
(SELECT name FROM cte2 WHERE id = 2) AS cte2_name
1459+
FROM t1
1460+
ORDER BY name
1461+
"
1462+
);
14121463
}
14131464

14141465
public function testSelectGroupByAmbiguousColumnResolution(): void {
@@ -1494,6 +1545,24 @@ public function testSelectGroupByAmbiguousColumnResolution(): void {
14941545
'SELECT `t1`.`name` AS `t1_name` FROM `t1` JOIN `t2` ON `t2`.`id` = `t1`.`id` GROUP BY `t1_name`',
14951546
'SELECT t1.name AS t1_name FROM t1 JOIN t2 ON t2.id = t1.id GROUP BY `t1_name`'
14961547
);
1548+
1549+
// With UNION, the column should be disambiguated in its subquery (differs from ORDER BY).
1550+
$this->assertQuery(
1551+
'SELECT `t1`.`name` FROM `t1` UNION ALL SELECT `t2`.`name` FROM `t2` GROUP BY `t2`.`name`',
1552+
'SELECT t1.name FROM t1 UNION ALL SELECT t2.name FROM t2 GROUP BY name'
1553+
);
1554+
1555+
// With EXCEPT, the column should be disambiguated in its subquery (differs from ORDER BY).
1556+
$this->assertQuery(
1557+
'SELECT `t1`.`name` FROM `t1` EXCEPT SELECT `t2`.`name` FROM `t2` GROUP BY `t2`.`name`',
1558+
'SELECT t1.name FROM t1 EXCEPT SELECT t2.name FROM t2 GROUP BY name'
1559+
);
1560+
1561+
// With INTERSECT, the column should be disambiguated in its subquery (differs from ORDER BY).
1562+
$this->assertQuery(
1563+
'SELECT `t1`.`name` FROM `t1` INTERSECT SELECT `t2`.`name` FROM `t2` GROUP BY `t2`.`name`',
1564+
'SELECT t1.name FROM t1 INTERSECT SELECT t2.name FROM t2 GROUP BY name'
1565+
);
14971566
}
14981567

14991568
public function testSelectHavingAmbiguousColumnResolution(): void {
@@ -1581,6 +1650,24 @@ public function testSelectHavingAmbiguousColumnResolution(): void {
15811650
'SELECT `t1`.`name` FROM `t1` JOIN `t2` ON `t2`.`id` = `t1`.`id` GROUP BY 1 HAVING `name` = 1',
15821651
'SELECT t1.name FROM t1 JOIN t2 ON t2.id = t1.id HAVING name = 1'
15831652
);
1653+
1654+
// With UNION, the column should be disambiguated in its subquery (differs from ORDER BY).
1655+
$this->assertQuery(
1656+
'SELECT `t1`.`name` FROM `t1` UNION ALL SELECT `t2`.`name` FROM `t2` GROUP BY 1 HAVING `t2`.`name`',
1657+
'SELECT t1.name FROM t1 UNION ALL SELECT t2.name FROM t2 HAVING name'
1658+
);
1659+
1660+
// With EXCEPT, the column should be disambiguated in its subquery (differs from ORDER BY).
1661+
$this->assertQuery(
1662+
'SELECT `t1`.`name` FROM `t1` EXCEPT SELECT `t2`.`name` FROM `t2` GROUP BY 1 HAVING `t2`.`name`',
1663+
'SELECT t1.name FROM t1 EXCEPT SELECT t2.name FROM t2 HAVING name'
1664+
);
1665+
1666+
// With INTERSECT, the column should be disambiguated in its subquery (differs from ORDER BY).
1667+
$this->assertQuery(
1668+
'SELECT `t1`.`name` FROM `t1` INTERSECT SELECT `t2`.`name` FROM `t2` GROUP BY 1 HAVING `t2`.`name`',
1669+
'SELECT t1.name FROM t1 INTERSECT SELECT t2.name FROM t2 HAVING name'
1670+
);
15841671
}
15851672

15861673
private function assertQuery( $expected, string $query ): void {

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2961,20 +2961,39 @@ private function translate_query_specification( WP_Parser_Node $node ): string {
29612961
* @throws WP_SQLite_Driver_Exception When the translation fails.
29622962
*/
29632963
private function translate_query_expression( WP_Parser_Node $node ): string {
2964+
// Get the query expression subnode under which we need to look for the
2965+
// SELECT item list node. This prevents searching under "withClause".
2966+
$query_expr_main = (
2967+
$node->get_first_child_node( 'queryExpressionBody' )
2968+
?? $node->get_first_child_node( 'queryExpressionParens' )
2969+
);
2970+
$query_term = $query_expr_main->get_first_descendant_node( 'queryTerm' );
2971+
$has_union = $query_expr_main->has_child_token( WP_MySQL_Lexer::UNION_SYMBOL );
2972+
$has_except = $query_expr_main->has_child_token( WP_MySQL_Lexer::EXCEPT_SYMBOL );
2973+
$has_intersect = $query_term->has_child_token( WP_MySQL_Lexer::INTERSECT_SYMBOL );
2974+
29642975
/*
29652976
* When the ORDER BY clause is present, we need to disambiguate the item
29662977
* list and make sure they don't cause an "ambiguous column name" error.
29672978
*/
29682979
$disambiguated_order_list = array();
29692980
$order_clause = $node->get_first_child_node( 'orderClause' );
2970-
if ( $order_clause ) {
2971-
$order_list = $order_clause->get_first_child_node( 'orderList' );
2972-
$select_item_list = $node->get_first_descendant_node( 'selectItemList' );
2981+
if ( $order_clause && ! $has_union && ! $has_except && ! $has_intersect ) {
2982+
/*
2983+
* [GRAMMAR]
2984+
* queryExpression: (withClause)? (
2985+
* queryExpressionBody orderClause? limitClause?
2986+
* | queryExpressionParens orderClause? limitClause?
2987+
* ) (procedureAnalyseClause)?
2988+
*/
29732989

2990+
// Create the SELECT item disambiguation map.
2991+
$select_item_list = $query_expr_main->get_first_descendant_node( 'selectItemList' );
29742992
$disambiguation_map = $this->create_select_item_disambiguation_map( $select_item_list );
29752993

29762994
// For each "orderList" item, search for a matching SELECT item.
29772995
$disambiguated_order_list = array();
2996+
$order_list = $order_clause->get_first_child_node( 'orderList' );
29782997
foreach ( $order_list->get_child_nodes() as $order_item ) {
29792998
/*
29802999
* [GRAMMAR]

0 commit comments

Comments
 (0)