@@ -2512,6 +2512,8 @@ private function translate( $node ): ?string {
25122512
25132513 $ rule_name = $ node ->rule_name ;
25142514 switch ( $ rule_name ) {
2515+ case 'queryExpression ' :
2516+ return $ this ->translate_query_expression ( $ node );
25152517 case 'querySpecification ' :
25162518 // Translate "HAVING ..." without "GROUP BY ..." to "GROUP BY 1 HAVING ...".
25172519 if ( $ node ->has_child_node ( 'havingClause ' ) && ! $ node ->has_child_node ( 'groupByClause ' ) ) {
@@ -2905,6 +2907,123 @@ private function translate_qualified_identifier(
29052907 return implode ( '. ' , $ parts );
29062908 }
29072909
2910+ /**
2911+ * Translate a MySQL query expression to SQLite.
2912+ *
2913+ * @param WP_Parser_Node $node The "queryExpression" AST node.
2914+ * @return string The translated value.
2915+ * @throws WP_SQLite_Driver_Exception When the translation fails.
2916+ */
2917+ private function translate_query_expression ( WP_Parser_Node $ node ): string {
2918+ /*
2919+ * When the ORDER BY clause is present, we need to make sure it doesn't
2920+ * cause an "ambiguous column name" error.
2921+ *
2922+ * In SQLite, all column names that exist in multiple tables used in the
2923+ * query must be fully qualified in the ORDER BY clause. In MySQL, these
2924+ * can be disambiguated in the SELECT item list.
2925+ *
2926+ * For example, with tables "t1" and "t2" both having a "name" column,
2927+ * the following query will cause an "ambiguous column name" error in
2928+ * SQLite, but not in MySQL:
2929+ *
2930+ * SELECT t1.name FROM t1 JOIN t2 ON t2.t1_id = t1.id ORDER BY name
2931+ *
2932+ * This is because MySQL first considers the "name" column that was used
2933+ * in the SELECT list. If it is unambiguous, it will be used in ORDER BY.
2934+ *
2935+ * To address this, let's look for unqualified column references in the
2936+ * ORDER BY clause and try to qualify them using the SELECT item list.
2937+ * In other words, the above query will be rewritten as follows:
2938+ *
2939+ * SELECT t1.name FROM t1 JOIN t2 ON t2.t1_id = t1.id ORDER BY t1.name
2940+ *
2941+ * Note that the ORDER BY column was rewritten from "name" to "t1.name".
2942+ */
2943+ $ disambiguated_order_list = array ();
2944+ $ order_clause = $ node ->get_first_child_node ( 'orderClause ' );
2945+ if ( $ order_clause ) {
2946+ $ order_list = $ order_clause ->get_first_child_node ( 'orderList ' );
2947+ $ select_item_list = $ node ->get_first_descendant_node ( 'selectItemList ' );
2948+
2949+ // Get a list of column references used in the SELECT item list.
2950+ $ select_column_refs = array ();
2951+ 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+ }
2956+
2957+ $ select_item_expr = $ select_item ->get_first_child_node ( 'expr ' );
2958+ if ( ! $ select_item_expr ) {
2959+ continue ;
2960+ }
2961+
2962+ $ select_column_ref = $ select_item ->get_first_descendant_node ( 'columnRef ' );
2963+ if (
2964+ $ select_column_ref
2965+ && $ this ->translate ( $ select_item_expr ) === $ this ->translate ( $ select_column_ref )
2966+ ) {
2967+ $ select_column_refs [] = $ select_column_ref ;
2968+ }
2969+ }
2970+
2971+ // For each ORDER BY item, try to find a corresponding SELECT item.
2972+ foreach ( $ order_list ->get_child_nodes () as $ order_item ) {
2973+ $ order_expr = $ order_item ->get_first_child_node ( 'expr ' );
2974+ $ order_column_ref = $ order_expr ->get_first_descendant_node ( 'columnRef ' );
2975+
2976+ $ select_item_matches = array ();
2977+ if (
2978+ $ order_column_ref
2979+ && $ this ->translate ( $ order_column_ref ) === $ this ->translate ( $ order_expr )
2980+ && null === $ order_column_ref ->get_first_descendant_node ( 'dotIdentifier ' )
2981+ ) {
2982+ // Look for select items that match the column reference.
2983+ foreach ( $ select_column_refs as $ select_column_ref ) {
2984+ $ dot_identifiers = $ select_column_ref ->get_descendant_nodes ( 'dotIdentifier ' );
2985+ if ( count ( $ dot_identifiers ) === 0 ) {
2986+ continue ;
2987+ }
2988+
2989+ $ last_dot_identifier = end ( $ dot_identifiers );
2990+ $ select_column_name = $ this ->translate ( $ last_dot_identifier ->get_first_child_node () );
2991+ $ order_column_name = $ this ->translate ( $ order_column_ref );
2992+ if ( $ select_column_name === $ order_column_name ) {
2993+ $ select_item_matches [] = $ this ->translate ( $ select_column_ref );
2994+ }
2995+ }
2996+ }
2997+
2998+ if ( 1 === count ( $ select_item_matches ) ) {
2999+ $ direction = $ order_item ->get_first_child_node ( 'direction ' );
3000+ $ translated_order_item = sprintf (
3001+ '%s%s ' ,
3002+ $ select_item_matches [0 ],
3003+ null !== $ direction ? ( ' ' . $ this ->translate ( $ direction ) ) : ''
3004+ );
3005+ } else {
3006+ $ translated_order_item = $ this ->translate ( $ order_item );
3007+ }
3008+ $ disambiguated_order_list [] = $ translated_order_item ;
3009+ }
3010+
3011+ // Translate the query expression, replacing the ORDER BY list with
3012+ // the one that was constructed using the disambiguation algorithm.
3013+ $ parts = array ();
3014+ foreach ( $ node ->get_children () as $ child ) {
3015+ if ( $ child instanceof WP_Parser_Node && 'orderClause ' === $ child ->rule_name ) {
3016+ $ parts [] = 'ORDER BY ' . implode ( ', ' , $ disambiguated_order_list );
3017+ } else {
3018+ $ parts [] = $ this ->translate ( $ child );
3019+ }
3020+ }
3021+ return implode ( ' ' , $ parts );
3022+ }
3023+
3024+ return $ this ->translate_sequence ( $ node ->get_children () );
3025+ }
3026+
29083027 /**
29093028 * Translate a MySQL simple expression to SQLite.
29103029 *
0 commit comments