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