8282import com .starrocks .sql .ast .PartitionKeyDesc ;
8383import com .starrocks .sql .ast .PartitionValue ;
8484import com .starrocks .sql .ast .QualifiedName ;
85+ import com .starrocks .sql .ast .QueryRelation ;
8586import com .starrocks .sql .ast .QueryStatement ;
8687import com .starrocks .sql .ast .RangePartitionDesc ;
8788import com .starrocks .sql .ast .Relation ;
140141
141142import java .time .LocalDateTime ;
142143import java .time .format .DateTimeFormatter ;
144+ import java .util .ArrayDeque ;
143145import java .util .ArrayList ;
144146import java .util .Arrays ;
145147import java .util .Collections ;
148+ import java .util .Deque ;
146149import java .util .HashMap ;
147150import java .util .HashSet ;
148151import java .util .Iterator ;
@@ -738,6 +741,8 @@ public static Map<TableName, Table> collectAllTable(StatementBase statementBase)
738741
739742 private static class TableCollector extends AstTraverser <Void , Void > {
740743 protected Map <TableName , Table > tables ;
744+ private final Deque <Set <String >> cteNameScopes = new ArrayDeque <>();
745+ private final Deque <Boolean > recursiveCteScopes = new ArrayDeque <>();
741746
742747 public TableCollector () {
743748 this .tables = Maps .newHashMap ();
@@ -754,6 +759,36 @@ public Void visitQueryStatement(QueryStatement statement, Void context) {
754759 return visit (statement .getQueryRelation ());
755760 }
756761
762+ @ Override
763+ public Void visitSelect (SelectRelation node , Void context ) {
764+ if (!node .hasWithClause ()) {
765+ return super .visitSelect (node , context );
766+ }
767+ cteNameScopes .push (new HashSet <>());
768+ recursiveCteScopes .push (node .isHasRecursiveCTE ());
769+ try {
770+ return super .visitSelect (node , context );
771+ } finally {
772+ recursiveCteScopes .pop ();
773+ cteNameScopes .pop ();
774+ }
775+ }
776+
777+ @ Override
778+ public Void visitSetOp (SetOperationRelation node , Void context ) {
779+ if (!node .hasWithClause ()) {
780+ return super .visitSetOp (node , context );
781+ }
782+ cteNameScopes .push (new HashSet <>());
783+ recursiveCteScopes .push (node .isHasRecursiveCTE ());
784+ try {
785+ return super .visitSetOp (node , context );
786+ } finally {
787+ recursiveCteScopes .pop ();
788+ cteNameScopes .pop ();
789+ }
790+ }
791+
757792 // ------------------------------------------- DML Statement -------------------------------------------------------
758793
759794 @ Override
@@ -790,10 +825,85 @@ public Void visitMergeIntoStatement(MergeIntoStmt node, Void context) {
790825
791826 @ Override
792827 public Void visitTable (TableRelation node , Void context ) {
828+ if (isUnresolvedCteReference (node )) {
829+ return null ;
830+ }
793831 Table table = node .getTable ();
794832 tables .put (node .getName (), table );
795833 return null ;
796834 }
835+
836+ @ Override
837+ public Void visitCTE (CTERelation node , Void context ) {
838+ if (node .isRecursive () && !node .isAnchor ()) {
839+ // An analyzed recursive member consumes the current CTE through a non-anchor CTERelation.
840+ // Do not expand its definition again, or the collector will revisit the same recursive member.
841+ return null ;
842+ }
843+ if (cteNameScopes .isEmpty ()) {
844+ return super .visitCTE (node , context );
845+ }
846+ if (isInRecursiveCteScope () && node .getCteQueryStatement ().getQueryRelation ()
847+ instanceof SetOperationRelation setOperationRelation ) {
848+ visitRecursiveCteDefinition (node , setOperationRelation , context );
849+ } else {
850+ visit (node .getCteQueryStatement (), context );
851+ }
852+ addCteName (cteNameScopes .peek (), node );
853+ return null ;
854+ }
855+
856+ private void visitRecursiveCteDefinition (CTERelation cteRelation , SetOperationRelation setOperationRelation ,
857+ Void context ) {
858+ List <QueryRelation > relations = setOperationRelation .getRelations ();
859+ if (relations .isEmpty ()) {
860+ return ;
861+ }
862+ // Mirror QueryAnalyzer.tryProcessRecursiveCte: the first set-op child is the anchor.
863+ // The current CTE name is not visible there, so a same-named table must still be collected.
864+ visit (relations .get (0 ), context );
865+
866+ // Only recursive members can see the current CTE name. Keep that visibility in a temporary
867+ // scope so unresolved self-references are skipped without hiding anchor base tables.
868+ Set <String > recursiveNames = new HashSet <>();
869+ addCteName (recursiveNames , cteRelation );
870+ cteNameScopes .push (recursiveNames );
871+ try {
872+ for (int i = 1 ; i < relations .size (); i ++) {
873+ visit (relations .get (i ), context );
874+ }
875+ } finally {
876+ cteNameScopes .pop ();
877+ }
878+ }
879+
880+ private boolean isInRecursiveCteScope () {
881+ return !recursiveCteScopes .isEmpty () && recursiveCteScopes .peek ();
882+ }
883+
884+ private void addCteName (Set <String > names , CTERelation cteRelation ) {
885+ if (!Strings .isNullOrEmpty (cteRelation .getName ())) {
886+ names .add (cteRelation .getName ());
887+ }
888+ }
889+
890+ private boolean isUnresolvedCteReference (TableRelation node ) {
891+ if (node .getTable () != null || cteNameScopes .isEmpty ()) {
892+ return false ;
893+ }
894+ TableName tableName = node .getName ();
895+ if (tableName == null || !Strings .isNullOrEmpty (tableName .getCatalog ()) ||
896+ !Strings .isNullOrEmpty (tableName .getDb ()) || Strings .isNullOrEmpty (tableName .getTbl ())) {
897+ return false ;
898+ }
899+ String tableNameWithoutDb = tableName .getTbl ();
900+ for (Set <String > cteNames : cteNameScopes ) {
901+ if (cteNames .contains (tableNameWithoutDb )) {
902+ return true ;
903+ }
904+ }
905+ return false ;
906+ }
797907 }
798908
799909 public static Map <TableName , Table > collectAllTableWithAlias (StatementBase statementBase ) {
0 commit comments