@@ -361,6 +361,30 @@ public SqlNode visit(SqlIdentifier id) {
361361 List .of (SqlIdentifier .STAR ),
362362 call .getParserPosition (),
363363 call .getFunctionQuantifier ());
364+ } else if (call .getKind () == SqlKind .IN || call .getKind () == SqlKind .NOT_IN ) {
365+ // Fix for tuple IN / NOT IN queries: Convert SqlNodeList to ROW SqlCall
366+ //
367+ // When RelToSqlConverter converts a tuple expression like (id, name) back to
368+ // SqlNode, it generates a bare SqlNodeList instead of wrapping it in a ROW
369+ // operator. This causes validation to fail because:
370+ // 1. SqlValidator.deriveType() doesn't know how to handle SqlNodeList
371+ // 2. SqlToRelConverter.visit(SqlNodeList) throws UnsupportedOperationException
372+ //
373+ // For example, the query:
374+ // WHERE (id, name) NOT IN (SELECT uid, name FROM ...)
375+ //
376+ // After Rel-to-SQL conversion becomes:
377+ // IN operator with operands: [SqlNodeList[id, name], SqlSelect[...]]
378+ //
379+ // But it should be:
380+ // IN operator with operands: [ROW(id, name), SqlSelect[...]]
381+ //
382+ // This fix wraps the SqlNodeList in a ROW SqlCall before validation,
383+ // ensuring proper type derivation and subsequent SQL-to-Rel conversion.
384+ if (!call .getOperandList ().isEmpty ()
385+ && call .getOperandList ().get (0 ) instanceof SqlNodeList nodes ) {
386+ call .setOperand (0 , SqlStdOperatorTable .ROW .createCall (nodes ));
387+ }
364388 }
365389 return super .visit (call );
366390 }
0 commit comments