001package gudusoft.gsqlparser.resolver2.namespace;
002
003import gudusoft.gsqlparser.nodes.TResultColumn;
004import gudusoft.gsqlparser.nodes.TResultColumnList;
005import gudusoft.gsqlparser.nodes.TTable;
006import gudusoft.gsqlparser.resolver2.ColumnLevel;
007import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
008import gudusoft.gsqlparser.resolver2.model.ColumnSource;
009import gudusoft.gsqlparser.sqlenv.TSQLEnv;
010import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
011
012import java.util.ArrayDeque;
013import java.util.ArrayList;
014import java.util.Collections;
015import java.util.Deque;
016import java.util.HashSet;
017import java.util.LinkedHashMap;
018import java.util.List;
019import java.util.Map;
020import java.util.Set;
021
022/**
023 * Namespace representing a subquery.
024 * Provides columns from the subquery's SELECT list.
025 *
026 * Example:
027 * FROM (SELECT id, name FROM users) AS t
028 *       ^^^^^^^^^^^^^^^^^^^^^^^^
029 *       SubqueryNamespace exposes columns: id, name
030 */
031public class SubqueryNamespace extends AbstractNamespace {
032
033    private final TSelectSqlStatement subquery;
034    private final String alias;
035
036    /** Inferred columns from star push-down */
037    private Map<String, ColumnSource> inferredColumns;
038
039    /** Track inferred column names for getInferredColumns() */
040    private Set<String> inferredColumnNames;
041
042    /** Traced columns from qualified references (e.g., table1.x in ON conditions) */
043    private Map<String, TTable> tracedColumnTables;
044
045    /** Whether this namespace was created from a TABLE function */
046    private final boolean fromTableFunction;
047
048    /**
049     * Cache for nested SubqueryNamespace objects to avoid repeated creation.
050     * Key: TSelectSqlStatement, Value: validated SubqueryNamespace
051     */
052    private Map<TSelectSqlStatement, SubqueryNamespace> nestedSubqueryCache;
053
054    /**
055     * Cache for nested TableNamespace objects to avoid repeated creation.
056     * Key: TTable, Value: validated TableNamespace
057     */
058    private Map<TTable, TableNamespace> nestedTableCache;
059
060    /** The TTable that wraps this subquery (for legacy sync) */
061    private TTable sourceTable;
062
063    /** TSQLEnv for looking up table metadata during star column resolution */
064    private TSQLEnv sqlEnv;
065
066    public SubqueryNamespace(TSelectSqlStatement subquery,
067                            String alias,
068                            INameMatcher nameMatcher) {
069        this(subquery, alias, nameMatcher, false);
070    }
071
072    public SubqueryNamespace(TSelectSqlStatement subquery,
073                            String alias,
074                            INameMatcher nameMatcher,
075                            boolean fromTableFunction) {
076        super(subquery, nameMatcher);
077        this.subquery = subquery;
078        this.alias = alias;
079        this.fromTableFunction = fromTableFunction;
080    }
081
082    public SubqueryNamespace(TSelectSqlStatement subquery, String alias) {
083        super(subquery);
084        this.subquery = subquery;
085        this.alias = alias;
086        this.fromTableFunction = false;
087    }
088
089    /**
090     * Gets or creates a cached nested SubqueryNamespace that inherits the guessColumnStrategy from this namespace.
091     * This ensures config-based isolation propagates through nested resolution.
092     * The namespace is cached to avoid repeated creation and validation for the same subquery.
093     * Also propagates sqlEnv for metadata lookup in star column resolution.
094     *
095     * @param subquery The subquery statement
096     * @param alias The alias for the subquery (not used as cache key, only for display)
097     * @return A validated SubqueryNamespace (cached or newly created)
098     */
099    private SubqueryNamespace getOrCreateNestedNamespace(TSelectSqlStatement subquery, String alias) {
100        if (subquery == null) {
101            return null;
102        }
103        // Initialize cache lazily
104        if (nestedSubqueryCache == null) {
105            nestedSubqueryCache = new java.util.HashMap<>();
106        }
107        // Check cache first
108        SubqueryNamespace cached = nestedSubqueryCache.get(subquery);
109        if (cached != null) {
110            return cached;
111        }
112        // Create new namespace
113        SubqueryNamespace nested = new SubqueryNamespace(subquery, alias, nameMatcher);
114        // Propagate guessColumnStrategy for config-based isolation
115        if (this.guessColumnStrategy >= 0) {
116            nested.setGuessColumnStrategy(this.guessColumnStrategy);
117        }
118        // Propagate sqlEnv for metadata lookup during star column resolution
119        if (this.sqlEnv != null) {
120            nested.setSqlEnv(this.sqlEnv);
121        }
122        // Validate and cache
123        nested.validate();
124        nestedSubqueryCache.put(subquery, nested);
125        return nested;
126    }
127
128    /**
129     * Gets or creates a cached TableNamespace.
130     * The namespace is cached to avoid repeated creation and validation for the same table.
131     * Passes sqlEnv to enable metadata lookup for star column resolution.
132     *
133     * @param table The table
134     * @return A validated TableNamespace (cached or newly created)
135     */
136    private TableNamespace getOrCreateTableNamespace(TTable table) {
137        if (table == null) {
138            return null;
139        }
140        // Initialize cache lazily
141        if (nestedTableCache == null) {
142            nestedTableCache = new java.util.HashMap<>();
143        }
144        // Check cache first
145        TableNamespace cached = nestedTableCache.get(table);
146        if (cached != null) {
147            return cached;
148        }
149        // Create new namespace with sqlEnv for metadata lookup
150        TableNamespace tableNs = new TableNamespace(table, nameMatcher, sqlEnv);
151        // Validate and cache
152        tableNs.validate();
153        nestedTableCache.put(table, tableNs);
154        return tableNs;
155    }
156
157    /**
158     * Creates a nested SubqueryNamespace that inherits the guessColumnStrategy from this namespace.
159     * This method is deprecated - use getOrCreateNestedNamespace for caching support.
160     *
161     * @param subquery The subquery statement
162     * @param alias The alias for the subquery
163     * @return A new SubqueryNamespace with inherited guessColumnStrategy and sqlEnv
164     * @deprecated Use getOrCreateNestedNamespace instead
165     */
166    @Deprecated
167    private SubqueryNamespace createNestedNamespace(TSelectSqlStatement subquery, String alias) {
168        SubqueryNamespace nested = new SubqueryNamespace(subquery, alias, nameMatcher);
169        // Propagate guessColumnStrategy for config-based isolation
170        if (this.guessColumnStrategy >= 0) {
171            nested.setGuessColumnStrategy(this.guessColumnStrategy);
172        }
173        // Propagate sqlEnv for metadata lookup
174        if (this.sqlEnv != null) {
175            nested.setSqlEnv(this.sqlEnv);
176        }
177        return nested;
178    }
179
180    /**
181     * Returns true if this namespace was created from a TABLE function's subquery.
182     */
183    public boolean isFromTableFunction() {
184        return fromTableFunction;
185    }
186
187    /**
188     * Set the TTable that wraps this subquery.
189     * Used by ScopeBuilder for legacy sync support.
190     *
191     * @param sourceTable the TTable that contains this subquery
192     */
193    public void setSourceTable(TTable sourceTable) {
194        this.sourceTable = sourceTable;
195    }
196
197    @Override
198    public TTable getSourceTable() {
199        return sourceTable;
200    }
201
202    /**
203     * Set the TSQLEnv for metadata lookup.
204     * Used when resolving star columns to find which columns exist in underlying tables.
205     *
206     * @param sqlEnv the SQL environment containing table metadata
207     */
208    public void setSqlEnv(TSQLEnv sqlEnv) {
209        this.sqlEnv = sqlEnv;
210    }
211
212    /**
213     * Get the TSQLEnv used for metadata lookup.
214     *
215     * @return the SQL environment, or null if not set
216     */
217    public TSQLEnv getSqlEnv() {
218        return sqlEnv;
219    }
220
221    @Override
222    public String getDisplayName() {
223        return alias != null ? alias : "<subquery>";
224    }
225
226    @Override
227    public TTable getFinalTable() {
228        // Try to find the underlying table from the FROM clause
229        // This works for both star subqueries (SELECT *) and explicit column subqueries
230        if (subquery != null && subquery.tables != null && subquery.tables.size() > 0) {
231            // First check if there's a qualified star column (e.g., ta.* or tb.*)
232            // If so, we should return the table matching that alias
233            TTable qualifiedStarTable = findTableFromQualifiedStar();
234            if (qualifiedStarTable != null) {
235                return qualifiedStarTable;
236            }
237
238            // For single-table subqueries, return that table
239            // For multi-table (JOIN/comma) subqueries, only return if we can determine source
240            TTable firstTable = subquery.tables.getTable(0);
241            if (firstTable != null) {
242                // If it's a physical table, return it - BUT only if we can identify the source
243                if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
244                    // For multi-table subqueries without qualified star, we can't determine
245                    // which table the column comes from. Don't blindly return first table.
246                    if (subquery.tables.size() > 1 && qualifiedStarTable == null) {
247                        return null;
248                    }
249                    // Check if it's a CTE reference - if so, trace through to physical table
250                    if (firstTable.isCTEName() && firstTable.getCTE() != null) {
251                        return traceTableThroughCTE(firstTable.getCTE());
252                    }
253                    return firstTable;
254                }
255                // If it's a subquery, recursively trace (use cached namespace)
256                if (firstTable.getSubquery() != null) {
257                    SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
258                        firstTable.getSubquery(),
259                        firstTable.getAliasName()
260                    );
261                    return nestedNs != null ? nestedNs.getFinalTable() : null;
262                }
263                // If it's a join, get the first base table from the join
264                if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
265                    return findFirstPhysicalTableFromJoin(firstTable);
266                }
267            }
268        }
269        return null;
270    }
271
272    /**
273     * Find the table referenced by a qualified star column (e.g., ta.* -> table_a).
274     * Returns null if there's no qualified star or if the star is unqualified (*).
275     */
276    private TTable findTableFromQualifiedStar() {
277        if (subquery == null || subquery.getResultColumnList() == null) {
278            return null;
279        }
280
281        TResultColumnList selectList = subquery.getResultColumnList();
282        for (int i = 0; i < selectList.size(); i++) {
283            TResultColumn resultCol = selectList.getResultColumn(i);
284            if (resultCol == null) continue;
285
286            String colStr = resultCol.toString().trim();
287            // Check if it's a qualified star (contains . before *)
288            if (colStr.endsWith("*") && colStr.contains(".")) {
289                // Extract the table alias/name prefix (e.g., "ta" from "ta.*")
290                int dotIndex = colStr.lastIndexOf('.');
291                if (dotIndex > 0) {
292                    String tablePrefix = colStr.substring(0, dotIndex).trim();
293                    // Find the table with this alias or name
294                    TTable matchingTable = findTableByAliasOrName(tablePrefix);
295                    if (matchingTable != null) {
296                        // Check for CTE reference first - trace through to physical table
297                        if (matchingTable.isCTEName() && matchingTable.getCTE() != null) {
298                            return traceTableThroughCTE(matchingTable.getCTE());
299                        }
300                        // If the matching table is itself a subquery, trace through it (use cached namespace)
301                        if (matchingTable.getSubquery() != null) {
302                            SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
303                                matchingTable.getSubquery(),
304                                matchingTable.getAliasName()
305                            );
306                            return nestedNs != null ? nestedNs.getFinalTable() : null;
307                        }
308                        return matchingTable;
309                    }
310                }
311            }
312        }
313        return null;
314    }
315
316    /**
317     * Find a table in the FROM clause by alias or table name.
318     * Also searches through JOIN expressions to find tables like PRD2_ODW.USI
319     * that are joined via LEFT OUTER JOIN.
320     */
321    private TTable findTableByAliasOrName(String nameOrAlias) {
322        if (subquery == null || nameOrAlias == null) {
323            return null;
324        }
325
326        // Use getRelations() to get ALL tables including those in JOINs
327        java.util.ArrayList<TTable> relations = subquery.getRelations();
328        if (relations != null) {
329            for (TTable table : relations) {
330                if (table == null) continue;
331
332                // For JOIN type tables, search inside the join expression
333                if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
334                    TTable found = findTableInJoinExpr(table.getJoinExpr(), nameOrAlias);
335                    if (found != null) {
336                        return found;
337                    }
338                    continue;
339                }
340
341                // Check alias first
342                String alias = table.getAliasName();
343                if (alias != null && nameMatcher.matches(alias, nameOrAlias)) {
344                    return table;
345                }
346
347                // Check table name (for physical tables)
348                if (table.getTableName() != null) {
349                    String tableName = table.getTableName().toString();
350                    if (nameMatcher.matches(tableName, nameOrAlias)) {
351                        return table;
352                    }
353                    // Also check just the table name part (without schema)
354                    String shortName = table.getName();
355                    if (shortName != null && nameMatcher.matches(shortName, nameOrAlias)) {
356                        return table;
357                    }
358                }
359            }
360        }
361
362        // Fallback to subquery.tables if getRelations() didn't find it
363        if (subquery.tables != null) {
364            for (int i = 0; i < subquery.tables.size(); i++) {
365                TTable table = subquery.tables.getTable(i);
366                if (table == null) continue;
367
368                // Check alias first
369                String alias = table.getAliasName();
370                if (alias != null && nameMatcher.matches(alias, nameOrAlias)) {
371                    return table;
372                }
373
374                // Check table name
375                if (table.getTableName() != null) {
376                    String tableName = table.getTableName().toString();
377                    if (nameMatcher.matches(tableName, nameOrAlias)) {
378                        return table;
379                    }
380                }
381            }
382        }
383        return null;
384    }
385
386    /**
387     * Find a table within a JOIN expression by alias or table name.
388     * Recursively searches both left and right sides of the join.
389     */
390    private TTable findTableInJoinExpr(gudusoft.gsqlparser.nodes.TJoinExpr joinExpr, String nameOrAlias) {
391        if (joinExpr == null || nameOrAlias == null) {
392            return null;
393        }
394
395        // Check left table
396        TTable leftTable = joinExpr.getLeftTable();
397        if (leftTable != null) {
398            TTable found = matchTableByAliasOrName(leftTable, nameOrAlias);
399            if (found != null) {
400                return found;
401            }
402            // Recursively check if left is also a join
403            if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join && leftTable.getJoinExpr() != null) {
404                found = findTableInJoinExpr(leftTable.getJoinExpr(), nameOrAlias);
405                if (found != null) {
406                    return found;
407                }
408            }
409        }
410
411        // Check right table
412        TTable rightTable = joinExpr.getRightTable();
413        if (rightTable != null) {
414            TTable found = matchTableByAliasOrName(rightTable, nameOrAlias);
415            if (found != null) {
416                return found;
417            }
418            // Recursively check if right is also a join
419            if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join && rightTable.getJoinExpr() != null) {
420                found = findTableInJoinExpr(rightTable.getJoinExpr(), nameOrAlias);
421                if (found != null) {
422                    return found;
423                }
424            }
425        }
426
427        return null;
428    }
429
430    /**
431     * Check if a table matches the given alias or name.
432     */
433    private TTable matchTableByAliasOrName(TTable table, String nameOrAlias) {
434        if (table == null || nameOrAlias == null) {
435            return null;
436        }
437
438        // Check alias first
439        String alias = table.getAliasName();
440        if (alias != null && nameMatcher.matches(alias, nameOrAlias)) {
441            return table;
442        }
443
444        // Check table name (for physical tables)
445        if (table.getTableName() != null) {
446            String tableName = table.getTableName().toString();
447            if (nameMatcher.matches(tableName, nameOrAlias)) {
448                return table;
449            }
450            // Also check just the table name part (without schema)
451            // For "PRD2_ODW.USI", getName() returns "USI"
452            String shortName = table.getName();
453            if (shortName != null && nameMatcher.matches(shortName, nameOrAlias)) {
454                return table;
455            }
456        }
457
458        return null;
459    }
460
461    /**
462     * Find the first physical table from a JOIN expression.
463     * Recursively traverses left side of joins.
464     */
465    private TTable findFirstPhysicalTableFromJoin(TTable joinTable) {
466        if (joinTable == null) {
467            return null;
468        }
469
470        gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr();
471        if (joinExpr != null && joinExpr.getLeftTable() != null) {
472            TTable leftTable = joinExpr.getLeftTable();
473
474            if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
475                // Check if it's a CTE reference - trace through if so
476                if (leftTable.isCTEName() && leftTable.getCTE() != null) {
477                    return traceTableThroughCTE(leftTable.getCTE());
478                }
479                return leftTable;
480            }
481            if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
482                return findFirstPhysicalTableFromJoin(leftTable);
483            }
484            // If it's a subquery, trace through it (use cached namespace)
485            if (leftTable.getSubquery() != null) {
486                SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
487                    leftTable.getSubquery(),
488                    leftTable.getAliasName()
489                );
490                return nestedNs != null ? nestedNs.getFinalTable() : null;
491            }
492        }
493        return null;
494    }
495
496    /**
497     * Trace through a CTE to find its underlying physical table.
498     * This handles CTE chains like: CTE1 -> CTE2 -> CTE3 -> physical_table
499     */
500    private TTable traceTableThroughCTE(gudusoft.gsqlparser.nodes.TCTE cteNode) {
501        if (cteNode == null || cteNode.getSubquery() == null) {
502            return null;
503        }
504
505        gudusoft.gsqlparser.stmt.TSelectSqlStatement cteSubquery = cteNode.getSubquery();
506
507        // Handle UNION in the CTE
508        if (cteSubquery.isCombinedQuery()) {
509            // For UNION, trace the left branch
510            gudusoft.gsqlparser.stmt.TSelectSqlStatement leftStmt = cteSubquery.getLeftStmt();
511            if (leftStmt != null && leftStmt.tables != null && leftStmt.tables.size() > 0) {
512                cteSubquery = leftStmt;
513            }
514        }
515
516        if (cteSubquery.tables == null || cteSubquery.tables.size() == 0) {
517            return null;
518        }
519
520        TTable firstTable = cteSubquery.tables.getTable(0);
521        if (firstTable == null) {
522            return null;
523        }
524
525        // If it's a physical table (not CTE), we found it
526        if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname && !firstTable.isCTEName()) {
527            return firstTable;
528        }
529
530        // If it's another CTE reference, continue tracing
531        if (firstTable.isCTEName() && firstTable.getCTE() != null) {
532            return traceTableThroughCTE(firstTable.getCTE());
533        }
534
535        // If it's a subquery, trace through it (use cached namespace)
536        if (firstTable.getSubquery() != null) {
537            SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
538                firstTable.getSubquery(),
539                firstTable.getAliasName()
540            );
541            return nestedNs != null ? nestedNs.getFinalTable() : null;
542        }
543
544        // If it's a join, get the first base table
545        if (firstTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
546            return findFirstPhysicalTableFromJoin(firstTable);
547        }
548
549        return null;
550    }
551
552    @Override
553    public List<TTable> getAllFinalTables() {
554        // For non-UNION subqueries, return the single final table
555        // Multiple tables only make sense for UNION queries where a column
556        // can come from multiple branches (handled by UnionNamespace)
557        TTable finalTable = getFinalTable();
558        if (finalTable != null) {
559            return Collections.singletonList(finalTable);
560        }
561
562        return Collections.emptyList();
563    }
564
565    @Override
566    protected void doValidate() {
567        // Extract columns from SELECT list
568        columnSources = new LinkedHashMap<>();
569
570        TResultColumnList selectList = subquery.getResultColumnList();
571        if (selectList == null) {
572            return;
573        }
574
575        for (int i = 0; i < selectList.size(); i++) {
576            TResultColumn resultCol = selectList.getResultColumn(i);
577
578            // Determine column name
579            String colName = getColumnName(resultCol);
580            if (colName == null) {
581                // Unnamed expression, use ordinal
582                colName = "col_" + (i + 1);
583            }
584
585            // Find the source table for this column (if it's a qualified column reference)
586            TTable sourceTable = findSourceTableForResultColumn(resultCol);
587
588            // Create column source with override table if found
589            ColumnSource source;
590            if (sourceTable != null) {
591                source = new ColumnSource(
592                    this,
593                    colName,
594                    resultCol,
595                    1.0,  // Definite - from SELECT list
596                    "subquery_select_list_qualified",
597                    sourceTable  // Override table for proper tracing
598                );
599            } else {
600                source = new ColumnSource(
601                    this,
602                    colName,
603                    resultCol,
604                    1.0,  // Definite - from SELECT list
605                    "subquery_select_list"
606                );
607            }
608
609            columnSources.put(colName, source);
610        }
611    }
612
613    /**
614     * Find the source table for a result column's expression.
615     * For qualified column references like SUBS_CUST.FIRST_TP_ID, this returns
616     * the SUBS_CUST table from the FROM clause.
617     * For star columns like SUBSCR.*, this returns the SUBSCR table.
618     *
619     * <p>IMPORTANT: For subquery aliases, we return the subquery's TTable directly
620     * and do NOT trace through. The column should be attributed to the subquery
621     * alias (e.g., SUBS_CUST.FIRST_TP_ID), not to tables inside the subquery.</p>
622     *
623     * @param resultCol The result column to analyze
624     * @return The source table, or null if not determinable
625     */
626    private TTable findSourceTableForResultColumn(TResultColumn resultCol) {
627        if (resultCol == null || resultCol.getExpr() == null) {
628            return null;
629        }
630
631        gudusoft.gsqlparser.nodes.TExpression expr = resultCol.getExpr();
632
633        // Handle simple column reference (e.g., SUBS_CUST.FIRST_TP_ID or FIRST_TP_ID)
634        if (expr.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) {
635            gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand();
636            if (objName != null) {
637                String tablePrefix = objName.getTableString();
638                if (tablePrefix != null && !tablePrefix.isEmpty()) {
639                    // Qualified reference - find the table
640                    TTable table = findTableByAliasOrName(tablePrefix);
641                    if (table != null) {
642                        // Return the table directly - don't trace through subqueries
643                        // This keeps columns attributed to their immediate source
644                        // (e.g., SUBS_CUST.FIRST_TP_ID stays as SUBS_CUST.FIRST_TP_ID)
645                        return table;
646                    }
647                }
648            }
649        }
650
651        // Handle star column (e.g., SUBSCR.* or *)
652        String colStr = resultCol.toString().trim();
653        if (colStr.endsWith("*") && colStr.contains(".")) {
654            // Qualified star - find the table
655            int dotIndex = colStr.lastIndexOf('.');
656            if (dotIndex > 0) {
657                String tablePrefix = colStr.substring(0, dotIndex).trim();
658                TTable table = findTableByAliasOrName(tablePrefix);
659                if (table != null) {
660                    // For star columns, we DO trace through to get all underlying columns
661                    // This is different from regular columns because star needs to expand (use cached namespace)
662                    if (table.getSubquery() != null) {
663                        SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
664                            table.getSubquery(), table.getAliasName());
665                        return nestedNs != null ? nestedNs.getFinalTable() : null;
666                    }
667                    return table;
668                }
669            }
670        }
671
672        return null;
673    }
674
675    /**
676     * Extract column name from TResultColumn.
677     * Handles aliases and expression columns.
678     */
679    private String getColumnName(TResultColumn resultCol) {
680        // Check for alias
681        if (resultCol.getAliasClause() != null &&
682            resultCol.getAliasClause().getAliasName() != null) {
683            return resultCol.getAliasClause().getAliasName().toString();
684        }
685
686        // Check for simple column reference
687        if (resultCol.getExpr() != null) {
688            gudusoft.gsqlparser.nodes.TExpression expr = resultCol.getExpr();
689            // Check if it's a simple object reference
690            if (expr.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) {
691                gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand();
692                if (objName != null) {
693                    return objName.getColumnNameOnly();
694                }
695            }
696        }
697
698        // Complex expression - no name
699        return null;
700    }
701
702    @Override
703    public ColumnLevel hasColumn(String columnName) {
704        ensureValidated();
705
706        // Check in SELECT list (explicit columns)
707        for (String existingCol : columnSources.keySet()) {
708            if (nameMatcher.matches(existingCol, columnName)) {
709                return ColumnLevel.EXISTS;
710            }
711        }
712
713        // Check in inferred columns (from star push-down)
714        if (inferredColumns != null && inferredColumns.containsKey(columnName)) {
715            return ColumnLevel.EXISTS;
716        }
717
718        // If subquery has SELECT *, unknown columns MAYBE exist
719        // They need to be resolved through star push-down
720        if (hasStarColumn()) {
721            return ColumnLevel.MAYBE;
722        }
723
724        return ColumnLevel.NOT_EXISTS;
725    }
726
727    public TSelectSqlStatement getSubquery() {
728        return subquery;
729    }
730
731    @Override
732    public TSelectSqlStatement getSelectStatement() {
733        return subquery;
734    }
735
736    @Override
737    public boolean hasStarColumn() {
738        if (subquery == null || subquery.getResultColumnList() == null) {
739            return false;
740        }
741
742        TResultColumnList selectList = subquery.getResultColumnList();
743        for (int i = 0; i < selectList.size(); i++) {
744            TResultColumn resultCol = selectList.getResultColumn(i);
745            if (resultCol != null && resultCol.toString().endsWith("*")) {
746                return true;
747            }
748        }
749        return false;
750    }
751
752    /**
753     * Get the first star column (TResultColumn) from this subquery's SELECT list.
754     * Used to track the definition node for columns inferred from the star.
755     *
756     * @return The star column, or null if no star column exists
757     */
758    public TResultColumn getStarColumn() {
759        if (subquery == null || subquery.getResultColumnList() == null) {
760            return null;
761        }
762
763        TResultColumnList selectList = subquery.getResultColumnList();
764        for (int i = 0; i < selectList.size(); i++) {
765            TResultColumn resultCol = selectList.getResultColumn(i);
766            if (resultCol != null && resultCol.toString().endsWith("*")) {
767                return resultCol;
768            }
769        }
770        return null;
771    }
772
773    /**
774     * Check if this subquery has an unqualified star with multiple tables.
775     * In this case, columns are ambiguous and should NOT be auto-resolved.
776     *
777     * Example:
778     * SELECT * FROM table_a, table_c  -- ambiguous, columns could come from either table
779     * SELECT ta.* FROM table_a ta, table_c tc  -- NOT ambiguous, star is qualified
780     * SELECT * FROM table_a JOIN table_c ON ... -- ambiguous, columns from both tables
781     *
782     * Uses getRelations() and TJoinExpr to properly count tables in JOINs.
783     */
784    public boolean hasAmbiguousStar() {
785        if (subquery == null || subquery.getResultColumnList() == null) {
786            return false;
787        }
788
789        // Count all sources (tables, subqueries, joins) using getRelations()
790        int sourceCount = 0;
791        java.util.ArrayList<TTable> relations = subquery.getRelations();
792        if (relations != null) {
793            for (TTable table : relations) {
794                if (table == null) continue;
795                if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
796                    // For JOIN type, count tables from the JoinExpr
797                    List<TTable> joinTables = new ArrayList<>();
798                    collectPhysicalTablesFromJoinExpr(table.getJoinExpr(), joinTables);
799                    sourceCount += joinTables.size();
800                } else if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
801                    sourceCount++;
802                } else if (table.getSubquery() != null) {
803                    // Subqueries also count as sources
804                    sourceCount++;
805                }
806            }
807        }
808
809        // If only one source, not ambiguous
810        if (sourceCount <= 1) {
811            return false;
812        }
813
814        // Check for unqualified star (just "*" without table prefix)
815        TResultColumnList selectList = subquery.getResultColumnList();
816        for (int i = 0; i < selectList.size(); i++) {
817            TResultColumn resultCol = selectList.getResultColumn(i);
818            if (resultCol == null) continue;
819
820            String colStr = resultCol.toString().trim();
821            // Check if it's an unqualified star (just "*", not "ta.*")
822            if (colStr.equals("*")) {
823                return true;
824            }
825        }
826
827        return false;
828    }
829
830    /**
831     * Count the number of "real" tables in the FROM clause, excluding implicit lateral
832     * derived tables (Teradata feature where references to undeclared tables in WHERE
833     * clause create implicit table references).
834     *
835     * @return The count of real tables (excluding implicit lateral derived tables)
836     */
837    private int countRealTablesInFromClause() {
838        if (subquery == null || subquery.tables == null) {
839            return 0;
840        }
841        int count = 0;
842        for (int i = 0; i < subquery.tables.size(); i++) {
843            TTable table = subquery.tables.getTable(i);
844            if (table != null &&
845                table.getEffectType() != gudusoft.gsqlparser.ETableEffectType.tetImplicitLateralDerivedTable) {
846                count++;
847            }
848        }
849        return count;
850    }
851
852    /**
853     * Get the single real table from the FROM clause when there's exactly one.
854     * This is used as a fallback for star column push-down when resolveColumnInFromScope()
855     * can't find the column (e.g., no SQLEnv metadata).
856     *
857     * @return The single real table, or null if there's not exactly one
858     */
859    private TTable getSingleRealTableFromFromClause() {
860        if (subquery == null || subquery.tables == null) {
861            return null;
862        }
863        TTable result = null;
864        for (int i = 0; i < subquery.tables.size(); i++) {
865            TTable table = subquery.tables.getTable(i);
866            if (table != null &&
867                table.getEffectType() != gudusoft.gsqlparser.ETableEffectType.tetImplicitLateralDerivedTable) {
868                if (result != null) {
869                    // Multiple tables - can't determine which one
870                    return null;
871                }
872                result = table;
873            }
874        }
875        // If the single table is itself a subquery, trace through it (use cached namespace)
876        if (result != null && result.getSubquery() != null) {
877            SubqueryNamespace nestedNs = getOrCreateNestedNamespace(result.getSubquery(), result.getAliasName());
878            return nestedNs != null ? nestedNs.getFinalTable() : null;
879        }
880
881        // If the single table is a CTE reference, trace through the CTE to find the physical table
882        // This is critical for star column push-down when SELECT * FROM cte_name
883        if (result != null && result.isCTEName() && result.getCTE() != null) {
884            gudusoft.gsqlparser.nodes.TCTE cte = result.getCTE();
885            if (cte.getSubquery() != null) {
886                // Create a temporary CTENamespace to trace through
887                CTENamespace cteNs = new CTENamespace(
888                    cte,
889                    cte.getTableName() != null ? cte.getTableName().toString() : result.getName(),
890                    cte.getSubquery(),
891                    nameMatcher
892                );
893                cteNs.setReferencingTable(result);
894                cteNs.validate();
895                TTable tracedTable = cteNs.getFinalTable();
896                // Return the physical table if found, otherwise return the CTE table as fallback
897                return tracedTable != null ? tracedTable : result;
898            }
899        }
900
901        return result;
902    }
903
904    @Override
905    public boolean supportsDynamicInference() {
906        // Support dynamic inference if this subquery has SELECT *
907        return hasStarColumn();
908    }
909
910    @Override
911    public boolean addInferredColumn(String columnName, double confidence, String evidence) {
912        if (columnName == null || columnName.isEmpty()) {
913            return false;
914        }
915
916        // Initialize maps if needed
917        if (inferredColumns == null) {
918            inferredColumns = new LinkedHashMap<>();
919        }
920        if (inferredColumnNames == null) {
921            inferredColumnNames = new HashSet<>();
922        }
923
924        // Check if already exists in explicit columns
925        if (columnSources != null && columnSources.containsKey(columnName)) {
926            return false;
927        }
928
929        // Check if already inferred
930        if (inferredColumns.containsKey(columnName)) {
931            return false;
932        }
933
934        // For star columns (SELECT *), trace the column to its underlying table.
935        // This is critical for Teradata UPDATE...FROM...SET syntax where columns
936        // reference subqueries with SELECT *.
937        //
938        // IMPORTANT: Only trace when there's exactly ONE real table (excluding implicit
939        // lateral derived tables). If there are multiple tables, the column source
940        // is ambiguous unless the star is qualified (handled elsewhere).
941        TTable overrideTable = null;
942        if (hasStarColumn() && !hasAmbiguousStar()) {
943            // Count real tables (excluding implicit lateral derived tables)
944            int realTableCount = countRealTablesInFromClause();
945            if (realTableCount == 1) {
946                ColumnSource fromSource = resolveColumnInFromScope(columnName);
947                if (fromSource != null) {
948                    // Get the table from the underlying source
949                    overrideTable = fromSource.getFinalTable();
950                    if (overrideTable == null && fromSource.getSourceNamespace() instanceof TableNamespace) {
951                        // Try to get table directly from TableNamespace
952                        overrideTable = ((TableNamespace) fromSource.getSourceNamespace()).getTable();
953                    }
954                }
955
956                // Fallback: If resolveColumnInFromScope couldn't find the column (no SQLEnv metadata),
957                // but we have exactly one table with SELECT *, assume the column comes from that table.
958                // This is the expected behavior for star column push-down in most real-world scenarios
959                // where the column name is not provable but is inferred from the context.
960                if (overrideTable == null) {
961                    overrideTable = getSingleRealTableFromFromClause();
962                }
963            }
964        }
965
966        // Create inferred column source with overrideTable if found
967        // Note: Do NOT set the star column as definitionNode here.
968        // The definitionNode is used by the formatter to attribute columns to tables.
969        // Star-inferred columns should be attributed based on the overrideTable, not the star column.
970        // The legacy sourceColumn for star-inferred columns should remain null since they don't
971        // have a direct 1:1 relationship with a specific TResultColumn.
972        ColumnSource source = new ColumnSource(
973            this,
974            columnName,
975            null,  // No definition node for inferred columns
976            confidence,
977            evidence,
978            overrideTable
979        );
980
981        inferredColumns.put(columnName, source);
982        inferredColumnNames.add(columnName);
983        return true;
984    }
985
986    @Override
987    public Set<String> getInferredColumns() {
988        if (inferredColumnNames == null) {
989            return java.util.Collections.emptySet();
990        }
991        return java.util.Collections.unmodifiableSet(inferredColumnNames);
992    }
993
994    @Override
995    public ColumnSource resolveColumn(String columnName) {
996        ensureValidated();
997
998        // First check explicit columns from this subquery's SELECT list
999        ColumnSource source = super.resolveColumn(columnName);
1000        if (source != null) {
1001            return source;
1002        }
1003
1004        // Then check inferred columns
1005        if (inferredColumns != null) {
1006            for (Map.Entry<String, ColumnSource> entry : inferredColumns.entrySet()) {
1007                if (nameMatcher.matches(entry.getKey(), columnName)) {
1008                    return entry.getValue();
1009                }
1010            }
1011        }
1012
1013        // For ambiguous star cases, try to find the column in explicit subquery sources
1014        // This handles cases like: SELECT * FROM table_a ta, (SELECT col1 FROM table_c) tc
1015        // where col1 can be uniquely traced to tc -> table_c
1016        if (hasAmbiguousStar()) {
1017            ColumnSource explicitSource = findColumnInExplicitSources(columnName);
1018            if (explicitSource != null) {
1019                return explicitSource;
1020            }
1021
1022            // Try to trace the column through qualified references in the FROM clause
1023            // This handles JOINs where ON condition uses qualified names like table1.x
1024            ColumnSource tracedSource = traceColumnThroughQualifiedReferences(columnName);
1025            if (tracedSource != null) {
1026                return tracedSource;
1027            }
1028
1029            // Try to resolve the column using sqlenv metadata from the FROM clause tables
1030            // This uses TableNamespace.resolveColumn() which has access to sqlenv
1031            ColumnSource fromScopeSource = resolveColumnInFromScope(columnName);
1032            if (fromScopeSource != null) {
1033                return fromScopeSource;
1034            }
1035
1036            // Column not found in any explicit source, and we have multiple physical tables
1037            // Apply GUESS_COLUMN_STRATEGY to pick a table or leave unresolved
1038            return applyGuessColumnStrategy(columnName);
1039        }
1040
1041        // If hasColumn returns MAYBE (we have SELECT *), auto-infer this column
1042        // This enables on-demand inference during resolution
1043        if (hasStarColumn()) {
1044            // Add as inferred column with moderate confidence
1045            // The confidence is lower because we're inferring from outer reference
1046            boolean added = addInferredColumn(columnName, 0.8, "auto_inferred_from_outer_reference");
1047            if (added && inferredColumns != null) {
1048                ColumnSource inferredSource = inferredColumns.get(columnName);
1049                if (inferredSource != null) {
1050                    return inferredSource;
1051                }
1052            }
1053        }
1054
1055        return null;
1056    }
1057
1058    /**
1059     * Resolve a column in the FROM scope (child namespaces).
1060     * This finds where a column originates from in the FROM clause tables/subqueries.
1061     *
1062     * <p>Unlike resolveColumn() which returns columns from THIS subquery's SELECT list,
1063     * this method looks into the FROM clause to find the underlying definition.</p>
1064     *
1065     * <p>IMPORTANT: For TableNamespace, this only returns columns that exist in
1066     * actual metadata (from DDL or SQLEnv). It does NOT use inferred columns.
1067     * This is critical for multi-table star resolution where we need to know
1068     * which table actually has the column based on metadata, not inference.</p>
1069     *
1070     * @param columnName The column name to find
1071     * @return ColumnSource from the FROM clause, or null if not found
1072     */
1073    public ColumnSource resolveColumnInFromScope(String columnName) {
1074        if (subquery == null || subquery.tables == null || columnName == null) {
1075            return null;
1076        }
1077
1078        // Search through tables in the FROM clause
1079        for (int i = 0; i < subquery.tables.size(); i++) {
1080            TTable table = subquery.tables.getTable(i);
1081            if (table == null) continue;
1082
1083            // For subqueries, look in their column sources (use cached namespace)
1084            if (table.getSubquery() != null) {
1085                SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
1086                    table.getSubquery(), table.getAliasName());
1087                if (nestedNs != null) {
1088                    ColumnSource source = nestedNs.resolveColumn(columnName);
1089                    if (source != null) {
1090                        return source;
1091                    }
1092                }
1093            }
1094
1095            // For physical tables, create a TableNamespace and resolve (use cached namespace)
1096            // IMPORTANT: Only return column if the table has actual metadata (DDL or SQLEnv)
1097            // This prevents inferring columns for ambiguous star resolution
1098            if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1099                TableNamespace tableNs = getOrCreateTableNamespace(table);
1100                if (tableNs != null && tableNs.hasMetadata()) {
1101                    ColumnSource source = tableNs.resolveColumn(columnName);
1102                    if (source != null) {
1103                        return source;
1104                    }
1105                }
1106            }
1107
1108            // For joins, recursively search
1109            if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1110                ColumnSource source = resolveColumnInJoin(table.getJoinExpr(), columnName);
1111                if (source != null) {
1112                    return source;
1113                }
1114            }
1115        }
1116
1117        return null;
1118    }
1119
1120    /**
1121     * Resolve a column within a JOIN expression.
1122     * Only returns columns from tables with actual metadata (DDL or SQLEnv).
1123     */
1124    private ColumnSource resolveColumnInJoin(gudusoft.gsqlparser.nodes.TJoinExpr joinExpr, String columnName) {
1125        if (joinExpr == null) return null;
1126
1127        // Check left table (use cached namespaces)
1128        TTable leftTable = joinExpr.getLeftTable();
1129        if (leftTable != null) {
1130            if (leftTable.getSubquery() != null) {
1131                SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
1132                    leftTable.getSubquery(), leftTable.getAliasName());
1133                if (nestedNs != null) {
1134                    ColumnSource source = nestedNs.resolveColumn(columnName);
1135                    if (source != null) {
1136                        return source;
1137                    }
1138                }
1139            } else if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1140                TableNamespace tableNs = getOrCreateTableNamespace(leftTable);
1141                // Only return if table has metadata - don't use inferred columns
1142                if (tableNs != null && tableNs.hasMetadata()) {
1143                    ColumnSource source = tableNs.resolveColumn(columnName);
1144                    if (source != null) {
1145                        return source;
1146                    }
1147                }
1148            } else if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1149                ColumnSource source = resolveColumnInJoin(leftTable.getJoinExpr(), columnName);
1150                if (source != null) {
1151                    return source;
1152                }
1153            }
1154        }
1155
1156        // Check right table (use cached namespaces)
1157        TTable rightTable = joinExpr.getRightTable();
1158        if (rightTable != null) {
1159            if (rightTable.getSubquery() != null) {
1160                SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
1161                    rightTable.getSubquery(), rightTable.getAliasName());
1162                if (nestedNs != null) {
1163                    ColumnSource source = nestedNs.resolveColumn(columnName);
1164                    if (source != null) {
1165                        return source;
1166                    }
1167                }
1168            } else if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1169                TableNamespace tableNs = getOrCreateTableNamespace(rightTable);
1170                // Only return if table has metadata - don't use inferred columns
1171                if (tableNs != null && tableNs.hasMetadata()) {
1172                    ColumnSource source = tableNs.resolveColumn(columnName);
1173                    if (source != null) {
1174                        return source;
1175                    }
1176                }
1177            } else if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1178                ColumnSource source = resolveColumnInJoin(rightTable.getJoinExpr(), columnName);
1179                if (source != null) {
1180                    return source;
1181                }
1182            }
1183        }
1184
1185        return null;
1186    }
1187
1188    /**
1189     * Apply GUESS_COLUMN_STRATEGY to pick a table for an ambiguous column.
1190     * Used when a star column could come from multiple tables.
1191     *
1192     * For NOT_PICKUP strategy, returns a ColumnSource with all candidate tables
1193     * stored so end users can access them via getCandidateTables().
1194     *
1195     * @param columnName The column name to resolve
1196     * @return ColumnSource (may have multiple candidate tables for NOT_PICKUP)
1197     */
1198    private ColumnSource applyGuessColumnStrategy(String columnName) {
1199        // Get the strategy from instance field (which falls back to TBaseType if not set)
1200        int strategy = getGuessColumnStrategy();
1201
1202        // Collect all physical tables from the FROM clause
1203        List<TTable> physicalTables = new ArrayList<>();
1204        java.util.ArrayList<TTable> relations = subquery.getRelations();
1205        if (relations != null) {
1206            for (TTable table : relations) {
1207                if (table == null) continue;
1208                if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1209                    collectPhysicalTablesFromJoinExpr(table.getJoinExpr(), physicalTables);
1210                } else if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1211                    physicalTables.add(table);
1212                }
1213            }
1214        }
1215
1216        if (physicalTables.isEmpty()) {
1217            return null;
1218        }
1219
1220        if (strategy == gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY_NOT_PICKUP) {
1221            // Don't pick any table - return null to treat column as "missed"
1222            // This is the expected behavior for ambiguous star columns with NOT_PICKUP strategy
1223            if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1224                System.out.println("[GUESS_COLUMN_STRATEGY] Column '" + columnName +
1225                    "' is ambiguous across " + physicalTables.size() + " tables (NOT_PICKUP strategy - treating as missed)");
1226            }
1227            return null;
1228        }
1229
1230        // Pick the table based on strategy
1231        TTable pickedTable;
1232        String evidence;
1233        if (strategy == gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY_NEAREST) {
1234            // Pick the first table (nearest in FROM clause order)
1235            pickedTable = physicalTables.get(0);
1236            evidence = "guess_strategy_nearest";
1237        } else if (strategy == gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY_FARTHEST) {
1238            // Pick the last table (farthest in FROM clause order)
1239            pickedTable = physicalTables.get(physicalTables.size() - 1);
1240            evidence = "guess_strategy_farthest";
1241        } else {
1242            // Unknown strategy - don't pick
1243            return null;
1244        }
1245
1246        if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1247            System.out.println("[GUESS_COLUMN_STRATEGY] Column '" + columnName +
1248                "' picked from table '" + pickedTable.getName() + "' using " + evidence);
1249        }
1250
1251        // Create ColumnSource with the picked table
1252        return new ColumnSource(
1253            this,
1254            columnName,
1255            null,
1256            0.7,  // Lower confidence since it's a guess
1257            evidence,
1258            pickedTable  // Override table for getFinalTable()
1259        );
1260    }
1261
1262    /**
1263     * Find a column in explicit subquery sources within the FROM clause.
1264     * This is used for ambiguous star cases where we try to find the column
1265     * in subqueries that have explicit columns before giving up.
1266     */
1267    private ColumnSource findColumnInExplicitSources(String columnName) {
1268        if (subquery == null || subquery.tables == null) {
1269            return null;
1270        }
1271
1272        ColumnSource foundSource = null;
1273        List<TTable> physicalTables = new ArrayList<>();
1274
1275        for (int i = 0; i < subquery.tables.size(); i++) {
1276            TTable table = subquery.tables.getTable(i);
1277            if (table == null) continue;
1278
1279            // For join tables, collect physical tables from within the join
1280            if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1281                collectPhysicalTablesFromJoin(table, physicalTables);
1282                continue;
1283            }
1284
1285            // Check if this is a subquery with explicit columns
1286            if (table.getSubquery() != null) {
1287                TResultColumnList resultCols = table.getSubquery().getResultColumnList();
1288                if (resultCols != null) {
1289                    for (int j = 0; j < resultCols.size(); j++) {
1290                        TResultColumn rc = resultCols.getResultColumn(j);
1291                        String rcName = getResultColumnName(rc);
1292                        if (rcName != null && nameMatcher.matches(rcName, columnName)) {
1293                            // Found in this subquery - trace to its final table (use cached namespace)
1294                            SubqueryNamespace nestedNs = getOrCreateNestedNamespace(
1295                                table.getSubquery(), table.getAliasName());
1296                            if (nestedNs != null) {
1297                                ColumnSource nestedSource = nestedNs.resolveColumn(columnName);
1298                                if (nestedSource != null) {
1299                                    if (foundSource != null) {
1300                                        // Found in multiple sources - ambiguous
1301                                        return null;
1302                                    }
1303                                    foundSource = nestedSource;
1304                                }
1305                            }
1306                        }
1307                    }
1308                }
1309            } else if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1310                // Physical table - add to list
1311                physicalTables.add(table);
1312            }
1313        }
1314
1315        // If found in an explicit subquery source, return it
1316        if (foundSource != null) {
1317            return foundSource;
1318        }
1319
1320        // If column not found in any explicit source, and there's exactly one physical table,
1321        // infer from that table (use cached namespace)
1322        if (physicalTables.size() == 1) {
1323            // Create inferred column source from the single physical table
1324            TableNamespace tableNs = getOrCreateTableNamespace(physicalTables.get(0));
1325            return tableNs != null ? tableNs.resolveColumn(columnName) : null;
1326        }
1327
1328        return null;
1329    }
1330
1331    /**
1332     * Trace a column through qualified references in the subquery.
1333     * This looks for qualified column references (like table1.x) in the subquery
1334     * that match the requested column name, and returns a ColumnSource from the
1335     * corresponding table.
1336     *
1337     * Uses getRelations() and TJoinExpr to properly traverse JOIN structures.
1338     */
1339    private ColumnSource traceColumnThroughQualifiedReferences(String columnName) {
1340        if (subquery == null || columnName == null) {
1341            return null;
1342        }
1343
1344        // Debug logging
1345        if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1346            System.out.println("TRACE: traceColumnThroughQualifiedReferences called for column: " + columnName);
1347        }
1348
1349        // Collect all physical tables using getRelations() - the proper way to get all tables
1350        List<TTable> physicalTables = new ArrayList<>();
1351        java.util.ArrayList<TTable> relations = subquery.getRelations();
1352        if (relations != null) {
1353            for (TTable table : relations) {
1354                if (table == null) continue;
1355                if (table.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1356                    // For JOIN type, collect tables from the JoinExpr
1357                    collectPhysicalTablesFromJoinExpr(table.getJoinExpr(), physicalTables);
1358                } else if (table.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1359                    physicalTables.add(table);
1360                }
1361            }
1362        }
1363
1364        if (physicalTables.isEmpty()) {
1365            return null;
1366        }
1367
1368        // Look for qualified column references in JOIN ON conditions using TJoinExpr
1369        TTable matchedTable = null;
1370        int matchCount = 0;
1371
1372        // Check WHERE clause
1373        if (subquery.getWhereClause() != null && subquery.getWhereClause().getCondition() != null) {
1374            TTable found = findTableFromQualifiedColumnInExpression(
1375                subquery.getWhereClause().getCondition(), columnName, physicalTables);
1376            if (found != null) {
1377                matchedTable = found;
1378                matchCount++;
1379            }
1380        }
1381
1382        // Check JOIN conditions using getRelations() and TJoinExpr
1383        if (relations != null) {
1384            for (TTable table : relations) {
1385                if (table != null && table.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1386                    gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = table.getJoinExpr();
1387                    if (joinExpr != null) {
1388                        TTable found = findTableFromQualifiedColumnInJoinExpr(joinExpr, columnName, physicalTables);
1389                        if (found != null && (matchedTable == null || found != matchedTable)) {
1390                            if (matchedTable != null && found != matchedTable) {
1391                                matchCount++;
1392                            } else {
1393                                matchedTable = found;
1394                                matchCount = 1;
1395                            }
1396                        }
1397                    }
1398                }
1399            }
1400        }
1401
1402        // If found in exactly one table, return column source with SubqueryNamespace as source
1403        // The overrideTable ensures getFinalTable() returns the traced table
1404        if (matchCount == 1 && matchedTable != null) {
1405            if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1406                System.out.println("TRACE: Found column " + columnName + " in table: " + matchedTable.getName());
1407            }
1408            // Store the traced table mapping for reference
1409            if (tracedColumnTables == null) {
1410                tracedColumnTables = new java.util.LinkedHashMap<>();
1411            }
1412            tracedColumnTables.put(columnName, matchedTable);
1413
1414            // Return a ColumnSource with this SubqueryNamespace as source (for priority)
1415            // and the traced table as overrideTable (for correct getFinalTable())
1416            ColumnSource source = new ColumnSource(
1417                this,
1418                columnName,
1419                null,
1420                0.95,  // High confidence - traced through qualified reference
1421                "traced_through_qualified_ref",
1422                matchedTable  // Override table for getFinalTable()
1423            );
1424            return source;
1425        }
1426
1427        if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1428            System.out.println("TRACE: Column " + columnName + " NOT found in qualified references, matchCount=" + matchCount);
1429        }
1430        return null;
1431    }
1432
1433    /**
1434     * Collect physical tables from a TJoinExpr recursively.
1435     */
1436    private void collectPhysicalTablesFromJoinExpr(gudusoft.gsqlparser.nodes.TJoinExpr joinExpr, List<TTable> physicalTables) {
1437        if (joinExpr == null) return;
1438
1439        // Process left table
1440        TTable leftTable = joinExpr.getLeftTable();
1441        if (leftTable != null) {
1442            if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join && leftTable.getJoinExpr() != null) {
1443                collectPhysicalTablesFromJoinExpr(leftTable.getJoinExpr(), physicalTables);
1444            } else if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1445                physicalTables.add(leftTable);
1446            }
1447        }
1448
1449        // Process right table
1450        TTable rightTable = joinExpr.getRightTable();
1451        if (rightTable != null) {
1452            if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join && rightTable.getJoinExpr() != null) {
1453                collectPhysicalTablesFromJoinExpr(rightTable.getJoinExpr(), physicalTables);
1454            } else if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1455                physicalTables.add(rightTable);
1456            }
1457        }
1458    }
1459
1460    /**
1461     * Find table from qualified column references in a TJoinExpr and its nested joins.
1462     */
1463    private TTable findTableFromQualifiedColumnInJoinExpr(
1464            gudusoft.gsqlparser.nodes.TJoinExpr joinExpr,
1465            String columnName,
1466            List<TTable> physicalTables) {
1467        if (joinExpr == null) return null;
1468
1469        // Check ON condition of this join
1470        if (joinExpr.getOnCondition() != null) {
1471            if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1472                System.out.println("TRACE: Checking TJoinExpr ON condition: " + joinExpr.getOnCondition());
1473            }
1474            TTable found = findTableFromQualifiedColumnInExpression(
1475                joinExpr.getOnCondition(), columnName, physicalTables);
1476            if (found != null) return found;
1477        }
1478
1479        // Recursively check nested joins on the left side
1480        TTable leftTable = joinExpr.getLeftTable();
1481        if (leftTable != null && leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join
1482            && leftTable.getJoinExpr() != null) {
1483            TTable found = findTableFromQualifiedColumnInJoinExpr(leftTable.getJoinExpr(), columnName, physicalTables);
1484            if (found != null) return found;
1485        }
1486
1487        // Recursively check nested joins on the right side
1488        TTable rightTable = joinExpr.getRightTable();
1489        if (rightTable != null && rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join
1490            && rightTable.getJoinExpr() != null) {
1491            TTable found = findTableFromQualifiedColumnInJoinExpr(rightTable.getJoinExpr(), columnName, physicalTables);
1492            if (found != null) return found;
1493        }
1494
1495        return null;
1496    }
1497
1498    /**
1499     * Find table from qualified column references in an expression.
1500     * Looks for patterns like table1.column_name and returns the matching table.
1501     * Uses iterative DFS to avoid StackOverflowError for deeply nested expression chains.
1502     */
1503    private TTable findTableFromQualifiedColumnInExpression(
1504            gudusoft.gsqlparser.nodes.TExpression expr,
1505            String columnName,
1506            List<TTable> physicalTables) {
1507        if (expr == null) return null;
1508
1509        Deque<gudusoft.gsqlparser.nodes.TExpression> stack = new ArrayDeque<>();
1510        stack.push(expr);
1511        while (!stack.isEmpty()) {
1512            gudusoft.gsqlparser.nodes.TExpression current = stack.pop();
1513            if (current == null) continue;
1514
1515            // Check if this expression is a qualified column reference
1516            if (current.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) {
1517                gudusoft.gsqlparser.nodes.TObjectName objName = current.getObjectOperand();
1518                if (objName != null) {
1519                    String colName = objName.getColumnNameOnly();
1520                    String tablePrefix = objName.getTableString();
1521
1522                    if (colName != null && nameMatcher.matches(colName, columnName) &&
1523                        tablePrefix != null && !tablePrefix.isEmpty()) {
1524                        // Found a qualified reference to this column - find the matching table
1525                        for (TTable table : physicalTables) {
1526                            String tableName = table.getName();
1527                            String tableAlias = table.getAliasName();
1528
1529                            if ((tableName != null && nameMatcher.matches(tableName, tablePrefix)) ||
1530                                (tableAlias != null && nameMatcher.matches(tableAlias, tablePrefix))) {
1531                                return table;
1532                            }
1533                        }
1534                    }
1535                }
1536            }
1537
1538            // Push sub-expressions onto stack (right first so left is processed first)
1539            if (current.getRightOperand() != null) stack.push(current.getRightOperand());
1540            if (current.getLeftOperand() != null) stack.push(current.getLeftOperand());
1541        }
1542
1543        return null;
1544    }
1545
1546    /**
1547     * Find table from qualified column references in JOIN conditions.
1548     */
1549    private TTable findTableFromQualifiedColumnInJoin(
1550            TTable joinTable,
1551            String columnName,
1552            List<TTable> physicalTables) {
1553        if (joinTable == null) return null;
1554
1555        gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr();
1556        if (joinExpr == null) {
1557            if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1558                System.out.println("TRACE: findTableFromQualifiedColumnInJoin - joinExpr is null");
1559            }
1560            return null;
1561        }
1562
1563        // Check the ON condition of this join
1564        if (joinExpr.getOnCondition() != null) {
1565            if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1566                System.out.println("TRACE: Checking ON condition for column " + columnName + ": " + joinExpr.getOnCondition());
1567            }
1568            TTable found = findTableFromQualifiedColumnInExpression(
1569                joinExpr.getOnCondition(), columnName, physicalTables);
1570            if (found != null) return found;
1571        } else {
1572            if (gudusoft.gsqlparser.TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1573                System.out.println("TRACE: findTableFromQualifiedColumnInJoin - ON condition is null");
1574            }
1575        }
1576
1577        // Recursively check nested joins on the left side
1578        if (joinExpr.getLeftTable() != null &&
1579            joinExpr.getLeftTable().getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1580            TTable found = findTableFromQualifiedColumnInJoin(
1581                joinExpr.getLeftTable(), columnName, physicalTables);
1582            if (found != null) return found;
1583        }
1584
1585        // Recursively check nested joins on the right side
1586        if (joinExpr.getRightTable() != null &&
1587            joinExpr.getRightTable().getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1588            TTable found = findTableFromQualifiedColumnInJoin(
1589                joinExpr.getRightTable(), columnName, physicalTables);
1590            if (found != null) return found;
1591        }
1592
1593        return null;
1594    }
1595
1596    /**
1597     * Collect all physical tables from a JOIN expression recursively.
1598     */
1599    private void collectPhysicalTablesFromJoin(TTable joinTable, List<TTable> physicalTables) {
1600        if (joinTable == null) return;
1601
1602        gudusoft.gsqlparser.nodes.TJoinExpr joinExpr = joinTable.getJoinExpr();
1603        if (joinExpr == null) return;
1604
1605        // Process left side
1606        TTable leftTable = joinExpr.getLeftTable();
1607        if (leftTable != null) {
1608            if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1609                collectPhysicalTablesFromJoin(leftTable, physicalTables);
1610            } else if (leftTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1611                physicalTables.add(leftTable);
1612            } else if (leftTable.getSubquery() != null) {
1613                // For subqueries within joins, we could trace through but for now just note it exists
1614                physicalTables.add(leftTable);
1615            }
1616        }
1617
1618        // Process right side
1619        TTable rightTable = joinExpr.getRightTable();
1620        if (rightTable != null) {
1621            if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.join) {
1622                collectPhysicalTablesFromJoin(rightTable, physicalTables);
1623            } else if (rightTable.getTableType() == gudusoft.gsqlparser.ETableSource.objectname) {
1624                physicalTables.add(rightTable);
1625            } else if (rightTable.getSubquery() != null) {
1626                physicalTables.add(rightTable);
1627            }
1628        }
1629    }
1630
1631    /**
1632     * Get the name of a result column (either alias or column name)
1633     */
1634    private String getResultColumnName(TResultColumn rc) {
1635        if (rc == null) return null;
1636
1637        // Check for alias
1638        if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
1639            return rc.getAliasClause().getAliasName().toString();
1640        }
1641
1642        // Check for star - not a named column
1643        String colStr = rc.toString().trim();
1644        if (colStr.endsWith("*")) {
1645            return null;
1646        }
1647
1648        // Check for simple column reference
1649        if (rc.getExpr() != null) {
1650            gudusoft.gsqlparser.nodes.TExpression expr = rc.getExpr();
1651            if (expr.getExpressionType() == gudusoft.gsqlparser.EExpressionType.simple_object_name_t) {
1652                gudusoft.gsqlparser.nodes.TObjectName objName = expr.getObjectOperand();
1653                if (objName != null) {
1654                    return objName.getColumnNameOnly();
1655                }
1656            }
1657        }
1658
1659        return null;
1660    }
1661
1662    @Override
1663    public String toString() {
1664        int totalColumns = (columnSources != null ? columnSources.size() : 0) +
1665                          (inferredColumns != null ? inferredColumns.size() : 0);
1666        return "SubqueryNamespace(" + getDisplayName() + ", columns=" + totalColumns +
1667               ", inferred=" + (inferredColumns != null ? inferredColumns.size() : 0) + ")";
1668    }
1669}