@@ -418,35 +418,41 @@ private static void unparseFloor(SqlWriter writer, SqlCall call) {
418418 }
419419 Join join = (Join ) rel ;
420420
421- // ClickHouse primarily requires wrapping the right side of a JOIN
422- // when it contains a nested JOIN
423- return containsJoinRecursive (join .getRight ());
421+ // ClickHouse requires wrapping the right-side input if it's a JOIN
422+ // to ensure that internal table qualifiers are flattened into explicit aliases.
423+ // This solves the Code 47 UNKNOWN_IDENTIFIER error.
424+ RelNode right = join .getRight ();
425+
426+ // If the right side is a Join or a Project containing a Join, it needs aliasing protection
427+ return right instanceof Join || containsJoinRecursive (right );
424428 }
425429
426430 /**
427431 * Checks whether the given RelNode contains a JOIN that is directly exposed
428- * at the JOIN boundary, possibly wrapped by a small number of transparent
429- * single-input operators (e.g. Project, Filter, Sort).
432+ * to the outer scope, which could lead to "Unknown Identifier" errors in ClickHouse.
430433 *
431- * <p>This method intentionally does NOT perform a full tree traversal.
432- * ClickHouse only requires wrapping when a JOIN appears directly as a JOIN input;
433- * JOINs deeper in the subtree do not trigger the restriction.
434+ * <p>ClickHouse (v25.x+) has strict scoping rules: when a JOIN appears on the
435+ * right side of another JOIN, internal table qualifiers (e.g., 'd2.loc') are
436+ * stripped and become invisible to the outer query unless they are explicitly
437+ * aliased within a subquery.
434438 *
435- * <p>Therefore, a full RelVisitor is avoided here to prevent over-detection
436- * and unnecessary wrapping.
439+ * <p>We only check for JOINs wrapped by transparent single-input operators
440+ * (Project, Filter, Sort) because these operators are typically collapsed
441+ * into the same SELECT block, exposing the problematic JOIN structure to
442+ * the outer boundary.
437443 */
438444 private static boolean containsJoinRecursive (RelNode rel ) {
439445 if (rel instanceof Join || rel instanceof Correlate ) {
440446 return true ;
441447 }
442448
443- // Look through transparent single-input operators
444- // These don't create subquery boundaries in the SQL
449+ // Look through transparent single-input operators.
450+ // We exclude Aggregate here because it naturally triggers a subquery
451+ // boundary in RelToSqlConverter, which already provides the necessary isolation.
445452 if (rel instanceof Project
446453 || rel instanceof Filter
447- || rel instanceof Sort
448- || rel instanceof Aggregate ) {
449- return containsJoinRecursive (rel .getInput (0 ));
454+ || rel instanceof Sort ) {
455+ return rel .getInputs ().size () == 1 && containsJoinRecursive (rel .getInput (0 ));
450456 }
451457
452458 return false ;
0 commit comments