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