001package gudusoft.gsqlparser.resolver2;
002
003import gudusoft.gsqlparser.TBaseType;
004import gudusoft.gsqlparser.TCustomSqlStatement;
005import gudusoft.gsqlparser.IRelation;
006import gudusoft.gsqlparser.TLog;
007import gudusoft.gsqlparser.TSourceToken;
008import gudusoft.gsqlparser.TStatementList;
009import gudusoft.gsqlparser.ETableSource;
010import gudusoft.gsqlparser.EDbVendor;
011import gudusoft.gsqlparser.EDbObjectType;
012import gudusoft.gsqlparser.EErrorType;
013import gudusoft.gsqlparser.ESqlClause;
014import gudusoft.gsqlparser.ESqlStatementType;
015import gudusoft.gsqlparser.TSyntaxError;
016import gudusoft.gsqlparser.stmt.dax.TDaxStmt;
017import gudusoft.gsqlparser.stmt.TAlterTableStatement;
018import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement;
019import gudusoft.gsqlparser.stmt.TInsertSqlStatement;
020import gudusoft.gsqlparser.stmt.TUpdateSqlStatement;
021import gudusoft.gsqlparser.stmt.TDeleteSqlStatement;
022import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
023import gudusoft.gsqlparser.compiler.TContext;
024import gudusoft.gsqlparser.nodes.TObjectName;
025import gudusoft.gsqlparser.nodes.TObjectNameList;
026import gudusoft.gsqlparser.nodes.TTable;
027import gudusoft.gsqlparser.nodes.TJoinExpr;
028import gudusoft.gsqlparser.nodes.TParseTreeNode;
029import gudusoft.gsqlparser.nodes.TParseTreeVisitor;
030import gudusoft.gsqlparser.nodes.TResultColumn;
031import gudusoft.gsqlparser.nodes.TResultColumnList;
032import gudusoft.gsqlparser.nodes.TQualifyClause;
033import gudusoft.gsqlparser.nodes.TExpression;
034import gudusoft.gsqlparser.EExpressionType;
035import gudusoft.gsqlparser.resolver2.model.ColumnSource;
036import gudusoft.gsqlparser.resolver2.model.FromScopeIndex;
037import gudusoft.gsqlparser.resolver2.model.ResolutionContext;
038import gudusoft.gsqlparser.resolver2.model.ResolutionResult;
039import gudusoft.gsqlparser.resolver2.model.ResolutionStatistics;
040import gudusoft.gsqlparser.resolver2.ResolutionStatus;
041import gudusoft.gsqlparser.resolver2.result.IResolutionResult;
042import gudusoft.gsqlparser.resolver2.result.ResolutionResultImpl;
043import gudusoft.gsqlparser.resolver2.scope.FromScope;
044import gudusoft.gsqlparser.resolver2.scope.GlobalScope;
045import gudusoft.gsqlparser.resolver2.scope.IScope;
046import gudusoft.gsqlparser.resolver2.scope.SelectScope;
047import gudusoft.gsqlparser.resolver2.scope.CTEScope;
048import gudusoft.gsqlparser.resolver2.scope.GroupByScope;
049import gudusoft.gsqlparser.resolver2.scope.HavingScope;
050import gudusoft.gsqlparser.resolver2.scope.OrderByScope;
051import gudusoft.gsqlparser.resolver2.scope.UpdateScope;
052import gudusoft.gsqlparser.resolver2.scope.DeleteScope;
053import gudusoft.gsqlparser.resolver2.namespace.INamespace;
054import gudusoft.gsqlparser.resolver2.namespace.TableNamespace;
055import gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace;
056import gudusoft.gsqlparser.resolver2.namespace.CTENamespace;
057import gudusoft.gsqlparser.nodes.TCTE;
058import gudusoft.gsqlparser.nodes.TCTEList;
059import gudusoft.gsqlparser.nodes.TUnnestClause;
060import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
061import gudusoft.gsqlparser.resolver2.iterative.ConvergenceDetector;
062import gudusoft.gsqlparser.resolver2.iterative.ResolutionPass;
063import gudusoft.gsqlparser.resolver2.enhancement.NamespaceEnhancer;
064import gudusoft.gsqlparser.resolver2.enhancement.EnhancementResult;
065import gudusoft.gsqlparser.resolver2.enhancement.CollectedColumnRef;
066import gudusoft.gsqlparser.resolver2.metadata.BatchMetadataCollector;
067import gudusoft.gsqlparser.resolver2.context.DatabaseContextTracker;
068import gudusoft.gsqlparser.resolver2.namespace.CTENamespace;
069import gudusoft.gsqlparser.sqlenv.TSQLEnv;
070import gudusoft.gsqlparser.TAttributeNode;
071
072import java.util.ArrayList;
073import java.util.HashMap;
074import java.util.HashSet;
075import java.util.IdentityHashMap;
076import java.util.List;
077import java.util.Map;
078import java.util.Set;
079
080// ScopeBuilder for visitor-based scope construction
081import gudusoft.gsqlparser.resolver2.ScopeBuilder;
082import gudusoft.gsqlparser.resolver2.ScopeBuildResult;
083
084/**
085 * New SQL Resolver - Phase 2 Enhanced Framework
086 *
087 * This is the main entry point for the new resolution architecture.
088 * Provides improved column-to-table resolution with:
089 * - Clear scope-based name resolution
090 * - Full candidate collection for ambiguous cases
091 * - Confidence-scored inference
092 * - Better tracing and debugging
093 *
094 * Usage:
095 * <pre>
096 * TSQLResolver2 resolver = new TSQLResolver2(context, statements);
097 * boolean success = resolver.resolve();
098 * ResolutionStatistics stats = resolver.getStatistics();
099 * </pre>
100 *
101 * Phase 1 capabilities:
102 * - Basic SELECT statement resolution
103 * - Table and subquery namespaces
104 * - Qualified and unqualified column references
105 * - FROM clause scope management
106 *
107 * Phase 2 capabilities:
108 * - JOIN scope handling with nullable semantics
109 * - CTE (WITH clause) resolution
110 * - Iterative resolution framework (auto-converges after first pass if no iteration needed)
111 *
112 * Future phases will add:
113 * - Evidence-based inference
114 * - Star column expansion
115 */
116public class TSQLResolver2 {
117
118    private final TContext globalContext;
119    private final TStatementList sqlStatements;
120    private final TSQLResolverConfig config;
121    private final ResolutionContext resolutionContext;
122    private final NameResolver nameResolver;
123
124    /** Global scope (root of scope tree) */
125    private GlobalScope globalScope;
126
127    /** Convergence detector for iterative resolution */
128    private ConvergenceDetector convergenceDetector;
129
130    /** History of all resolution passes */
131    private final List<ResolutionPass> passHistory;
132
133    /**
134     * Scope cache for iterative resolution.
135     * Maps statements to their scope trees to avoid rebuilding scopes on each pass.
136     * Key: TCustomSqlStatement, Value: SelectScope (or other scope type)
137     */
138    private final java.util.Map<Object, IScope> statementScopeCache;
139
140    /**
141     * Column-to-Scope mapping for iterative resolution (Principle 1: Scope完全复用).
142     * Built once in Pass 1, reused in Pass 2+ to avoid rebuilding scopes.
143     * Maps each TObjectName (column reference) to the IScope where it should be resolved.
144     */
145    private final java.util.Map<TObjectName, IScope> columnToScopeMap;
146
147    /**
148     * FromScope index cache for O(1) table/namespace lookups (Performance Optimization B).
149     * Maps FromScope instances to their pre-built indexes.
150     * Built lazily on first access, cleared at the start of each resolve() call.
151     * Uses IdentityHashMap because we need object identity, not equals().
152     */
153    private final Map<IScope, FromScopeIndex> fromScopeIndexCache;
154
155    /**
156     * Cache for Teradata NAMED alias lookup.
157     * Maps SELECT statements to their alias index (alias name -> TResultColumn).
158     * Uses IdentityHashMap because we need object identity, not equals().
159     * Optimization C: Reduces O(cols * select_items) to O(cols) for Teradata.
160     */
161    private final Map<TSelectSqlStatement, Map<String, TResultColumn>> teradataNamedAliasCache;
162
163    /**
164     * All column references collected during Pass 1 (Principle 1: Scope完全复用).
165     * Used in Pass 2+ to re-resolve names without rebuilding the scope tree.
166     */
167    private final List<TObjectName> allColumnReferences;
168
169    /**
170     * ScopeBuilder for visitor-based scope construction.
171     * Replaces manual scope building with proper nested scope handling.
172     */
173    private final ScopeBuilder scopeBuilder;
174
175    /**
176     * Result from ScopeBuilder containing the complete scope tree.
177     * This is populated in Pass 1 and reused in Pass 2+.
178     */
179    private ScopeBuildResult scopeBuildResult;
180
181    /**
182     * NamespaceEnhancer for explicit column collection and enhancement.
183     * Handles the explicit namespace enhancement phase between resolution passes.
184     * Columns are collected during resolution and added to namespaces explicitly.
185     */
186    private NamespaceEnhancer namespaceEnhancer;
187
188    /**
189     * Create resolver with default configuration
190     */
191    public TSQLResolver2(TContext context, TStatementList statements) {
192        this(context, statements, TSQLResolverConfig.createDefault());
193    }
194
195    /**
196     * Create resolver with custom configuration
197     */
198    public TSQLResolver2(TContext context, TStatementList statements, TSQLResolverConfig config) {
199        this.globalContext = context;
200        this.sqlStatements = statements;
201        this.config = config;
202        this.resolutionContext = new ResolutionContext();
203        this.nameResolver = new NameResolver(config, resolutionContext);
204        this.passHistory = new ArrayList<>();
205        this.statementScopeCache = new java.util.HashMap<>();
206        this.columnToScopeMap = new java.util.HashMap<>();
207        this.fromScopeIndexCache = new IdentityHashMap<>();
208        this.teradataNamedAliasCache = new IdentityHashMap<>();
209        this.allColumnReferences = new ArrayList<>();
210
211        // Initialize ScopeBuilder for visitor-based scope construction
212        this.scopeBuilder = new ScopeBuilder(context, config.getNameMatcher());
213        // Pass guessColumnStrategy from config for namespace isolation (prevents test side effects)
214        if (config.hasCustomGuessColumnStrategy()) {
215            this.scopeBuilder.setGuessColumnStrategy(config.getGuessColumnStrategy());
216        }
217
218        // If context is null, try to get TSQLEnv from statements
219        // This allows TSQLEnv to flow from parser.setSqlEnv() through statements
220        if (statements != null && statements.size() > 0) {
221            try {
222                TCustomSqlStatement firstStmt = statements.get(0);
223                if (firstStmt != null && firstStmt.getGlobalScope() != null &&
224                    firstStmt.getGlobalScope().getSqlEnv() != null) {
225                    this.scopeBuilder.setSqlEnv(firstStmt.getGlobalScope().getSqlEnv());
226                }
227            } catch (Exception e) {
228                // Silently ignore - SQLEnv is optional enhancement
229            }
230        }
231
232        // Initialize convergence detector for iterative resolution
233        this.convergenceDetector = new ConvergenceDetector(
234            config.getMaxIterations(),
235            config.getStablePassesForConvergence(),
236            config.getMinProgressRate()
237        );
238
239        // Initialize namespace enhancer for explicit column collection
240        // Debug mode follows the global resolver log setting
241        this.namespaceEnhancer = new NamespaceEnhancer(TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE);
242    }
243
244    /**
245     * Set the TSQLEnv to use for table metadata lookup.
246     * This allows external callers to provide TSQLEnv if automatic detection fails.
247     *
248     * @param sqlEnv the SQL environment containing table metadata
249     */
250    public void setSqlEnv(gudusoft.gsqlparser.sqlenv.TSQLEnv sqlEnv) {
251        if (scopeBuilder != null) {
252            scopeBuilder.setSqlEnv(sqlEnv);
253        }
254    }
255
256    /**
257     * Get the TSQLEnv used for table metadata lookup.
258     *
259     * @return the SQL environment, or null if not set
260     */
261    public gudusoft.gsqlparser.sqlenv.TSQLEnv getSqlEnv() {
262        return scopeBuilder != null ? scopeBuilder.getSqlEnv() : null;
263    }
264
265    /**
266     * Get the set of virtual trigger tables (deleted/inserted in SQL Server triggers).
267     * These tables should be excluded from table output since their columns are
268     * resolved to the trigger's target table.
269     *
270     * @return Set of TTable objects that are virtual trigger tables
271     */
272    public java.util.Set<gudusoft.gsqlparser.nodes.TTable> getVirtualTriggerTables() {
273        return scopeBuilder != null ? scopeBuilder.getVirtualTriggerTables() : java.util.Collections.emptySet();
274    }
275
276    /**
277     * Get the SQL statements being resolved.
278     *
279     * @return the list of SQL statements
280     */
281    public TStatementList getStatements() {
282        return sqlStatements;
283    }
284
285    // Performance timing fields (instance-level for single resolve() call)
286    private long timeScopeBuilder = 0;
287    private long timeNameResolution = 0;
288    private long timeEnhancement = 0;
289    private long timeLegacySync = 0;
290    private long timeOther = 0;
291
292    // Global accumulators for profiling across all resolve() calls
293    private static long globalTimeScopeBuilder = 0;
294    private static long globalTimeNameResolution = 0;
295    private static long globalTimeEnhancement = 0;
296    private static long globalTimeLegacySync = 0;
297    private static long globalTimeOther = 0;
298    private static int globalResolveCount = 0;
299
300    /**
301     * Reset global timing accumulators.
302     */
303    public static void resetGlobalTimings() {
304        globalTimeScopeBuilder = 0;
305        globalTimeNameResolution = 0;
306        globalTimeEnhancement = 0;
307        globalTimeLegacySync = 0;
308        globalTimeOther = 0;
309        globalResolveCount = 0;
310        // Reset detailed legacy sync timings
311        globalTimeClearLinked = 0;
312        globalTimeFillAttributes = 0;
313        globalTimeSyncColumns = 0;
314        globalTimePopulateOrphans = 0;
315        globalTimeClearHints = 0;
316    }
317
318    /**
319     * Get global performance timing breakdown for profiling across all resolve() calls.
320     * @return formatted timing information
321     */
322    public static String getGlobalPerformanceTimings() {
323        long total = globalTimeScopeBuilder + globalTimeNameResolution + globalTimeEnhancement + globalTimeLegacySync + globalTimeOther;
324        return String.format(
325            "TSQLResolver2 Global Timings (across %d resolve() calls):\n" +
326            "  ScopeBuilder: %d ms (%.1f%%)\n" +
327            "  NameResolution: %d ms (%.1f%%)\n" +
328            "  Enhancement: %d ms (%.1f%%)\n" +
329            "  LegacySync: %d ms (%.1f%%)\n" +
330            "  Other: %d ms (%.1f%%)\n" +
331            "  Total: %d ms",
332            globalResolveCount,
333            globalTimeScopeBuilder, total > 0 ? 100.0 * globalTimeScopeBuilder / total : 0,
334            globalTimeNameResolution, total > 0 ? 100.0 * globalTimeNameResolution / total : 0,
335            globalTimeEnhancement, total > 0 ? 100.0 * globalTimeEnhancement / total : 0,
336            globalTimeLegacySync, total > 0 ? 100.0 * globalTimeLegacySync / total : 0,
337            globalTimeOther, total > 0 ? 100.0 * globalTimeOther / total : 0,
338            total);
339    }
340
341    /**
342     * Get performance timing breakdown for profiling.
343     * @return formatted timing information
344     */
345    public String getPerformanceTimings() {
346        long total = timeScopeBuilder + timeNameResolution + timeEnhancement + timeLegacySync + timeOther;
347        return String.format(
348            "TSQLResolver2 Timings:\n" +
349            "  ScopeBuilder: %d ms (%.1f%%)\n" +
350            "  NameResolution: %d ms (%.1f%%)\n" +
351            "  Enhancement: %d ms (%.1f%%)\n" +
352            "  LegacySync: %d ms (%.1f%%)\n" +
353            "  Other: %d ms (%.1f%%)\n" +
354            "  Total: %d ms",
355            timeScopeBuilder, total > 0 ? 100.0 * timeScopeBuilder / total : 0,
356            timeNameResolution, total > 0 ? 100.0 * timeNameResolution / total : 0,
357            timeEnhancement, total > 0 ? 100.0 * timeEnhancement / total : 0,
358            timeLegacySync, total > 0 ? 100.0 * timeLegacySync / total : 0,
359            timeOther, total > 0 ? 100.0 * timeOther / total : 0,
360            total);
361    }
362
363    /**
364     * Perform resolution on all SQL statements
365     */
366    public boolean resolve() {
367        // Reset timing counters
368        timeScopeBuilder = 0;
369        timeNameResolution = 0;
370        timeEnhancement = 0;
371        timeLegacySync = 0;
372        timeOther = 0;
373
374        // Setup logging
375        TLog.clearLogs();
376        if (!TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
377            TLog.disableLog();
378        } else {
379            TLog.enableAllLevelLog();
380        }
381
382        try {
383            logInfo("Starting TSQLResolver2.resolve()");
384
385            long startTime = System.currentTimeMillis();
386
387            // Delta 1: Collect metadata from DDL statements if no SQLEnv provided
388            if (getSqlEnv() == null) {
389                collectBatchMetadata();
390            }
391
392            // Delta 4: Track database context from USE/SET statements
393            trackDatabaseContext();
394
395            // Phase 1: Build global scope (once for all passes)
396            buildGlobalScope();
397
398            timeOther += System.currentTimeMillis() - startTime;
399
400            // Phase 2: Perform iterative resolution
401            // (automatically completes after first pass if no second pass is needed)
402            return performIterativeResolution();
403
404        } catch (Exception e) {
405            logError("Exception in TSQLResolver2.resolve(): " + e.getMessage());
406            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
407                e.printStackTrace();
408            }
409            return false;
410        } finally {
411            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
412                TBaseType.dumpLogs(false);
413            }
414        }
415    }
416
417    /**
418     * Perform iterative resolution.
419     * Automatically converges after first pass if no additional passes are needed.
420     *
421     * Architecture:
422     * - Pass 1: Build scope tree + initial name resolution
423     * - Pass 2-N: Reuse scope tree, collect evidence, infer columns, re-resolve names
424     *
425     * This separation allows:
426     * 1. Scopes to accumulate inferred columns across iterations
427     * 2. Later scopes to reference earlier scopes' inferred columns
428     * 3. Forward references to be resolved in subsequent passes
429     */
430    private boolean performIterativeResolution() {
431        logInfo("Performing iterative resolution (max iterations: " + config.getMaxIterations() + ")");
432
433        int passNumber = 1;
434        ResolutionStatistics previousStats = null;
435        boolean continueIterating = true;
436        boolean scopesBuilt = false;
437
438        while (continueIterating) {
439            logInfo("=== Pass " + passNumber + " ===");
440
441            // Create a resolution pass
442            ResolutionPass pass = new ResolutionPass(passNumber, previousStats);
443
444            if (passNumber == 1) {
445                // ========== PASS 1: Build scope tree + initial resolution ==========
446                logInfo("Pass 1: Building scope tree using ScopeBuilder and performing initial resolution");
447
448                // Clear all state for fresh start
449                resolutionContext.clear();
450                columnToScopeMap.clear();
451                fromScopeIndexCache.clear();
452                dmlIndexCache.clear();
453                teradataNamedAliasCache.clear();
454                allColumnReferences.clear();
455
456                // Use ScopeBuilder to build complete scope tree (handles all nesting correctly)
457                long scopeBuilderStart = System.currentTimeMillis();
458                scopeBuildResult = scopeBuilder.build(sqlStatements);
459
460                // Get global scope from builder
461                globalScope = scopeBuildResult.getGlobalScope();
462
463                // Copy column references and scope mappings from ScopeBuildResult
464                columnToScopeMap.putAll(scopeBuildResult.getColumnToScopeMap());
465                allColumnReferences.addAll(scopeBuildResult.getAllColumnReferences());
466                timeScopeBuilder += System.currentTimeMillis() - scopeBuilderStart;
467
468                logInfo("ScopeBuilder complete: " + scopeBuildResult.getStatistics());
469                logInfo("Built " + scopeBuildResult.getStatementScopeMap().size() + " SelectScopes");
470
471                // Initialize NamespaceEnhancer with scope tree (caches star namespaces)
472                namespaceEnhancer.initialize(scopeBuildResult);
473                namespaceEnhancer.startPass(passNumber);
474
475                // Get SET clause target columns that should not be re-resolved
476                Set<TObjectName> setClauseTargetColumns = scopeBuilder.getSetClauseTargetColumns();
477
478                // Get INSERT ALL target columns that should not be re-resolved
479                Set<TObjectName> insertAllTargetColumns = scopeBuilder.getInsertAllTargetColumns();
480
481                // Perform initial name resolution for all collected columns
482                logInfo("Performing initial name resolution for " + allColumnReferences.size() + " column references");
483                long nameResStart = System.currentTimeMillis();
484                for (TObjectName objName : allColumnReferences) {
485                    // Skip SET clause target columns - they already have sourceTable correctly set
486                    // to the UPDATE target table and should NOT be resolved through star columns
487                    if (setClauseTargetColumns.contains(objName)) {
488                        continue;
489                    }
490
491                    // Skip INSERT ALL target columns - they already have sourceTable correctly set
492                    // to the INSERT target table and should NOT be resolved against the subquery scope
493                    if (insertAllTargetColumns.contains(objName)) {
494                        continue;
495                    }
496
497                    IScope scope = columnToScopeMap.get(objName);
498                    if (scope != null) {
499                        nameResolver.resolve(objName, scope);
500
501                        // Handle USING column priority for JOIN...USING syntax
502                        handleUsingColumnResolution(objName);
503
504                        // Handle Teradata NAMED alias resolution
505                        handleTeradataNamedAliasResolution(objName);
506                        handleQualifyClauseAliasResolution(objName);
507
508                        // Handle subquery aliased/calculated column resolution
509                        // Ensures aliased columns don't incorrectly trace to base tables
510                        handleSubqueryAliasedColumnResolution(objName);
511
512                        // Collect unresolved references for enhancement
513                        collectForEnhancementIfNeeded(objName, scope);
514                    }
515                }
516                timeNameResolution += System.currentTimeMillis() - nameResStart;
517
518                // Explicit Enhancement Phase: Add collected columns to namespaces
519                long enhanceStart = System.currentTimeMillis();
520                EnhancementResult enhanceResult = namespaceEnhancer.enhance();
521                timeEnhancement += System.currentTimeMillis() - enhanceStart;
522                logInfo("Pass 1 enhancement: " + enhanceResult.getTotalAdded() + " columns added to namespaces");
523
524                scopesBuilt = true;
525                logInfo("Pass 1 complete. Resolved " + allColumnReferences.size() + " column references.");
526
527
528            } else {
529                // ========== PASS 2+: Explicit Enhancement + Re-resolve ==========
530                logInfo("Pass " + passNumber + ": Explicit namespace enhancement and re-resolution");
531
532                // ======== Phase A: Start New Pass ========
533                namespaceEnhancer.startPass(passNumber);
534
535                // ======== Phase B: Clear Resolution Results (keep scopes!) ========
536                logInfo("Phase B: Clearing resolution results (scopes preserved)");
537                resolutionContext.clear();
538
539                // ======== Phase C: Re-resolve with Enhanced Namespaces ========
540                logInfo("Phase C: Re-resolving with enhanced namespaces");
541
542                // Get SET clause target columns that should not be re-resolved
543                Set<TObjectName> setClauseTargetColumns = scopeBuilder.getSetClauseTargetColumns();
544
545                // Get INSERT ALL target columns that should not be re-resolved
546                Set<TObjectName> insertAllTargetColumns = scopeBuilder.getInsertAllTargetColumns();
547
548                // Re-resolve all column references using their original scopes
549                // Scopes are reused from Pass 1, but namespaces may have been enhanced
550                for (TObjectName objName : allColumnReferences) {
551                    // Skip SET clause target columns - they already have sourceTable correctly set
552                    // to the UPDATE target table and should NOT be resolved through star columns
553                    if (setClauseTargetColumns.contains(objName)) {
554                        continue;
555                    }
556
557                    // Skip INSERT ALL target columns - they already have sourceTable correctly set
558                    // to the INSERT target table and should NOT be resolved against the subquery scope
559                    if (insertAllTargetColumns.contains(objName)) {
560                        continue;
561                    }
562
563                    IScope scope = columnToScopeMap.get(objName);
564                    if (scope != null) {
565                        nameResolver.resolve(objName, scope);
566
567                        // Handle USING column priority for JOIN...USING syntax
568                        handleUsingColumnResolution(objName);
569
570                        // Handle Teradata NAMED alias resolution
571                        handleTeradataNamedAliasResolution(objName);
572                        handleQualifyClauseAliasResolution(objName);
573
574                        // Handle subquery aliased/calculated column resolution
575                        // Ensures aliased columns don't incorrectly trace to base tables
576                        handleSubqueryAliasedColumnResolution(objName);
577
578                        // Collect for next enhancement pass if still targets star namespace
579                        collectForEnhancementIfNeeded(objName, scope);
580                    }
581                }
582
583                // ======== Phase D: Explicit Namespace Enhancement ========
584                logInfo("Phase D: Explicit namespace enhancement");
585                EnhancementResult enhanceResult = namespaceEnhancer.enhance();
586                logInfo("Pass " + passNumber + " enhancement: " +
587                       enhanceResult.getTotalAdded() + " columns added, " +
588                       enhanceResult.getTotalSkipped() + " skipped (existing)");
589
590                // Legacy support: also run old evidence collection (if needed)
591                if (config.isEvidenceCollectionEnabled()) {
592                    runLegacyEvidenceCollection();
593                }
594            }
595
596            // Get statistics after this pass
597            ResolutionStatistics currentStats = getStatistics();
598            pass.complete(currentStats);
599
600            // Record this pass
601            convergenceDetector.recordPass(pass);
602            passHistory.add(pass);
603
604            logInfo(pass.getSummary());
605
606            // Check convergence
607            ConvergenceDetector.ConvergenceResult convergence = convergenceDetector.checkConvergence();
608            if (convergence.hasConverged()) {
609                logInfo("Convergence detected: " + convergence.getReason());
610                pass.setStopReason(convergence.getReason());
611                continueIterating = false;
612            } else {
613                // Prepare for next pass
614                previousStats = currentStats;
615                passNumber++;
616            }
617        }
618
619        // Create cloned columns for star column tracing
620        // This is a CORE part of TSQLResolver2 - when a column traces through a CTE/subquery
621        // with SELECT * to a physical table, we create a cloned TObjectName with sourceTable
622        // pointing to the traced physical table. This ensures complete lineage tracking.
623        createTracedColumnClones();
624
625        // Sync to legacy structures if enabled
626        if (config.isLegacyCompatibilityEnabled()) {
627            long syncStart = System.currentTimeMillis();
628            syncToLegacyStructures();
629            timeLegacySync += System.currentTimeMillis() - syncStart;
630        }
631
632        // Print final statistics
633        logInfo("Iterative resolution complete after " + passHistory.size() + " passes");
634        ResolutionStatistics finalStats = getStatistics();
635        logInfo("Final statistics: " + finalStats);
636
637        // Print namespace enhancement summary if in debug mode
638        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
639            logInfo("=== Namespace Enhancement Summary ===");
640            logInfo("Total columns added: " + namespaceEnhancer.getTotalColumnsAdded());
641        }
642
643        // Print performance timing breakdown
644        logInfo(getPerformanceTimings());
645
646        // Accumulate to global timings for profiling
647        globalTimeScopeBuilder += timeScopeBuilder;
648        globalTimeNameResolution += timeNameResolution;
649        globalTimeEnhancement += timeEnhancement;
650        globalTimeLegacySync += timeLegacySync;
651        globalTimeOther += timeOther;
652        globalResolveCount++;
653
654        return true;
655    }
656
657    /**
658     * Run legacy evidence collection (deprecated).
659     * Kept for backward compatibility.
660     */
661    @SuppressWarnings("deprecation")
662    private void runLegacyEvidenceCollection() {
663        logInfo("Running legacy evidence collection (deprecated)");
664
665        gudusoft.gsqlparser.resolver2.inference.EvidenceCollector evidenceCollector =
666            new gudusoft.gsqlparser.resolver2.inference.EvidenceCollector();
667
668        int evidenceCount = 0;
669        for (int i = 0; i < sqlStatements.size(); i++) {
670            Object stmt = sqlStatements.get(i);
671            if (stmt instanceof TSelectSqlStatement) {
672                List<gudusoft.gsqlparser.resolver2.inference.InferenceEvidence> stmtEvidence =
673                    evidenceCollector.collectFromSelect((TSelectSqlStatement) stmt);
674                evidenceCount += stmtEvidence.size();
675            }
676        }
677
678        logInfo("Legacy evidence collection: " + evidenceCount + " items");
679    }
680
681    /**
682     * Get the namespace enhancer for external access to enhancement history.
683     *
684     * @return the namespace enhancer
685     */
686    public NamespaceEnhancer getNamespaceEnhancer() {
687        return namespaceEnhancer;
688    }
689
690    /**
691     * Get a detailed enhancement report.
692     *
693     * @return detailed report string
694     */
695    public String getEnhancementReport() {
696        return namespaceEnhancer.generateReport();
697    }
698
699    /**
700     * Re-process a statement for name resolution only (without rebuilding scopes).
701     * This is used in Pass 2+ to re-resolve names using enhanced scopes.
702     *
703     * CRITICAL (Principle 1: Scope完全复用):
704     * - Scope tree is built ONCE in Pass 1 and completely reused in Pass 2+
705     * - This method MUST NOT call processStatement() which rebuilds scopes
706     * - Instead, it iterates through allColumnReferences and re-resolves each
707     *   column using its original scope from columnToScopeMap
708     *
709     * This allows:
710     * - Namespaces to be enhanced across iterations (Principle 2)
711     * - Star columns to benefit from reverse inference (Principle 3)
712     * - All previous inference results to be preserved
713     */
714    private void reprocessStatementNamesOnly(Object statement) {
715        logDebug("Re-resolving column references without rebuilding scopes");
716
717        // Re-resolve all column references using their original scopes
718        // The scopes are reused from Pass 1, but their namespaces may have been enhanced
719        for (TObjectName objName : allColumnReferences) {
720            IScope scope = columnToScopeMap.get(objName);
721            if (scope != null) {
722                // Re-resolve this column using the (potentially enhanced) scope
723                nameResolver.resolve(objName, scope);
724
725                // Handle USING column priority for JOIN...USING syntax
726                handleUsingColumnResolution(objName);
727
728                // Handle Teradata NAMED alias resolution
729                handleTeradataNamedAliasResolution(objName);
730                handleQualifyClauseAliasResolution(objName);
731
732                // Collect for next enhancement pass if still unresolved
733                collectForEnhancementIfNeeded(objName, scope);
734            } else {
735                logError("No scope found for column: " + objName);
736            }
737        }
738    }
739
740    /**
741     * Handle special resolution for USING columns in JOIN...USING syntax.
742     * In "a JOIN table2 USING (id)", the USING column exists in BOTH tables.
743     * - The synthetic column (clone) resolves to the right-side table (table2)
744     * - The original USING column resolves to the left-side table (a)
745     *
746     * @param objName The column reference
747     */
748    private void handleUsingColumnResolution(TObjectName objName) {
749        if (objName == null || scopeBuildResult == null) return;
750
751        // Check if this is a synthetic USING column (should resolve to right table)
752        TTable rightTable = scopeBuildResult.getUsingColumnRightTable(objName);
753        if (rightTable != null) {
754            // This is the synthetic USING column - set its sourceTable to the right-side table
755            objName.setSourceTable(rightTable);
756
757            // Create a proper resolution with the right-side table
758            gudusoft.gsqlparser.resolver2.model.ColumnSource source =
759                new gudusoft.gsqlparser.resolver2.model.ColumnSource(
760                    null,  // no namespace for USING columns
761                    objName.getColumnNameOnly(),
762                    null,  // no definition node
763                    1.0,   // high confidence
764                    "using_column_right",
765                    rightTable  // override table - the right-side table of the JOIN
766                );
767            gudusoft.gsqlparser.resolver2.model.ResolutionResult result =
768                gudusoft.gsqlparser.resolver2.model.ResolutionResult.exactMatch(source);
769
770            // Update the TObjectName's resolution so formatter uses correct finalTable
771            objName.setResolution(result);
772
773            // Also register in ResolutionContext so getReferencesTo(table) can find it
774            resolutionContext.registerResolution(objName, result);
775
776            logDebug("USING column " + objName.getColumnNameOnly() +
777                " -> right-side table " + rightTable.getName());
778            return;
779        }
780
781        // Check if this is the original USING column (should resolve to left table)
782        TTable leftTable = scopeBuildResult.getUsingColumnLeftTable(objName);
783        if (leftTable != null) {
784            // This is the original USING column - set its sourceTable to the left-side table
785            objName.setSourceTable(leftTable);
786
787            // Create a proper resolution with the left-side table
788            gudusoft.gsqlparser.resolver2.model.ColumnSource source =
789                new gudusoft.gsqlparser.resolver2.model.ColumnSource(
790                    null,  // no namespace for USING columns
791                    objName.getColumnNameOnly(),
792                    null,  // no definition node
793                    1.0,   // high confidence
794                    "using_column_left",
795                    leftTable  // override table - the left-side table of the JOIN
796                );
797            gudusoft.gsqlparser.resolver2.model.ResolutionResult result =
798                gudusoft.gsqlparser.resolver2.model.ResolutionResult.exactMatch(source);
799
800            // Update the TObjectName's resolution so formatter uses correct finalTable
801            objName.setResolution(result);
802
803            // Also register in ResolutionContext so getReferencesTo(table) can find it
804            resolutionContext.registerResolution(objName, result);
805
806            logDebug("USING column " + objName.getColumnNameOnly() +
807                " -> left-side table " + leftTable.getName());
808        }
809    }
810
811    /**
812     * Handle Teradata NAMED alias resolution.
813     *
814     * <p>In Teradata, NAMED aliases defined in the SELECT list (using the {@code (NAMED alias)} syntax)
815     * can be referenced in the WHERE and QUALIFY clauses of the same SELECT statement. This is different
816     * from standard SQL where column aliases are only visible in ORDER BY.</p>
817     *
818     * <p>This method checks if a resolved column matches a NAMED alias from the enclosing SELECT list.
819     * If it does, the resolution is updated to indicate this is a calculated column (alias), not a
820     * physical column from the table.</p>
821     *
822     * <p>Example:</p>
823     * <pre>
824     * SELECT USI_ID, SUBS_ID,
825     *        (CAST(:param AS TIMESTAMP(0)))(NAMED REPORT_DTTM)
826     * FROM PRD2_ODW.SUBS_USI_HISTORY
827     * WHERE stime <= REPORT_DTTM AND etime > REPORT_DTTM
828     * </pre>
829     * <p>Here, REPORT_DTTM references in WHERE should NOT be linked to PRD2_ODW.SUBS_USI_HISTORY
830     * because REPORT_DTTM is a NAMED alias, not a physical column.</p>
831     *
832     * @param objName The column reference to check
833     */
834    private void handleTeradataNamedAliasResolution(TObjectName objName) {
835        if (objName == null || sqlStatements == null || sqlStatements.size() == 0) return;
836
837        // Only applies to Teradata
838        EDbVendor dbVendor = sqlStatements.get(0).dbvendor;
839        if (dbVendor != EDbVendor.dbvteradata) return;
840
841        String columnName = objName.getColumnNameOnly();
842        if (columnName == null || columnName.isEmpty()) return;
843
844        // Only apply to UNQUALIFIED column references (no table prefix)
845        // If a column has a table qualifier like "CP.CALC_PLATFORM_ID", it's clearly
846        // referencing a specific table's column, not a NAMED alias
847        if (objName.getTableToken() != null) return;
848
849        // Get the scope for this column reference
850        IScope scope = columnToScopeMap.get(objName);
851        if (scope == null) return;
852
853        // Find the enclosing SELECT statement from the scope
854        TSelectSqlStatement enclosingSelect = findEnclosingSelectFromScope(scope);
855        if (enclosingSelect == null) return;
856
857        // Optimization C: Use cached index for O(1) lookup instead of O(N) iteration
858        Map<String, TResultColumn> aliasIndex = getTeradataNamedAliasIndex(enclosingSelect);
859        if (aliasIndex == null || aliasIndex.isEmpty()) return;
860
861        // Look up the result column by alias name (case-insensitive, stored as lowercase)
862        TResultColumn resultCol = aliasIndex.get(columnName.toLowerCase());
863        if (resultCol == null) return;
864
865        // Skip if objName is part of this result column's expression
866        // This handles cases like "CAST(ID AS DECIMAL) AS ID" where the ID inside
867        // CAST is the source column, not a reference to the ID alias
868        if (isColumnWithinResultColumn(objName, resultCol)) {
869            return;
870        }
871
872        // Found a matching NAMED alias
873        // Clear the source table since this is an alias, not a physical column
874        objName.setSourceTable(null);
875
876        // Create a new ColumnSource with the TResultColumn as the definition node
877        // This will make isCalculatedColumn() return true
878        ColumnSource source = new ColumnSource(
879            null,  // namespace - not from a table
880            columnName,
881            resultCol,  // definition node - the TResultColumn with the alias
882            1.0,   // high confidence
883            "teradata_named_alias"
884        );
885        ResolutionResult result = ResolutionResult.exactMatch(source);
886        objName.setResolution(result);
887        resolutionContext.registerResolution(objName, result);
888
889        logDebug("Teradata NAMED alias: " + columnName + " -> alias from SELECT list");
890    }
891
892    /**
893     * Handle QUALIFY clause alias resolution for Snowflake, BigQuery, and Databricks.
894     *
895     * <p>In Snowflake, BigQuery, and Databricks, column aliases defined in the SELECT list
896     * can be referenced in the QUALIFY clause. This is different from standard SQL where
897     * column aliases are only visible in ORDER BY.</p>
898     *
899     * <p>This method checks if a column reference in the QUALIFY clause matches an alias
900     * from the enclosing SELECT list. If it does, the resolution is updated to indicate
901     * this is a calculated column (alias), not a physical column from the table.</p>
902     *
903     * <p>Example:</p>
904     * <pre>
905     * SELECT RoomNumber, RoomType, BlockFloor,
906     *        ROW_NUMBER() OVER (PARTITION BY RoomType ORDER BY BlockFloor) AS row_num
907     * FROM Hospital.Room
908     * QUALIFY row_num = 1
909     * </pre>
910     * <p>Here, row_num in QUALIFY should NOT be linked to Hospital.Room because
911     * row_num is an alias for the window function, not a physical column.</p>
912     *
913     * @param objName The column reference to check
914     */
915    private void handleQualifyClauseAliasResolution(TObjectName objName) {
916        if (objName == null || sqlStatements == null || sqlStatements.size() == 0) return;
917
918        // Only applies to databases that support QUALIFY with alias visibility
919        EDbVendor dbVendor = sqlStatements.get(0).dbvendor;
920        if (dbVendor != EDbVendor.dbvsnowflake &&
921            dbVendor != EDbVendor.dbvbigquery &&
922            dbVendor != EDbVendor.dbvdatabricks) return;
923
924        String columnName = objName.getColumnNameOnly();
925        if (columnName == null || columnName.isEmpty()) return;
926
927        // Only apply to UNQUALIFIED column references (no table prefix)
928        if (objName.getTableToken() != null) return;
929
930        // Check if this column is within a QUALIFY clause
931        if (!isInQualifyClause(objName)) return;
932
933        // Get the scope for this column reference
934        IScope scope = columnToScopeMap.get(objName);
935        if (scope == null) return;
936
937        // Find the enclosing SELECT statement from the scope
938        TSelectSqlStatement enclosingSelect = findEnclosingSelectFromScope(scope);
939        if (enclosingSelect == null) return;
940
941        // Look for a matching alias in the SELECT list
942        TResultColumnList resultColumns = enclosingSelect.getResultColumnList();
943        if (resultColumns == null || resultColumns.size() == 0) return;
944
945        TResultColumn matchingResultCol = null;
946        for (int i = 0; i < resultColumns.size(); i++) {
947            TResultColumn resultCol = resultColumns.getResultColumn(i);
948            if (resultCol == null) continue;
949
950            // Check if this result column has an alias matching the column name
951            if (resultCol.getAliasClause() != null &&
952                resultCol.getAliasClause().getAliasName() != null) {
953                String aliasName = resultCol.getAliasClause().getAliasName().toString();
954                if (aliasName != null && aliasName.equalsIgnoreCase(columnName)) {
955                    matchingResultCol = resultCol;
956                    break;
957                }
958            }
959        }
960
961        if (matchingResultCol == null) return;
962
963        // Found a matching alias - clear the source table since this is an alias, not a physical column
964        objName.setSourceTable(null);
965
966        // Create a new ColumnSource with the TResultColumn as the definition node
967        // This will make isCalculatedColumn() return true
968        ColumnSource source = new ColumnSource(
969            null,  // namespace - not from a table
970            columnName,
971            matchingResultCol,  // definition node - the TResultColumn with the alias
972            1.0,   // high confidence
973            "qualify_clause_alias"
974        );
975        ResolutionResult result = ResolutionResult.exactMatch(source);
976        objName.setResolution(result);
977        resolutionContext.registerResolution(objName, result);
978
979        logDebug("QUALIFY clause alias: " + columnName + " -> alias from SELECT list");
980    }
981
982    /**
983     * Check if a column reference is within a QUALIFY clause.
984     *
985     * @param objName The column reference to check
986     * @return true if the column is within a QUALIFY clause
987     */
988    private boolean isInQualifyClause(TObjectName objName) {
989        if (objName == null) return false;
990
991        // Get the column's scope to find the enclosing SELECT statement
992        IScope scope = columnToScopeMap.get(objName);
993        if (scope == null) return false;
994
995        TSelectSqlStatement enclosingSelect = findEnclosingSelectFromScope(scope);
996        if (enclosingSelect == null) return false;
997
998        // Check if this SELECT has a QUALIFY clause
999        TQualifyClause qualifyClause = enclosingSelect.getQualifyClause();
1000        if (qualifyClause == null) return false;
1001
1002        // Check if the column's token position is within the QUALIFY clause's range
1003        if (objName.getStartToken() != null && qualifyClause.getStartToken() != null &&
1004            qualifyClause.getEndToken() != null) {
1005            long objPos = objName.getStartToken().posinlist;
1006            long qualifyStart = qualifyClause.getStartToken().posinlist;
1007            long qualifyEnd = qualifyClause.getEndToken().posinlist;
1008
1009            return objPos >= qualifyStart && objPos <= qualifyEnd;
1010        }
1011
1012        return false;
1013    }
1014
1015    /**
1016     * Gets or builds the Teradata NAMED alias index for a SELECT statement.
1017     * Optimization C: Caches the alias map for O(1) lookup instead of O(N) iteration.
1018     *
1019     * @param selectStmt The SELECT statement to get/build the index for
1020     * @return Map from lowercase alias name to TResultColumn, or null if no aliases
1021     */
1022    private Map<String, TResultColumn> getTeradataNamedAliasIndex(TSelectSqlStatement selectStmt) {
1023        if (selectStmt == null) return null;
1024
1025        // Check cache first
1026        Map<String, TResultColumn> index = teradataNamedAliasCache.get(selectStmt);
1027        if (index != null) {
1028            return index;
1029        }
1030
1031        // Build index for this SELECT statement
1032        TResultColumnList resultColumns = selectStmt.getResultColumnList();
1033        if (resultColumns == null || resultColumns.size() == 0) {
1034            // Cache empty map to avoid rebuilding
1035            index = java.util.Collections.emptyMap();
1036            teradataNamedAliasCache.put(selectStmt, index);
1037            return index;
1038        }
1039
1040        index = new java.util.HashMap<>();
1041        for (int i = 0; i < resultColumns.size(); i++) {
1042            TResultColumn resultCol = resultColumns.getResultColumn(i);
1043            if (resultCol == null) continue;
1044
1045            // Check if this result column has a NAMED alias
1046            if (resultCol.getAliasClause() != null &&
1047                resultCol.getAliasClause().getAliasName() != null) {
1048                String aliasName = resultCol.getAliasClause().getAliasName().toString();
1049                if (aliasName != null && !aliasName.isEmpty()) {
1050                    // Store with lowercase key for case-insensitive matching
1051                    index.put(aliasName.toLowerCase(), resultCol);
1052                }
1053            }
1054        }
1055
1056        // Cache the index (even if empty, to avoid rebuilding)
1057        teradataNamedAliasCache.put(selectStmt, index);
1058
1059        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE && !index.isEmpty()) {
1060            logDebug("Built Teradata NAMED alias index for SELECT with " + index.size() + " aliases");
1061        }
1062
1063        return index;
1064    }
1065
1066    /**
1067     * Check if a column reference (TObjectName) is within a result column's expression.
1068     * This is used to prevent treating source columns in expressions like "CAST(ID AS DECIMAL) AS ID"
1069     * as references to the alias.
1070     *
1071     * @param objName The column reference to check
1072     * @param resultCol The result column to check against
1073     * @return true if objName is within resultCol's expression tree
1074     */
1075    private boolean isColumnWithinResultColumn(TObjectName objName, TResultColumn resultCol) {
1076        if (objName == null || resultCol == null) return false;
1077
1078        // Get the expression of the result column
1079        TExpression expr = resultCol.getExpr();
1080        if (expr == null) return false;
1081
1082        // Check by comparing start/end positions
1083        // If objName's position is within resultCol's expression, it's part of it
1084        long objStart = objName.getStartToken() != null ? objName.getStartToken().posinlist : -1;
1085        long objEnd = objName.getEndToken() != null ? objName.getEndToken().posinlist : -1;
1086        long exprStart = expr.getStartToken() != null ? expr.getStartToken().posinlist : -1;
1087        long exprEnd = expr.getEndToken() != null ? expr.getEndToken().posinlist : -1;
1088
1089        if (objStart >= 0 && exprStart >= 0 && objEnd >= 0 && exprEnd >= 0) {
1090            return objStart >= exprStart && objEnd <= exprEnd;
1091        }
1092
1093        return false;
1094    }
1095
1096    /**
1097     * Handle subquery aliased/calculated column resolution.
1098     *
1099     * <p>When a column reference resolves through a subquery (or CTE containing subqueries),
1100     * and the underlying column is an alias or calculated expression, we should NOT trace
1101     * it to the base table. This method ensures that such columns have their sourceTable
1102     * cleared to prevent incorrect attribution.</p>
1103     *
1104     * <p>This is essential for queries like:</p>
1105     * <pre>
1106     * WITH DataCTE AS (
1107     *     SELECT t.col, COUNT(*) AS cnt FROM table1 t ...
1108     * )
1109     * SELECT * FROM DataCTE
1110     * </pre>
1111     * <p>The 'cnt' column should NOT be traced to 'table1' because it's a calculated column.</p>
1112     *
1113     * @param objName The column reference to check
1114     */
1115    private void handleSubqueryAliasedColumnResolution(TObjectName objName) {
1116        if (objName == null) return;
1117
1118        // Check if column has a table qualifier pointing to a subquery/CTE
1119        // If so, we should KEEP the sourceTable link for lineage tracing
1120        // The qualifier explicitly tells us which subquery the column belongs to
1121        String tableQualifier = objName.getTableString();
1122        if (tableQualifier != null && !tableQualifier.isEmpty()) {
1123            IScope scope = columnToScopeMap.get(objName);
1124            if (scope != null) {
1125                TTable qualifiedTable = findTableByQualifier(scope, tableQualifier);
1126                if (qualifiedTable != null &&
1127                    (qualifiedTable.getSubquery() != null || qualifiedTable.getCTE() != null)) {
1128                    // Column has qualifier pointing to a subquery/CTE
1129                    // Keep the sourceTable link for lineage tracing (e.g., a.num_emp -> subquery a)
1130                    // Don't clear sourceTable - this link is correct and needed
1131                    logDebug("Subquery/CTE qualified column: " + objName.toString() +
1132                        " - keeping sourceTable link to " + tableQualifier);
1133                    return;
1134                }
1135            }
1136        }
1137
1138        // For unqualified columns (or columns qualified with base tables),
1139        // check if this is a calculated column or alias that should not trace to base tables
1140        ColumnSource source = objName.getColumnSource();
1141        if (source != null) {
1142            if (source.isCalculatedColumn() || source.isColumnAlias()) {
1143                TTable currentSource = objName.getSourceTable();
1144                if (currentSource != null) {
1145                    // Only clear if sourceTable is a base table (not subquery/CTE)
1146                    // For subquery/CTE references, keep the link for lineage tracing
1147                    if (currentSource.getSubquery() == null && currentSource.getCTE() == null) {
1148                        objName.setSourceTable(null);
1149                        logDebug("Calculated/alias column: " + objName.getColumnNameOnly() +
1150                            " cleared sourceTable (was " + currentSource.getName() + ") - not linked to base table");
1151                    }
1152                }
1153            }
1154        }
1155    }
1156
1157    /**
1158     * Gets or builds the FromScopeIndex for a scope (Performance Optimization B).
1159     *
1160     * <p>This method implements lazy initialization: the index is built on first access
1161     * and cached for subsequent lookups within the same resolution pass.</p>
1162     *
1163     * @param scope The scope to get the index for (SelectScope, UpdateScope, or FromScope)
1164     * @return The cached or newly built FromScopeIndex, or null if scope has no FROM clause
1165     */
1166    private FromScopeIndex getFromScopeIndex(IScope scope) {
1167        if (scope == null) {
1168            return null;
1169        }
1170
1171        // Get the actual FromScope to use as cache key
1172        IScope fromScope = null;
1173        if (scope instanceof SelectScope) {
1174            fromScope = ((SelectScope) scope).getFromScope();
1175        } else if (scope instanceof gudusoft.gsqlparser.resolver2.scope.UpdateScope) {
1176            fromScope = ((gudusoft.gsqlparser.resolver2.scope.UpdateScope) scope).getFromScope();
1177        } else if (scope instanceof FromScope) {
1178            fromScope = scope;
1179        }
1180
1181        if (fromScope == null) {
1182            return null;
1183        }
1184
1185        // Check cache first (lazy initialization)
1186        FromScopeIndex index = fromScopeIndexCache.get(fromScope);
1187        if (index == null) {
1188            // Build index and cache it
1189            index = new FromScopeIndex(fromScope.getChildren());
1190            fromScopeIndexCache.put(fromScope, index);
1191
1192            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1193                logDebug("Built FromScopeIndex for scope: " + index);
1194            }
1195        }
1196
1197        return index;
1198    }
1199
1200    /**
1201     * Find a table by its qualifier (alias or name) in the scope.
1202     * Uses FromScopeIndex for O(1) lookup instead of O(N) linear scan.
1203     */
1204    private TTable findTableByQualifier(IScope scope, String qualifier) {
1205        if (scope == null || qualifier == null) return null;
1206
1207        // Use indexed lookup (Performance Optimization B)
1208        FromScopeIndex index = getFromScopeIndex(scope);
1209        if (index != null) {
1210            return index.findTableByQualifier(qualifier);
1211        }
1212
1213        return null;
1214    }
1215
1216    /**
1217     * Check if a column name is an alias (not a passthrough column) in the subquery.
1218     */
1219    private boolean isColumnAnAliasInSubquery(TSelectSqlStatement subquery, String columnName) {
1220        if (subquery == null || columnName == null) return false;
1221
1222        TResultColumnList resultCols = subquery.getResultColumnList();
1223        if (resultCols == null) return false;
1224
1225        for (int i = 0; i < resultCols.size(); i++) {
1226            TResultColumn rc = resultCols.getResultColumn(i);
1227            if (rc == null) continue;
1228
1229            // Check if this result column has an alias matching the column name
1230            if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
1231                String alias = rc.getAliasClause().getAliasName().toString();
1232                if (alias != null && alias.equalsIgnoreCase(columnName)) {
1233                    // Found matching alias - check if it's a calculated column
1234                    TExpression expr = rc.getExpr();
1235                    if (expr != null) {
1236                        // Not a simple column reference = calculated
1237                        if (expr.getExpressionType() != EExpressionType.simple_object_name_t) {
1238                            return true;
1239                        }
1240                    }
1241                }
1242            }
1243
1244            // Also check for SQL Server proprietary alias syntax: alias = expr
1245            // In this case, the alias is the column name itself
1246            String colName = getResultColumnName(rc);
1247            if (colName != null && colName.equalsIgnoreCase(columnName)) {
1248                TExpression expr = rc.getExpr();
1249                if (expr != null && expr.getExpressionType() != EExpressionType.simple_object_name_t) {
1250                    return true;
1251                }
1252            }
1253        }
1254        return false;
1255    }
1256
1257    /**
1258     * Get the column name from a result column (handles aliases and SQL Server proprietary syntax).
1259     */
1260    private String getResultColumnName(TResultColumn rc) {
1261        if (rc == null) return null;
1262
1263        // Check for explicit alias
1264        if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
1265            return rc.getAliasClause().getAliasName().toString();
1266        }
1267
1268        // Check for SQL Server proprietary alias: alias = expr
1269        // In this case, the expression itself contains the alias
1270        TExpression expr = rc.getExpr();
1271        if (expr != null && expr.getExpressionType() == EExpressionType.assignment_t) {
1272            // The left side is the alias
1273            if (expr.getLeftOperand() != null && expr.getLeftOperand().getObjectOperand() != null) {
1274                return expr.getLeftOperand().getObjectOperand().toString();
1275            }
1276        }
1277
1278        return null;
1279    }
1280
1281    /**
1282     * Find the enclosing SELECT statement from a scope.
1283     * Traverses up the scope hierarchy to find a SelectScope and gets its node.
1284     *
1285     * @param scope The scope to start from
1286     * @return The enclosing SELECT statement, or null if not found
1287     */
1288    private TSelectSqlStatement findEnclosingSelectFromScope(IScope scope) {
1289        if (scope == null) return null;
1290
1291        IScope currentScope = scope;
1292        int maxIterations = 100; // Prevent infinite loops
1293        int iterations = 0;
1294
1295        while (currentScope != null && iterations < maxIterations) {
1296            iterations++;
1297
1298            // Check if current scope is a SelectScope
1299            if (currentScope instanceof SelectScope) {
1300                TParseTreeNode node = currentScope.getNode();
1301                if (node instanceof TSelectSqlStatement) {
1302                    return (TSelectSqlStatement) node;
1303                }
1304            }
1305
1306            // Move up to parent scope
1307            currentScope = currentScope.getParent();
1308        }
1309        return null;
1310    }
1311
1312    /**
1313     * Collect a column reference for namespace enhancement if it targets a star namespace.
1314     * This is called during resolution to gather columns that need to be added to namespaces.
1315     *
1316     * @param objName The column reference
1317     * @param scope The scope where the column should be resolved
1318     */
1319    private void collectForEnhancementIfNeeded(TObjectName objName, IScope scope) {
1320        if (objName == null || scope == null) return;
1321
1322        String columnName = objName.getColumnNameOnly();
1323        if (columnName == null || columnName.isEmpty()) return;
1324
1325        // Get the resolution result to check status
1326        gudusoft.gsqlparser.resolver2.model.ResolutionResult result = objName.getResolution();
1327
1328        // Find candidate namespace from scope's FROM clause
1329        INamespace candidateNamespace = findCandidateNamespace(objName, scope);
1330
1331        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1332            logInfo("[TSQLResolver2] collectForEnhancement: column=" + columnName +
1333                    ", candidateNs=" + (candidateNamespace != null ? candidateNamespace.getDisplayName() : "null") +
1334                    ", hasStar=" + (candidateNamespace != null ? candidateNamespace.hasStarColumn() : "N/A"));
1335        }
1336
1337        if (candidateNamespace != null) {
1338            // Determine confidence based on context
1339            double confidence = 0.7; // Default for unqualified reference
1340            String evidence = "outer_reference";
1341
1342            // Higher confidence for qualified references (e.g., "a.column")
1343            if (objName.getTableToken() != null) {
1344                confidence = 0.9;
1345                evidence = "qualified_reference";
1346            }
1347
1348            // Collect for enhancement
1349            namespaceEnhancer.collectColumnRef(
1350                columnName,
1351                candidateNamespace,
1352                objName,
1353                confidence,
1354                evidence
1355            );
1356        }
1357    }
1358
1359    /**
1360     * Find the candidate namespace for a column reference.
1361     * Looks at the scope's FROM clause to find namespaces with star columns.
1362     * Uses FromScopeIndex for O(1) lookup instead of O(N) linear scan.
1363     */
1364    private INamespace findCandidateNamespace(TObjectName objName, IScope scope) {
1365        // Use indexed lookup (Performance Optimization B)
1366        FromScopeIndex index = getFromScopeIndex(scope);
1367        if (index == null) {
1368            return null;
1369        }
1370
1371        String tablePrefix = objName.getTableToken() != null ?
1372            objName.getTableToken().toString() : null;
1373
1374        return index.findCandidateNamespace(tablePrefix);
1375    }
1376
1377    /**
1378     * Delta 1: Collect metadata from DDL statements in the batch.
1379     *
1380     * If no SQLEnv is provided, this method extracts table/column metadata
1381     * from CREATE TABLE and CREATE VIEW statements in the SQL batch and
1382     * creates a TSQLEnv for use during resolution.
1383     *
1384     * This enables standalone resolution of SQL batches that contain both
1385     * DDL and DML without requiring external metadata.
1386     */
1387    private void collectBatchMetadata() {
1388        if (sqlStatements == null || sqlStatements.size() == 0) {
1389            return;
1390        }
1391
1392        EDbVendor vendor = config != null ? config.getVendor() : EDbVendor.dbvmssql;
1393        BatchMetadataCollector collector = new BatchMetadataCollector(sqlStatements, vendor);
1394        TSQLEnv batchEnv = collector.collect();
1395
1396        if (batchEnv != null) {
1397            setSqlEnv(batchEnv);
1398            logDebug("Collected batch-local DDL metadata into TSQLEnv");
1399        }
1400    }
1401
1402    /**
1403     * Delta 4: Track database context from USE/SET statements.
1404     *
1405     * Scans the statement list for USE DATABASE, USE SCHEMA, SET SCHEMA,
1406     * and similar statements, and applies the context to TSQLEnv for
1407     * proper resolution of unqualified table names.
1408     */
1409    private void trackDatabaseContext() {
1410        if (sqlStatements == null || sqlStatements.size() == 0) {
1411            return;
1412        }
1413
1414        DatabaseContextTracker tracker = new DatabaseContextTracker();
1415        tracker.processStatements(sqlStatements);
1416
1417        // Apply context to TSQLEnv if any context was found
1418        if (tracker.hasContext()) {
1419            TSQLEnv env = getSqlEnv();
1420            if (env != null) {
1421                tracker.applyDefaults(env);
1422                logDebug("Applied database context: " + tracker);
1423            } else {
1424                // Create a minimal TSQLEnv if none exists
1425                EDbVendor vendor = config != null ? config.getVendor() : EDbVendor.dbvmssql;
1426                try {
1427                    env = new TSQLEnv(vendor) {
1428                        @Override
1429                        public void initSQLEnv() {
1430                            // Minimal initialization
1431                        }
1432                    };
1433                    tracker.applyDefaults(env);
1434                    setSqlEnv(env);
1435                    logDebug("Created minimal TSQLEnv with database context: " + tracker);
1436                } catch (Exception e) {
1437                    // TSQLEnv creation failed - context will not be applied
1438                    logDebug("Failed to create TSQLEnv for database context: " + e.getMessage());
1439                }
1440            }
1441        }
1442    }
1443
1444    /**
1445     * Build the global scope
1446     */
1447    private void buildGlobalScope() {
1448        logDebug("Building global scope");
1449
1450        // Get SQLEnv and vendor for qualified name resolution
1451        TSQLEnv sqlEnv = globalContext != null ? globalContext.getSqlEnv() : null;
1452        EDbVendor vendor = EDbVendor.dbvoracle; // Default
1453
1454        // Try to get vendor from statements
1455        if (sqlStatements != null && sqlStatements.size() > 0) {
1456            vendor = sqlStatements.get(0).dbvendor;
1457        }
1458
1459        // Create global scope with sqlEnv and vendor for proper qualified name resolution
1460        globalScope = new GlobalScope(globalContext, config.getNameMatcher(), sqlEnv, vendor);
1461
1462        logDebug("GlobalScope created with defaults: catalog=" +
1463                 globalScope.getDefaultCatalog() + ", schema=" + globalScope.getDefaultSchema());
1464    }
1465
1466    /**
1467     * Process a single statement
1468     */
1469    private void processStatement(Object statement) {
1470        if (statement instanceof TSelectSqlStatement) {
1471            processSelectStatement((TSelectSqlStatement) statement);
1472        }
1473        // TODO: Add support for INSERT, UPDATE, DELETE, etc.
1474    }
1475
1476    /**
1477     * Process a SELECT statement
1478     */
1479    private void processSelectStatement(TSelectSqlStatement select) {
1480        processSelectStatement(select, globalScope);
1481    }
1482
1483    /**
1484     * Process a SELECT statement with a specific parent scope.
1485     * This is used for recursive processing of CTE subqueries.
1486     */
1487    private void processSelectStatement(TSelectSqlStatement select, IScope givenParentScope) {
1488        logDebug("Processing SELECT statement");
1489
1490        // Create SELECT scope (will be child of CTE scope if CTEs exist, otherwise child of given parent scope)
1491        IScope parentScope = givenParentScope;
1492
1493        // Process CTEs (WITH clause) if present
1494        CTEScope cteScope = null;
1495        if (select.getCteList() != null && select.getCteList().size() > 0) {
1496            cteScope = processCTEs(select.getCteList(), givenParentScope);
1497            parentScope = cteScope;  // CTEs become parent of SELECT
1498        }
1499
1500        SelectScope selectScope = new SelectScope(parentScope, select);
1501
1502        // Process FROM clause
1503        if (select.tables != null && select.tables.size() > 0) {
1504            FromScope fromScope = processFromClause(select, selectScope);
1505            selectScope.setFromScope(fromScope);
1506        }
1507
1508        // Process column references in SELECT list
1509        if (select.getResultColumnList() != null) {
1510            List<TObjectName> selectListColumns = collectObjectNamesFromResultColumns(select.getResultColumnList());
1511            processColumnReferences(selectListColumns, selectScope);
1512        }
1513
1514        // Process WHERE clause
1515        if (select.getWhereClause() != null &&
1516            select.getWhereClause().getCondition() != null) {
1517            List<TObjectName> whereColumns = select.getWhereClause().getCondition().getColumnsInsideExpression();
1518            processColumnReferences(whereColumns, selectScope);
1519        }
1520
1521        // Process GROUP BY clause
1522        GroupByScope groupByScope = null;
1523        if (select.getGroupByClause() != null) {
1524            groupByScope = processGroupBy(select, selectScope);
1525        }
1526
1527        // Process HAVING clause
1528        if (select.getGroupByClause() != null &&
1529            select.getGroupByClause().getHavingClause() != null) {
1530            processHaving(select, selectScope, groupByScope);
1531        }
1532
1533        // Process ORDER BY clause
1534        if (select.getOrderbyClause() != null) {
1535            processOrderBy(select, selectScope);
1536        }
1537    }
1538
1539    /**
1540     * Process FROM clause and build FROM scope
1541     */
1542    private FromScope processFromClause(TSelectSqlStatement select, IScope parentScope) {
1543        FromScope fromScope = new FromScope(parentScope, select.tables);
1544
1545        // Process each relation (table or join)
1546        ArrayList<TTable> relations = select.getRelations();
1547        if (relations != null) {
1548            for (TTable table : relations) {
1549                processTableOrJoin(table, fromScope);
1550            }
1551        }
1552
1553        return fromScope;
1554    }
1555
1556    /**
1557     * Recursively process a table or join expression and add to FROM scope
1558     */
1559    private void processTableOrJoin(TTable table, FromScope fromScope) {
1560        if (table.getTableType() == ETableSource.join) {
1561            // This is a JOIN - recursively process left and right tables
1562            TJoinExpr joinExpr = table.getJoinExpr();
1563            if (joinExpr != null) {
1564                logDebug("Processing JOIN: " + joinExpr.getJointype());
1565
1566                // Recursively process left table
1567                TTable leftTable = joinExpr.getLeftTable();
1568                if (leftTable != null) {
1569                    processTableOrJoin(leftTable, fromScope);
1570                }
1571
1572                // Recursively process right table
1573                TTable rightTable = joinExpr.getRightTable();
1574                if (rightTable != null) {
1575                    processTableOrJoin(rightTable, fromScope);
1576                }
1577
1578                // TODO: Create JoinScope to handle nullable semantics
1579                // For now, we just add the base tables to FROM scope
1580            }
1581        } else {
1582            // This is a base table (objectname, subquery, etc.)
1583            INamespace namespace = createNamespaceForTable(table);
1584
1585            // Validate namespace (load metadata)
1586            namespace.validate();
1587
1588            // Determine alias
1589            String alias = table.getAliasName() != null
1590                ? table.getAliasName()
1591                : table.getName();
1592
1593            // Add to FROM scope
1594            fromScope.addChild(namespace, alias, false);
1595
1596            logDebug("Added table to FROM scope: " + alias);
1597        }
1598    }
1599
1600    /**
1601     * Process CTEs (WITH clause) and build CTE scope
1602     */
1603    private CTEScope processCTEs(TCTEList cteList, IScope parentScope) {
1604        CTEScope cteScope = new CTEScope(parentScope, cteList);
1605        logDebug("Processing WITH clause with " + cteList.size() + " CTE(s)");
1606
1607        // Process each CTE in order (later CTEs can reference earlier ones)
1608        for (int i = 0; i < cteList.size(); i++) {
1609            TCTE cte = cteList.getCTE(i);
1610
1611            // Get CTE name
1612            String cteName = cte.getTableName() != null ? cte.getTableName().toString() : null;
1613            if (cteName == null) {
1614                logDebug("Skipping CTE with null name");
1615                continue;
1616            }
1617
1618            // Get CTE subquery
1619            TSelectSqlStatement cteSubquery = cte.getSubquery();
1620            if (cteSubquery == null) {
1621                logDebug("Skipping CTE '" + cteName + "' with null subquery");
1622                continue;
1623            }
1624
1625            // Create CTENamespace
1626            CTENamespace cteNamespace = new CTENamespace(
1627                cte,
1628                cteName,
1629                cteSubquery,
1630                config.getNameMatcher()
1631            );
1632
1633            // Validate namespace (load column metadata from subquery)
1634            cteNamespace.validate();
1635
1636            // Add to CTE scope (makes it visible to later CTEs and main query)
1637            cteScope.addCTE(cteName, cteNamespace);
1638
1639            logDebug("Added CTE to scope: " + cteName +
1640                    " (columns=" + cteNamespace.getExplicitColumns().size() +
1641                    ", recursive=" + cteNamespace.isRecursive() + ")");
1642
1643            // Recursively process CTE subquery
1644            // This ensures that:
1645            // 1. Columns within the CTE are properly resolved
1646            // 2. Nested CTEs within this CTE are handled
1647            // 3. Later CTEs can reference this CTE's columns
1648            logDebug("Recursively processing CTE subquery: " + cteName);
1649            processSelectStatement(cteSubquery, cteScope);
1650        }
1651
1652        return cteScope;
1653    }
1654
1655    /**
1656     * Process GROUP BY clause and build GROUP BY scope
1657     */
1658    private GroupByScope processGroupBy(TSelectSqlStatement select, SelectScope selectScope) {
1659        GroupByScope groupByScope = new GroupByScope(selectScope, select.getGroupByClause());
1660        logDebug("Processing GROUP BY clause");
1661
1662        // Set the FROM scope for column resolution
1663        if (selectScope.getFromScope() != null) {
1664            groupByScope.setFromScope(selectScope.getFromScope());
1665        }
1666
1667        // Process column references in GROUP BY items
1668        if (select.getGroupByClause().getItems() != null) {
1669            for (int i = 0; i < select.getGroupByClause().getItems().size(); i++) {
1670                gudusoft.gsqlparser.nodes.TGroupByItem item = select.getGroupByClause().getItems().getGroupByItem(i);
1671                if (item.getExpr() != null) {
1672                    List<TObjectName> groupByColumns = item.getExpr().getColumnsInsideExpression();
1673                    processColumnReferences(groupByColumns, groupByScope);
1674                }
1675            }
1676        }
1677
1678        return groupByScope;
1679    }
1680
1681    /**
1682     * Process HAVING clause and build HAVING scope
1683     */
1684    private void processHaving(TSelectSqlStatement select, SelectScope selectScope, GroupByScope groupByScope) {
1685        logDebug("Processing HAVING clause");
1686
1687        HavingScope havingScope = new HavingScope(
1688            selectScope,
1689            select.getGroupByClause().getHavingClause()
1690        );
1691
1692        // Set GROUP BY scope for grouped column resolution
1693        if (groupByScope != null) {
1694            havingScope.setGroupByScope(groupByScope);
1695        }
1696
1697        // Set SELECT scope for alias resolution
1698        havingScope.setSelectScope(selectScope);
1699
1700        // Process column references in HAVING condition
1701        List<TObjectName> havingColumns = select.getGroupByClause().getHavingClause().getColumnsInsideExpression();
1702        processColumnReferences(havingColumns, havingScope);
1703    }
1704
1705    /**
1706     * Process ORDER BY clause and build ORDER BY scope
1707     */
1708    private void processOrderBy(TSelectSqlStatement select, SelectScope selectScope) {
1709        logDebug("Processing ORDER BY clause");
1710
1711        OrderByScope orderByScope = new OrderByScope(selectScope, select.getOrderbyClause());
1712
1713        // Set SELECT scope for alias resolution
1714        orderByScope.setSelectScope(selectScope);
1715
1716        // Set FROM scope for direct column resolution (database-dependent)
1717        if (selectScope.getFromScope() != null) {
1718            orderByScope.setFromScope(selectScope.getFromScope());
1719        }
1720
1721        // Process column references in ORDER BY items
1722        if (select.getOrderbyClause().getItems() != null) {
1723            for (int i = 0; i < select.getOrderbyClause().getItems().size(); i++) {
1724                gudusoft.gsqlparser.nodes.TOrderByItem item = select.getOrderbyClause().getItems().getOrderByItem(i);
1725                if (item.getSortKey() != null) {
1726                    List<TObjectName> orderByColumns = item.getSortKey().getColumnsInsideExpression();
1727                    processColumnReferences(orderByColumns, orderByScope);
1728                }
1729            }
1730        }
1731    }
1732
1733    /**
1734     * Create appropriate namespace for a table
1735     */
1736    private INamespace createNamespaceForTable(TTable table) {
1737        // Check if it's a subquery
1738        if (table.getSubquery() != null) {
1739            return new SubqueryNamespace(
1740                table.getSubquery(),
1741                table.getAliasName(),
1742                config.getNameMatcher()
1743            );
1744        }
1745
1746        // Regular table - pass sqlEnv and vendor for qualified name resolution
1747        TSQLEnv sqlEnv = globalContext != null ? globalContext.getSqlEnv() : null;
1748        EDbVendor vendor = table.dbvendor != null ? table.dbvendor : EDbVendor.dbvoracle;
1749        return new TableNamespace(table, config.getNameMatcher(), sqlEnv, vendor);
1750    }
1751
1752    /**
1753     * Collect all TObjectName from TResultColumnList
1754     */
1755    private List<TObjectName> collectObjectNamesFromResultColumns(
1756            gudusoft.gsqlparser.nodes.TResultColumnList resultColumns) {
1757        List<TObjectName> objNames = new ArrayList<>();
1758
1759        for (int i = 0; i < resultColumns.size(); i++) {
1760            gudusoft.gsqlparser.nodes.TResultColumn rc = resultColumns.getResultColumn(i);
1761            if (rc.getExpr() != null) {
1762                // Get all column references from the expression
1763                List<TObjectName> exprColumns = rc.getExpr().getColumnsInsideExpression();
1764                if (exprColumns != null) {
1765                    objNames.addAll(exprColumns);
1766                }
1767            }
1768        }
1769
1770        return objNames;
1771    }
1772
1773    /**
1774     * Process column references (TObjectName list)
1775     */
1776    private void processColumnReferences(List<TObjectName> objectNames, IScope scope) {
1777        if (objectNames == null) return;
1778
1779        for (TObjectName objName : objectNames) {
1780            // Record column-to-scope mapping for iterative resolution (Principle 1)
1781            columnToScopeMap.put(objName, scope);
1782            allColumnReferences.add(objName);
1783
1784            // Resolve the column reference
1785            nameResolver.resolve(objName, scope);
1786
1787            // Handle USING column priority for JOIN...USING syntax
1788            handleUsingColumnResolution(objName);
1789
1790            // Handle Teradata NAMED alias resolution
1791            handleTeradataNamedAliasResolution(objName);
1792            handleQualifyClauseAliasResolution(objName);
1793        }
1794    }
1795
1796    // Detailed legacy sync timing (for profiling)
1797    private static long globalTimeClearLinked = 0;
1798    private static long globalTimeFillAttributes = 0;
1799    private static long globalTimeSyncColumns = 0;
1800    private static long globalTimePopulateOrphans = 0;
1801    private static long globalTimeClearHints = 0;
1802
1803    /**
1804     * Get detailed legacy sync timing breakdown.
1805     */
1806    public static String getLegacySyncTimings() {
1807        long total = globalTimeClearLinked + globalTimeFillAttributes + globalTimeSyncColumns + globalTimePopulateOrphans + globalTimeClearHints;
1808        return String.format(
1809            "LegacySync Breakdown:\n" +
1810            "  ClearLinkedColumns: %d ms (%.1f%%)\n" +
1811            "  FillTableAttributes: %d ms (%.1f%%)\n" +
1812            "  SyncColumnToLegacy: %d ms (%.1f%%)\n" +
1813            "  PopulateOrphanColumns: %d ms (%.1f%%)\n" +
1814            "  ClearSyntaxHints: %d ms (%.1f%%)\n" +
1815            "  Total: %d ms",
1816            globalTimeClearLinked, total > 0 ? 100.0 * globalTimeClearLinked / total : 0,
1817            globalTimeFillAttributes, total > 0 ? 100.0 * globalTimeFillAttributes / total : 0,
1818            globalTimeSyncColumns, total > 0 ? 100.0 * globalTimeSyncColumns / total : 0,
1819            globalTimePopulateOrphans, total > 0 ? 100.0 * globalTimePopulateOrphans / total : 0,
1820            globalTimeClearHints, total > 0 ? 100.0 * globalTimeClearHints / total : 0,
1821            total);
1822    }
1823
1824    /**
1825     * Create cloned columns for star column tracing.
1826     *
1827     * <p>This is a CORE part of TSQLResolver2's name resolution. When a column traces
1828     * through a CTE or subquery with SELECT * to a physical table, we create a cloned
1829     * TObjectName with sourceTable pointing to the traced physical table.
1830     *
1831     * <p>Example:
1832     * <pre>
1833     * WITH cte AS (SELECT * FROM physical_table)
1834     * SELECT a FROM cte
1835     * </pre>
1836     *
1837     * <p>For column 'a' in the outer SELECT:
1838     * <ul>
1839     *   <li>Original column: sourceTable = cte (immediate source)</li>
1840     *   <li>Cloned column: sourceTable = physical_table (traced through star)</li>
1841     * </ul>
1842     *
1843     * <p>Both columns are added to allColumnReferences for complete lineage tracking.
1844     * This ensures the formatter can output both the immediate source and the traced
1845     * physical table when needed.
1846     */
1847    private void createTracedColumnClones() {
1848        // Collect clones to add (avoid ConcurrentModificationException)
1849        java.util.List<TObjectName> clonesToAdd = new java.util.ArrayList<>();
1850
1851        for (TObjectName column : allColumnReferences) {
1852            // Skip star columns - they represent all columns from a table and shouldn't be cloned
1853            String colName = column.getColumnNameOnly();
1854            if (colName != null && colName.equals("*")) {
1855                continue;
1856            }
1857
1858            // Skip columns without resolution
1859            gudusoft.gsqlparser.resolver2.model.ResolutionResult resolution = column.getResolution();
1860            if (resolution == null || !resolution.isExactMatch()) {
1861                continue;
1862            }
1863
1864            gudusoft.gsqlparser.resolver2.model.ColumnSource source = resolution.getColumnSource();
1865            if (source == null) {
1866                continue;
1867            }
1868
1869            TTable sourceTable = column.getSourceTable();
1870            if (sourceTable == null) {
1871                continue;
1872            }
1873
1874            // Only process CTE or subquery columns
1875            if (!sourceTable.isCTEName() && sourceTable.getTableType() != ETableSource.subquery) {
1876                continue;
1877            }
1878
1879            // Get the traced physical table
1880            TTable finalTable = source.getFinalTable();
1881            if (finalTable == null || finalTable == sourceTable) {
1882                continue;
1883            }
1884
1885            // Skip if finalTable is also a CTE or subquery
1886            if (finalTable.isCTEName() || finalTable.getTableType() == ETableSource.subquery) {
1887                continue;
1888            }
1889
1890            // Skip subquery columns when the column matches an explicit column in the subquery's
1891            // SELECT list. Cloning is only needed when tracing through star columns.
1892            // For example, in "SELECT al1.COL1, al1.COL3 FROM (SELECT t1.COL1, t2.* FROM t1, t2) al1":
1893            // - al1.COL1 matches explicit "t1.COL1" -> don't clone (stays at subquery level)
1894            // - al1.COL3 doesn't match explicit column, must come from t2.* -> clone to t2
1895            if (sourceTable.getTableType() == ETableSource.subquery) {
1896                TSelectSqlStatement subquery = sourceTable.getSubquery();
1897                if (subquery != null && subqueryHasExplicitColumn(subquery, colName)) {
1898                    continue;
1899                }
1900            }
1901
1902            // Skip UNION scenarios - syncToLegacyStructures already handles linking to all
1903            // UNION branch tables via getAllFinalTables(). Creating clones would cause duplicates.
1904            java.util.List<TTable> allFinalTables = source.getAllFinalTables();
1905            if (allFinalTables != null && allFinalTables.size() > 1) {
1906                continue;
1907            }
1908
1909            // Skip UNQUALIFIED join condition columns - they should not be traced to the source
1910            // subquery's underlying table via star column expansion.
1911            // This is particularly important for MERGE ON clause columns which may
1912            // belong to the target table rather than the source subquery.
1913            // QUALIFIED columns (like S.id) should still be traced as they explicitly reference
1914            // the source subquery.
1915            // Note: We check location only because ownStmt may be null for unresolved columns.
1916            if (column.getLocation() == ESqlClause.joinCondition
1917                    && (column.getTableString() == null || column.getTableString().isEmpty())) {
1918                if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1919                    logInfo("createTracedColumnClones: Skipping unqualified join condition column " + column.toString() +
1920                        " - should not be traced to subquery's underlying table");
1921                }
1922                continue;
1923            }
1924
1925            // Check if a column with same name and same finalTable already exists
1926            // (colName was already extracted above when checking for star columns)
1927            boolean alreadyExists = false;
1928            for (TObjectName existing : allColumnReferences) {
1929                if (existing.getSourceTable() == finalTable &&
1930                    colName != null && colName.equalsIgnoreCase(existing.getColumnNameOnly())) {
1931                    alreadyExists = true;
1932                    break;
1933                }
1934            }
1935            // Also check in clonesToAdd
1936            if (!alreadyExists) {
1937                for (TObjectName clone : clonesToAdd) {
1938                    if (clone.getSourceTable() == finalTable &&
1939                        colName != null && colName.equalsIgnoreCase(clone.getColumnNameOnly())) {
1940                        alreadyExists = true;
1941                        break;
1942                    }
1943                }
1944            }
1945
1946            if (!alreadyExists) {
1947                // Clone the column and set sourceTable to the traced physical table
1948                TObjectName clonedColumn = column.clone();
1949                clonedColumn.setSourceTable(finalTable);
1950                clonesToAdd.add(clonedColumn);
1951
1952                if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
1953                    logInfo("createTracedColumnClones: Cloned column " + column.toString() +
1954                        " with sourceTable traced from " + sourceTable.getTableName() +
1955                        " to physical table " + finalTable.getTableName());
1956                }
1957            }
1958        }
1959
1960        // Add all clones to allColumnReferences (local copy in TSQLResolver2)
1961        allColumnReferences.addAll(clonesToAdd);
1962
1963        // Also add to scopeBuildResult so consumers using scopeBuildResult.getAllColumnReferences()
1964        // (like TestGetTableColumn2 for star column expansion tests) can see the clones
1965        if (scopeBuildResult != null && !clonesToAdd.isEmpty()) {
1966            scopeBuildResult.addColumnReferences(clonesToAdd);
1967        }
1968
1969        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE && !clonesToAdd.isEmpty()) {
1970            logInfo("createTracedColumnClones: Created " + clonesToAdd.size() + " traced column clones");
1971        }
1972    }
1973
1974    /**
1975     * Sync results to legacy structures for backward compatibility.
1976     * This populates:
1977     * - TTable.linkedColumns: columns resolved to this table
1978     * - TObjectName.sourceTable: already set in setResolution()
1979     * - TObjectName.linkedColumnDef: from ColumnSource.definitionNode
1980     * - TObjectName.sourceColumn: from ColumnSource.definitionNode (if TResultColumn)
1981     */
1982    private void syncToLegacyStructures() {
1983        if (!config.isLegacyCompatibilityEnabled()) {
1984            logInfo("Legacy compatibility disabled, skipping sync");
1985            return;
1986        }
1987
1988        for (int i = 0; i < sqlStatements.size(); i++) {
1989        }
1990
1991        logInfo("Syncing to legacy structures...");
1992
1993        long phaseStart;
1994
1995        // Clear existing linkedColumns on all tables
1996        phaseStart = System.currentTimeMillis();
1997        clearAllLinkedColumns();
1998
1999        // Clear existing orphanColumns on all statements
2000        // These will be repopulated in Phase 4b based on TSQLResolver2 resolution
2001        for (int i = 0; i < sqlStatements.size(); i++) {
2002            clearOrphanColumnsRecursive(sqlStatements.get(i));
2003        }
2004        globalTimeClearLinked += System.currentTimeMillis() - phaseStart;
2005
2006        // Phase 1: Fill TTable.getAttributes() for all tables
2007        // This uses the namespace data already collected during resolution
2008        phaseStart = System.currentTimeMillis();
2009        Set<TTable> processedTables = new HashSet<>();
2010        for (int i = 0; i < sqlStatements.size(); i++) {
2011            fillTableAttributesRecursive(sqlStatements.get(i), processedTables);
2012        }
2013        globalTimeFillAttributes += System.currentTimeMillis() - phaseStart;
2014        logInfo("Filled attributes for " + processedTables.size() + " tables");
2015
2016        // Phase 2: Iterate through all column references and sync to legacy structures
2017        phaseStart = System.currentTimeMillis();
2018        int syncCount = 0;
2019        for (TObjectName column : allColumnReferences) {
2020            if (syncColumnToLegacy(column)) {
2021                syncCount++;
2022            }
2023        }
2024        globalTimeSyncColumns += System.currentTimeMillis() - phaseStart;
2025
2026        // Phase 3: Link CTAS target table columns
2027        // For CREATE TABLE AS SELECT, the SELECT list columns should be linked to the target table
2028        for (int i = 0; i < sqlStatements.size(); i++) {
2029            linkCTASTargetTableColumns(sqlStatements.get(i));
2030        }
2031
2032        // Phase 4: Sync implicit database/schema from USE DATABASE/USE SCHEMA to AST
2033        // This enables TObjectName.getAnsiSchemaName() and getAnsiCatalogName() to work correctly
2034        syncImplicitDbSchemaToAST();
2035
2036        // Phase 4b: Populate orphan columns
2037        // Columns with sourceTable=null (unresolved or ambiguous) should be added to
2038        // their containing statement's orphanColumns list. This enables TGetTableColumn
2039        // to report them as orphan columns (with linkOrphanColumnToFirstTable option).
2040        phaseStart = System.currentTimeMillis();
2041        populateOrphanColumns();
2042        globalTimePopulateOrphans += System.currentTimeMillis() - phaseStart;
2043
2044        // Phase 4c: Expand star columns using push-down inferred columns
2045        // For SELECT * and SELECT table.*, expand to individual columns based on:
2046        // 1. Inferred columns from the namespace (via push-down algorithm)
2047        // 2. This enables star column expansion without TSQLEnv metadata
2048        phaseStart = System.currentTimeMillis();
2049        expandStarColumnsUsingPushDown();
2050        long expandTime = System.currentTimeMillis() - phaseStart;
2051        logInfo("Star column expansion took " + expandTime + "ms");
2052
2053        // Phase 5: Clear orphan column syntax hints for resolved columns
2054        // The old resolver adds "sphint" (syntax hint) warnings for columns that can't be resolved.
2055        // TSQLResolver2 resolves these columns but doesn't clear the syntax hints.
2056        // This phase cleans up those hints to maintain compatibility with tests expecting no hints.
2057        phaseStart = System.currentTimeMillis();
2058        clearOrphanColumnSyntaxHints();
2059        globalTimeClearHints += System.currentTimeMillis() - phaseStart;
2060
2061        logInfo("Legacy sync complete: " + syncCount + "/" + allColumnReferences.size() + " columns synced");
2062    }
2063
2064    /**
2065     * Link SELECT list columns to CTAS target table.
2066     * For CREATE TABLE AS SELECT statements, the output column names (aliases)
2067     * should be linked to the target table. The source column references
2068     * remain linked to their source tables.
2069     *
2070     * NOTE: For CTAS, the parser (TCreateTableSqlStatement.doParseStatement) already
2071     * correctly creates and links alias columns to the target table. The source columns
2072     * that were incorrectly added are filtered out in clearLinkedColumnsRecursive().
2073     * This method now only handles cases where the parser didn't create alias columns.
2074     */
2075    private void linkCTASTargetTableColumns(TCustomSqlStatement stmt) {
2076        if (stmt == null) return;
2077
2078        // CTAS columns are already handled by the parser (TCreateTableSqlStatement.doParseStatement)
2079        // and incorrectly added source columns are filtered in clearLinkedColumnsRecursive().
2080        // No additional processing needed here for CTAS.
2081
2082        // Process nested statements (for other statement types that might need CTAS handling)
2083        for (int i = 0; i < stmt.getStatements().size(); i++) {
2084            linkCTASTargetTableColumns(stmt.getStatements().get(i));
2085        }
2086    }
2087
2088    /**
2089     * Populate orphanColumns for unresolved columns.
2090     * Columns with sourceTable=null should be added to their containing statement's orphanColumns.
2091     * This enables TGetTableColumn to report these as "missed" columns.
2092     */
2093    private void populateOrphanColumns() {
2094        int addedCount = 0;
2095        for (TObjectName column : allColumnReferences) {
2096            if (column == null) continue;
2097
2098            // Skip non-column types that should not be in orphan columns
2099            EDbObjectType dbObjectType = column.getDbObjectType();
2100            if (dbObjectType == EDbObjectType.column_alias    // alias clause column definitions (e.g., AS x (numbers, animals))
2101                || dbObjectType == EDbObjectType.variable     // stored procedure variables
2102                || dbObjectType == EDbObjectType.parameter    // stored procedure parameters
2103                || dbObjectType == EDbObjectType.cursor       // cursors
2104                || dbObjectType == EDbObjectType.constant     // constants
2105                || dbObjectType == EDbObjectType.label        // labels
2106            ) {
2107                continue;
2108            }
2109
2110            // Check resolution status directly - ambiguous columns should be added to orphanColumns
2111            // Note: column.getColumnSource() returns the first candidate for ambiguous columns,
2112            // which would cause them to be incorrectly skipped. We need to check the resolution status first.
2113            // IMPORTANT: This check must come BEFORE the sourceTable check because Phase 1 (linkColumnToTable)
2114            // might have already set sourceTable during parsing, but TSQLResolver2 correctly marked it as ambiguous.
2115            // NOTE: Skip star columns (*) since they are handled specially via sourceTableList
2116            ResolutionResult resolution = column.getResolution();
2117            String columnName = column.getColumnNameOnly();
2118            boolean isStarColumn = columnName != null && columnName.equals("*");
2119
2120            if (resolution != null && resolution.getStatus() == ResolutionStatus.AMBIGUOUS && !isStarColumn) {
2121                // Ambiguous columns should be added to orphanColumns so they appear as "missed"
2122                // Clear sourceTable if it was set by Phase 1 (linkColumnToTable) so the column
2123                // doesn't also appear as resolved in the output
2124                if (column.getSourceTable() != null) {
2125                    if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
2126                        logInfo("populateOrphanColumns: Clearing sourceTable for AMBIGUOUS column: " + column.toString()
2127                            + " at (" + column.getLineNo() + "," + column.getColumnNo() + ")"
2128                            + " was linked to " + column.getSourceTable().getTableName()
2129                            + " with " + (resolution.getAmbiguousSource() != null ?
2130                                resolution.getAmbiguousSource().getCandidateCount() : 0) + " candidates");
2131                    }
2132                    column.setSourceTable(null);
2133                }
2134                // Fall through to add to orphanColumns
2135            } else {
2136                // Star columns (*) should NEVER be orphan columns - they represent all columns
2137                // from all tables and are handled specially via sourceTableList and linked
2138                // to tables in syncColumnToLegacy() which runs after this phase.
2139                if (isStarColumn) {
2140                    continue;
2141                }
2142
2143                // For non-ambiguous columns, skip if they have a sourceTable
2144                if (column.getSourceTable() != null) {
2145                    continue;
2146                }
2147
2148                // Also skip columns that have a ColumnSource with a valid table
2149                ColumnSource source = column.getColumnSource();
2150                if (source != null) {
2151                    TTable finalTable = source.getFinalTable();
2152                    if (finalTable != null) {
2153                        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
2154                            logInfo("populateOrphanColumns: Skipping column with ColumnSource: " + column.toString()
2155                                + " at (" + column.getLineNo() + "," + column.getColumnNo() + ")"
2156                                + " -> resolved to " + finalTable.getTableName());
2157                        }
2158                        continue;
2159                    }
2160                    // Also check overrideTable for derived table columns
2161                    TTable overrideTable = source.getOverrideTable();
2162                    if (overrideTable != null) {
2163                        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
2164                            logInfo("populateOrphanColumns: Skipping column with ColumnSource (override): " + column.toString()
2165                                + " at (" + column.getLineNo() + "," + column.getColumnNo() + ")"
2166                                + " -> resolved to " + overrideTable.getTableName());
2167                        }
2168                        continue;
2169                    }
2170                }
2171            }
2172
2173            // Debug: log columns being added to orphan
2174            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
2175                ColumnSource debugSource = column.getColumnSource();
2176                logInfo("populateOrphanColumns: Adding orphan column: " + column.toString()
2177                    + " at (" + column.getLineNo() + "," + column.getColumnNo() + ")"
2178                    + ", hasColumnSource=" + (debugSource != null)
2179                    + (debugSource != null ? ", namespace=" + (debugSource.getSourceNamespace() != null ?
2180                        debugSource.getSourceNamespace().getClass().getSimpleName() : "null") : ""));
2181            }
2182
2183            // Find the containing statement for this column
2184            TCustomSqlStatement containingStmt = findContainingStatement(column);
2185            if (containingStmt != null) {
2186                // Set ownStmt so TSQLResolver2ResultFormatter can use getOwnStmt().getFirstPhysicalTable()
2187                // to link orphan columns to the first physical table (matching TGetTableColumn behavior)
2188                column.setOwnStmt(containingStmt);
2189
2190                TObjectNameList orphanColumns = containingStmt.getOrphanColumns();
2191                if (orphanColumns != null && !containsColumn(orphanColumns, column)) {
2192                    orphanColumns.addObjectName(column);
2193                    addedCount++;
2194                }
2195            } else {
2196                if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
2197                    logInfo("Could not find containing statement for orphan column: " + column.toString());
2198                }
2199            }
2200        }
2201        logInfo("Populated " + addedCount + " orphan columns");
2202    }
2203
2204    /**
2205     * Find the statement that contains a column reference.
2206     * First tries to use the scope information (more reliable), then falls back to AST traversal.
2207     * For PL/SQL blocks, searches for the innermost DML statement that contains the column.
2208     */
2209    private TCustomSqlStatement findContainingStatement(TObjectName column) {
2210        // First, try to use the scope information from columnToScopeMap
2211        // The scope's node is typically the containing statement
2212        IScope scope = columnToScopeMap.get(column);
2213        if (scope != null) {
2214            TParseTreeNode scopeNode = scope.getNode();
2215            if (scopeNode instanceof TCustomSqlStatement) {
2216                TCustomSqlStatement stmt = (TCustomSqlStatement) scopeNode;
2217                // If the scope is a PL/SQL block or procedure, search for DML statements within it
2218                // that actually contain the column (by line number)
2219                if (isPLSQLBlockStatement(stmt)) {
2220                    TCustomSqlStatement dmlStmt = findDMLStatementContaining(stmt, column);
2221                    if (dmlStmt != null) {
2222                        return dmlStmt;
2223                    }
2224                }
2225                return stmt;
2226            }
2227        }
2228
2229        // Fallback: traverse up the AST to find the nearest TCustomSqlStatement parent
2230        TParseTreeNode node = column;
2231        while (node != null) {
2232            if (node instanceof TCustomSqlStatement) {
2233                return (TCustomSqlStatement) node;
2234            }
2235            node = node.getParentObjectName();
2236        }
2237
2238        // Last resort: search all statements for a DML statement containing the column
2239        TCustomSqlStatement result = null;
2240        if (sqlStatements.size() > 0) {
2241            for (int i = 0; i < sqlStatements.size(); i++) {
2242                TCustomSqlStatement stmt = sqlStatements.get(i);
2243                TCustomSqlStatement dmlStmt = findDMLStatementContaining(stmt, column);
2244                if (dmlStmt != null) {
2245                    result = dmlStmt;
2246                    break;
2247                }
2248            }
2249            if (result == null) {
2250                result = sqlStatements.get(0);
2251            }
2252        }
2253        return result;
2254    }
2255
2256    /**
2257     * Check if a statement is a PL/SQL block type statement.
2258     */
2259    private boolean isPLSQLBlockStatement(TCustomSqlStatement stmt) {
2260        if (stmt == null) return false;
2261        String className = stmt.getClass().getSimpleName();
2262        return className.startsWith("TPlsql") || className.startsWith("TPLSql") ||
2263               className.contains("Block") || className.contains("Procedure") ||
2264               className.contains("Function") || className.contains("Package");
2265    }
2266
2267    /**
2268     * DML Statement Range for efficient line-based lookup.
2269     * Used by the DML index cache (Performance Optimization A).
2270     */
2271    private static class DmlRange implements Comparable<DmlRange> {
2272        final long startLine;
2273        final long endLine;
2274        final TCustomSqlStatement stmt;
2275
2276        DmlRange(TCustomSqlStatement stmt) {
2277            this.stmt = stmt;
2278            this.startLine = stmt.getStartToken() != null ? stmt.getStartToken().lineNo : -1;
2279            this.endLine = stmt.getEndToken() != null ? stmt.getEndToken().lineNo : -1;
2280        }
2281
2282        boolean contains(long line) {
2283            return startLine >= 0 && startLine <= line && line <= endLine;
2284        }
2285
2286        // Sort by startLine for binary search
2287        @Override
2288        public int compareTo(DmlRange other) {
2289            return Long.compare(this.startLine, other.startLine);
2290        }
2291    }
2292
2293    /**
2294     * Cache for DML statement ranges per parent statement (Performance Optimization A).
2295     * Built lazily on first access, cleared at start of each resolve() call.
2296     * Uses IdentityHashMap because we need object identity, not equals().
2297     */
2298    private final Map<TCustomSqlStatement, List<DmlRange>> dmlIndexCache = new IdentityHashMap<>();
2299
2300    /**
2301     * Build DML index for a parent statement.
2302     */
2303    private List<DmlRange> buildDmlIndex(TCustomSqlStatement parent) {
2304        final List<DmlRange> ranges = new ArrayList<>();
2305        parent.acceptChildren(new TParseTreeVisitor() {
2306            @Override
2307            public void preVisit(TInsertSqlStatement stmt) {
2308                ranges.add(new DmlRange(stmt));
2309            }
2310            @Override
2311            public void preVisit(TUpdateSqlStatement stmt) {
2312                ranges.add(new DmlRange(stmt));
2313            }
2314            @Override
2315            public void preVisit(TDeleteSqlStatement stmt) {
2316                ranges.add(new DmlRange(stmt));
2317            }
2318            @Override
2319            public void preVisit(TSelectSqlStatement stmt) {
2320                ranges.add(new DmlRange(stmt));
2321            }
2322        });
2323        // Sort by startLine for efficient lookup
2324        java.util.Collections.sort(ranges);
2325        return ranges;
2326    }
2327
2328    /**
2329     * Get or build the DML index for a parent statement (Performance Optimization A).
2330     */
2331    private List<DmlRange> getDmlIndex(TCustomSqlStatement parent) {
2332        return dmlIndexCache.computeIfAbsent(parent, this::buildDmlIndex);
2333    }
2334
2335    /**
2336     * Find the innermost DML statement (INSERT/UPDATE/DELETE/SELECT) within a parent statement
2337     * that contains the given column reference (by line number range).
2338     * Uses cached DML index for O(log N) lookup instead of O(N) traversal.
2339     */
2340    private TCustomSqlStatement findDMLStatementContaining(TCustomSqlStatement parent, TObjectName column) {
2341        if (parent == null || column == null) return null;
2342
2343        long columnLine = column.getLineNo();
2344        TCustomSqlStatement result = null;
2345
2346        // Use cached DML index (Performance Optimization A)
2347        List<DmlRange> ranges = getDmlIndex(parent);
2348
2349        // Find all DML statements that contain the column by line number
2350        // Need to check all ranges that could contain the column (can't use pure binary search
2351        // because ranges can overlap and we want the innermost one)
2352        for (DmlRange range : ranges) {
2353            // Optimization: if startLine > columnLine, no more ranges can contain it
2354            if (range.startLine > columnLine) {
2355                break;
2356            }
2357            if (range.contains(columnLine)) {
2358                // Found a matching DML statement - prefer the innermost one (later startLine)
2359                if (result == null ||
2360                    (range.startLine >= result.getStartToken().lineNo)) {
2361                    result = range.stmt;
2362                }
2363            }
2364        }
2365
2366        return result;
2367    }
2368
2369    /**
2370     * Sync implicit database/schema from USE DATABASE/USE SCHEMA statements to AST.
2371     * This enables TObjectName.getAnsiSchemaName() and getAnsiCatalogName() to work correctly
2372     * for unqualified object names.
2373     *
2374     * This is similar to what TDatabaseObjectResolver does in the legacy resolver:
2375     * it visits all TObjectName nodes and sets implicitDatabaseName/implicitSchemaName
2376     * based on the current database/schema context.
2377     */
2378    private void syncImplicitDbSchemaToAST() {
2379        // Get the tracked database context
2380        TSQLEnv env = getSqlEnv();
2381        if (env == null) {
2382            return;
2383        }
2384
2385        String defaultCatalog = env.getDefaultCatalogName();
2386        String defaultSchema = env.getDefaultSchemaName();
2387
2388        // If no defaults are set, nothing to sync
2389        if ((defaultCatalog == null || defaultCatalog.isEmpty()) &&
2390            (defaultSchema == null || defaultSchema.isEmpty())) {
2391            return;
2392        }
2393
2394        logDebug("Syncing implicit DB/schema to AST: catalog=" + defaultCatalog + ", schema=" + defaultSchema);
2395
2396        // Visit all statements and set implicit names on TObjectName nodes
2397        for (int i = 0; i < sqlStatements.size(); i++) {
2398            TCustomSqlStatement stmt = sqlStatements.get(i);
2399            if (stmt != null) {
2400                stmt.acceptChildren(new ImplicitDbSchemaVisitor(defaultCatalog, defaultSchema));
2401            }
2402        }
2403    }
2404
2405    /**
2406     * Visitor to set implicit database/schema on TObjectName nodes.
2407     */
2408    private static class ImplicitDbSchemaVisitor extends TParseTreeVisitor {
2409        private final String defaultCatalog;
2410        private final String defaultSchema;
2411
2412        public ImplicitDbSchemaVisitor(String defaultCatalog, String defaultSchema) {
2413            this.defaultCatalog = defaultCatalog;
2414            this.defaultSchema = defaultSchema;
2415        }
2416
2417        @Override
2418        public void preVisit(TObjectName node) {
2419            if (node == null) return;
2420
2421            // Skip column objects - they don't need implicit DB/schema
2422            if (node.getDbObjectType() == EDbObjectType.column) return;
2423
2424            // Set default database name if not qualified
2425            if (defaultCatalog != null && !defaultCatalog.isEmpty() && node.getDatabaseToken() == null) {
2426                node.setImplictDatabaseName(defaultCatalog);
2427            }
2428
2429            // Set default schema name if not qualified
2430            if (defaultSchema != null && !defaultSchema.isEmpty() && node.getSchemaToken() == null) {
2431                node.setImplictSchemaName(defaultSchema);
2432            }
2433        }
2434    }
2435
2436    /**
2437     * Selectively clear orphan column syntax hints (sphint) based on TSQLResolver2 resolution.
2438     *
2439     * Phase 1 (linkColumnToTable during parsing) adds sphint hints for columns it can't resolve.
2440     * TSQLResolver2 should:
2441     * 1. KEEP sphint hints for columns that are in allColumnReferences with NOT_FOUND/AMBIGUOUS status
2442     *    (these are genuinely orphan/ambiguous columns)
2443     * 2. CLEAR sphint hints for all other columns:
2444     *    - Columns successfully resolved (EXACT_MATCH)
2445     *    - Columns filtered out by ScopeBuilder (package constants, function keywords, etc.)
2446     *    - Columns in contexts TSQLResolver2 doesn't collect (MERGE VALUES, etc.)
2447     */
2448    private void clearOrphanColumnSyntaxHints() {
2449        // Build a set of positions for columns that should KEEP their sphint hints
2450        // These are columns in allColumnReferences with NOT_FOUND or AMBIGUOUS status
2451        Set<String> orphanPositions = new HashSet<>();
2452
2453        for (TObjectName col : allColumnReferences) {
2454            if (col == null) continue;
2455            gudusoft.gsqlparser.resolver2.model.ResolutionResult resolution = col.getResolution();
2456            if (resolution != null) {
2457                ResolutionStatus status = resolution.getStatus();
2458                // Only keep sphint for genuinely AMBIGUOUS columns
2459                // NOT_FOUND columns might be due to TSQLResolver2 scope issues (e.g., MERGE WHEN clause)
2460                // so we clear their sphint to match old resolver behavior
2461                if (status == ResolutionStatus.AMBIGUOUS) {
2462                    TSourceToken startToken = col.getStartToken();
2463                    if (startToken != null) {
2464                        String key = startToken.lineNo + ":" + startToken.columnNo;
2465                        orphanPositions.add(key);
2466                    }
2467                }
2468            }
2469        }
2470
2471        // Clear sphint hints for positions NOT in orphanPositions
2472        for (int i = 0; i < sqlStatements.size(); i++) {
2473            TCustomSqlStatement stmt = sqlStatements.get(i);
2474            if (stmt == null) continue;
2475            clearNonOrphanSphintHintsRecursive(stmt, orphanPositions);
2476        }
2477    }
2478
2479    /**
2480     * Recursively clear sphint hints except for genuinely orphan columns.
2481     */
2482    private void clearNonOrphanSphintHintsRecursive(TCustomSqlStatement stmt, Set<String> orphanPositions) {
2483        if (stmt == null) return;
2484
2485        // Clear sphint hints that are NOT for genuinely orphan columns
2486        if (stmt.getSyntaxHints() != null && stmt.getSyntaxHints().size() > 0) {
2487            for (int j = stmt.getSyntaxHints().size() - 1; j >= 0; j--) {
2488                TSyntaxError syntaxError = stmt.getSyntaxHints().get(j);
2489                if (syntaxError.errortype == EErrorType.sphint) {
2490                    String key = syntaxError.lineNo + ":" + syntaxError.columnNo;
2491                    if (!orphanPositions.contains(key)) {
2492                        // This sphint is NOT for a genuinely orphan column - clear it
2493                        stmt.getSyntaxHints().remove(j);
2494                        logDebug("Cleared sphint at line " + syntaxError.lineNo);
2495                    }
2496                    // Keep sphint hints for genuinely orphan columns (in orphanPositions)
2497                }
2498            }
2499        }
2500
2501        // Note: orphanColumns is populated by populateOrphanColumns() in Phase 4b
2502        // DO NOT clear it here - TGetTableColumn relies on orphanColumns for
2503        // linkOrphanColumnToFirstTable functionality
2504
2505        // Process nested statements
2506        for (int k = 0; k < stmt.getStatements().size(); k++) {
2507            clearNonOrphanSphintHintsRecursive(stmt.getStatements().get(k), orphanPositions);
2508        }
2509    }
2510
2511
2512
2513    /**
2514     * Filter UNNEST table's linkedColumns to keep only legitimate columns.
2515     * Phase 1 (linkColumnToTable) may incorrectly link external variables to UNNEST
2516     * when UNNEST is the only table in scope. This method removes such incorrect links.
2517     *
2518     * Legitimate columns for UNNEST:
2519     * - Implicit column: the alias (e.g., "arry_pair" from "UNNEST(...) AS arry_pair")
2520     * - WITH OFFSET column (e.g., "pos" from "WITH OFFSET AS pos")
2521     * - Derived struct field columns (from UNNEST of STRUCT arrays)
2522     */
2523    private void filterUnnestLinkedColumns(TTable unnestTable) {
2524        if (unnestTable == null || unnestTable.getTableType() != ETableSource.unnest) {
2525            return;
2526        }
2527
2528        TObjectNameList linkedColumns = unnestTable.getLinkedColumns();
2529        if (linkedColumns == null || linkedColumns.size() == 0) {
2530            return;
2531        }
2532
2533        // Build set of legitimate column names
2534        java.util.Set<String> legitimateNames = new java.util.HashSet<>();
2535
2536        // 1. Implicit column (alias name)
2537        String aliasName = unnestTable.getAliasName();
2538        if (aliasName != null && !aliasName.isEmpty()) {
2539            legitimateNames.add(aliasName.toUpperCase());
2540        }
2541
2542        // 2. WITH OFFSET column
2543        TUnnestClause unnestClause = unnestTable.getUnnestClause();
2544        if (unnestClause != null && unnestClause.getWithOffset() != null) {
2545            if (unnestClause.getWithOffsetAlais() != null &&
2546                unnestClause.getWithOffsetAlais().getAliasName() != null) {
2547                legitimateNames.add(unnestClause.getWithOffsetAlais().getAliasName().toString().toUpperCase());
2548            } else {
2549                legitimateNames.add("OFFSET");
2550            }
2551        }
2552
2553        // 3. Derived struct field columns
2554        if (unnestClause != null && unnestClause.getDerivedColumnList() != null) {
2555            for (int i = 0; i < unnestClause.getDerivedColumnList().size(); i++) {
2556                TObjectName derivedCol = unnestClause.getDerivedColumnList().getObjectName(i);
2557                if (derivedCol != null) {
2558                    legitimateNames.add(derivedCol.toString().toUpperCase());
2559                }
2560            }
2561        }
2562
2563        // 4. Explicit alias columns (Presto/Trino syntax: UNNEST(...) AS t(col1, col2))
2564        if (unnestTable.getAliasClause() != null &&
2565            unnestTable.getAliasClause().getColumns() != null) {
2566            for (int i = 0; i < unnestTable.getAliasClause().getColumns().size(); i++) {
2567                TObjectName colName = unnestTable.getAliasClause().getColumns().getObjectName(i);
2568                if (colName != null) {
2569                    legitimateNames.add(colName.toString().toUpperCase());
2570                }
2571            }
2572        }
2573
2574        // Collect columns to keep
2575        java.util.List<TObjectName> toKeep = new java.util.ArrayList<>();
2576        for (int i = 0; i < linkedColumns.size(); i++) {
2577            TObjectName col = linkedColumns.getObjectName(i);
2578            if (col != null) {
2579                String colName = col.getColumnNameOnly();
2580                if (colName != null && legitimateNames.contains(colName.toUpperCase())) {
2581                    toKeep.add(col);
2582                }
2583            }
2584        }
2585
2586        // Clear and re-add only legitimate columns
2587        linkedColumns.clear();
2588        for (TObjectName col : toKeep) {
2589            linkedColumns.addObjectName(col);
2590        }
2591    }
2592
2593    /**
2594     * Clear linkedColumns on all tables in all statements.
2595     */
2596    private void clearAllLinkedColumns() {
2597        // Use a set to track processed statements and avoid processing duplicates
2598        // This is important when processing subqueries within tables, as the same
2599        // subquery might be reachable from multiple paths
2600        java.util.Set<TCustomSqlStatement> processed = new java.util.HashSet<>();
2601        for (int i = 0; i < sqlStatements.size(); i++) {
2602            clearLinkedColumnsRecursive(sqlStatements.get(i), processed);
2603        }
2604    }
2605
2606    /**
2607     * Recursively clear orphanColumns on statements.
2608     * These will be repopulated with genuinely unresolved columns in Phase 4b.
2609     */
2610    private void clearOrphanColumnsRecursive(TCustomSqlStatement stmt) {
2611        if (stmt == null) return;
2612
2613        if (stmt.getOrphanColumns() != null) {
2614            stmt.getOrphanColumns().clear();
2615        }
2616
2617        // Process nested statements
2618        for (int i = 0; i < stmt.getStatements().size(); i++) {
2619            clearOrphanColumnsRecursive(stmt.getStatements().get(i));
2620        }
2621
2622        // Also handle stored procedure/function body statements
2623        if (stmt instanceof gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) {
2624            gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement sp =
2625                (gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) stmt;
2626            for (int i = 0; i < sp.getBodyStatements().size(); i++) {
2627                clearOrphanColumnsRecursive(sp.getBodyStatements().get(i));
2628            }
2629        }
2630    }
2631
2632    private void clearLinkedColumnsRecursive(TCustomSqlStatement stmt, java.util.Set<TCustomSqlStatement> processed) {
2633        if (stmt == null) return;
2634
2635        // Skip if already processed to avoid redundant work and potential infinite loops
2636        if (processed.contains(stmt)) {
2637            return;
2638        }
2639        processed.add(stmt);
2640
2641        // Skip DAX statements - they populate their own linkedColumns during parsing
2642        // via TDaxFunction.doParse() which calls psql.linkColumnToTable() directly.
2643        // TSQLResolver2's ScopeBuilder doesn't traverse DAX expressions, so we must
2644        // preserve the linkedColumns that DAX parsing already established.
2645        if (stmt instanceof TDaxStmt) {
2646            return;
2647        }
2648
2649        // Skip ALTER TABLE statements - they populate linkedColumns during parsing
2650        // via TAlterTableOption.doParse() which directly adds columns to the target table's
2651        // linkedColumns. TSQLResolver2's ScopeBuilder doesn't traverse these option nodes,
2652        // so we must preserve the linkedColumns that parsing already established.
2653        if (stmt instanceof TAlterTableStatement) {
2654            return;
2655        }
2656
2657        // For CREATE TABLE statements, we need special handling:
2658        // - Regular CREATE TABLE (with column definitions): Preserve constraint columns
2659        //   populated during TConstraint.doParse()
2660        // - CTAS (CREATE TABLE AS SELECT): Filter out source columns incorrectly added
2661        //   to target table, but preserve the correctly created alias columns
2662        boolean isCreateTable = (stmt instanceof TCreateTableSqlStatement);
2663        if (isCreateTable) {
2664            TCreateTableSqlStatement ctas = (TCreateTableSqlStatement) stmt;
2665            boolean isCTAS = (ctas.getSubQuery() != null);
2666            // For CTAS, filter out source columns from target table's linkedColumns
2667            // The old resolver incorrectly adds source columns (from the SELECT) to the target table
2668            // Keep only columns whose sourceTable is the target table itself
2669            if (isCTAS && ctas.getTargetTable() != null) {
2670                TTable targetTable = ctas.getTargetTable();
2671                TObjectNameList linkedColumns = targetTable.getLinkedColumns();
2672                if (linkedColumns != null && linkedColumns.size() > 0) {
2673                    // Collect columns to keep (those belonging to target table)
2674                    java.util.List<TObjectName> toKeep = new java.util.ArrayList<>();
2675                    for (int i = 0; i < linkedColumns.size(); i++) {
2676                        TObjectName col = linkedColumns.getObjectName(i);
2677                        if (col != null && col.getSourceTable() == targetTable) {
2678                            toKeep.add(col);
2679                        }
2680                    }
2681                    // Clear and re-add only the columns to keep
2682                    linkedColumns.clear();
2683                    for (TObjectName col : toKeep) {
2684                        linkedColumns.addObjectName(col);
2685                    }
2686                }
2687            }
2688        }
2689
2690        if (!isCreateTable && stmt.tables != null) {
2691            // Check if this statement contains a TD_UNPIVOT table
2692            // TD_UNPIVOT populates linkedColumns on its inner table during TTDUnpivot.doParse()
2693            // If we clear linkedColumns here, we lose those column references
2694            boolean hasTDUnpivot = false;
2695            for (int i = 0; i < stmt.tables.size(); i++) {
2696                TTable table = stmt.tables.getTable(i);
2697                if (table != null && table.getTableType() == ETableSource.td_unpivot) {
2698                    hasTDUnpivot = true;
2699                    break;
2700                }
2701            }
2702
2703            for (int i = 0; i < stmt.tables.size(); i++) {
2704                TTable table = stmt.tables.getTable(i);
2705                if (table != null && table.getLinkedColumns() != null) {
2706                    // For UNNEST tables, filter out incorrectly linked columns from Phase 1.
2707                    // Phase 1 (linkColumnToTable) may have linked external variables to UNNEST
2708                    // when it's the only table in scope. Keep only legitimate columns:
2709                    // - Implicit column (the UNNEST alias, e.g., "arry_pair" from "UNNEST(...) AS arry_pair")
2710                    // - WITH OFFSET column (e.g., "pos" from "WITH OFFSET AS pos")
2711                    if (table.getTableType() == ETableSource.unnest) {
2712                        filterUnnestLinkedColumns(table);
2713                        continue;
2714                    }
2715                    // Skip TD_UNPIVOT tables - they don't have their own columns but
2716                    // TTDUnpivot.doParse() populates columns on the inner table
2717                    if (table.getTableType() == ETableSource.td_unpivot) {
2718                        continue;
2719                    }
2720                    // If this statement contains TD_UNPIVOT, skip clearing all tables
2721                    // because TD_UNPIVOT populates linkedColumns on inner tables
2722                    if (hasTDUnpivot) {
2723                        continue;
2724                    }
2725                    table.getLinkedColumns().clear();
2726                }
2727            }
2728        }
2729
2730        // Skip recursive processing if this statement contains TD_UNPIVOT
2731        // TD_UNPIVOT's inner table (in the ON clause) has columns populated during parsing
2732        // and those columns need to be preserved
2733        boolean hasTDUnpivot = false;
2734        if (stmt.tables != null) {
2735            for (int i = 0; i < stmt.tables.size(); i++) {
2736                TTable table = stmt.tables.getTable(i);
2737                if (table != null && table.getTableType() == ETableSource.td_unpivot) {
2738                    hasTDUnpivot = true;
2739                    break;
2740                }
2741            }
2742        }
2743
2744        if (!hasTDUnpivot) {
2745            for (int i = 0; i < stmt.getStatements().size(); i++) {
2746                clearLinkedColumnsRecursive(stmt.getStatements().get(i), processed);
2747            }
2748
2749            // Also process subqueries within tables - these are NOT in getStatements()
2750            // but are accessed via table.getSubquery()
2751            if (stmt.tables != null) {
2752                for (int i = 0; i < stmt.tables.size(); i++) {
2753                    TTable table = stmt.tables.getTable(i);
2754                    if (table != null && table.getSubquery() != null) {
2755                        clearLinkedColumnsRecursive(table.getSubquery(), processed);
2756                    }
2757                }
2758            }
2759        }
2760    }
2761
2762    /**
2763     * Recursively fill TTable.getAttributes() for all tables in a statement.
2764     * Uses namespace data already collected during name resolution.
2765     *
2766     * Processing order is important:
2767     * 1. Process CTEs first
2768     * 2. Process leaf tables (objectname, function, etc.) - not JOIN or subquery
2769     * 3. Process subqueries (recursively)
2770     * 4. Process JOIN tables last (they depend on child tables having attributes)
2771     */
2772    private void fillTableAttributesRecursive(TCustomSqlStatement stmt, Set<TTable> processedTables) {
2773        if (stmt == null) return;
2774
2775        // Skip DAX statements - they use their own attribute/linkedColumn mechanism
2776        // established during TDaxFunction.doParse() parsing phase.
2777        if (stmt instanceof TDaxStmt) {
2778            return;
2779        }
2780
2781        // Skip ALTER TABLE statements - they use their own linkedColumn mechanism
2782        // established during TAlterTableOption.doParse() parsing phase.
2783        if (stmt instanceof TAlterTableStatement) {
2784            return;
2785        }
2786
2787        // Skip CREATE TABLE statements - they use their own linkedColumn mechanism
2788        // established during TConstraint.doParse() parsing phase.
2789        if (stmt instanceof TCreateTableSqlStatement) {
2790            return;
2791        }
2792
2793        // Phase 1: Process CTE tables first
2794        if (stmt instanceof TSelectSqlStatement) {
2795            TSelectSqlStatement selectStmt = (TSelectSqlStatement) stmt;
2796            TCTEList cteList = selectStmt.getCteList();
2797            if (cteList != null) {
2798                for (int i = 0; i < cteList.size(); i++) {
2799                    TCTE cte = cteList.getCTE(i);
2800                    if (cte != null && cte.getSubquery() != null) {
2801                        fillTableAttributesRecursive(cte.getSubquery(), processedTables);
2802                    }
2803                }
2804            }
2805        }
2806
2807        // Collect tables by type for proper processing order
2808        List<TTable> leafTables = new ArrayList<>();
2809        List<TTable> subqueryTables = new ArrayList<>();
2810        List<TTable> joinTables = new ArrayList<>();
2811
2812        // First, collect from stmt.tables
2813        if (stmt.tables != null) {
2814            for (int i = 0; i < stmt.tables.size(); i++) {
2815                TTable table = stmt.tables.getTable(i);
2816                if (table == null || processedTables.contains(table)) continue;
2817
2818                switch (table.getTableType()) {
2819                    case join:
2820                        joinTables.add(table);
2821                        // Also collect nested tables within the join
2822                        collectNestedJoinTables(table, leafTables, subqueryTables, joinTables, processedTables);
2823                        break;
2824                    case subquery:
2825                        subqueryTables.add(table);
2826                        break;
2827                    default:
2828                        leafTables.add(table);
2829                        break;
2830                }
2831            }
2832        }
2833
2834        // Also collect from getRelations() - JOIN tables are often stored there
2835        if (stmt.getRelations() != null) {
2836            for (int i = 0; i < stmt.getRelations().size(); i++) {
2837                IRelation rel = stmt.getRelations().get(i);
2838                if (!(rel instanceof TTable)) continue;
2839                TTable table = (TTable) rel;
2840                if (processedTables.contains(table)) continue;
2841
2842                if (table.getTableType() == ETableSource.join) {
2843                    if (!joinTables.contains(table)) {
2844                        joinTables.add(table);
2845                        // Also collect nested tables within the join
2846                        collectNestedJoinTables(table, leafTables, subqueryTables, joinTables, processedTables);
2847                    }
2848                }
2849            }
2850        }
2851
2852        // Phase 2: Process leaf tables first (objectname, function, xml, etc.)
2853        for (TTable table : leafTables) {
2854            if (!processedTables.contains(table)) {
2855                fillTableAttributes(table, processedTables, stmt);
2856                processedTables.add(table);
2857            }
2858        }
2859
2860        // Phase 3: Process subqueries (recursively process their contents first)
2861        for (TTable table : subqueryTables) {
2862            if (!processedTables.contains(table)) {
2863                if (table.getSubquery() != null) {
2864                    fillTableAttributesRecursive(table.getSubquery(), processedTables);
2865                }
2866                fillTableAttributes(table, processedTables, stmt);
2867                processedTables.add(table);
2868            }
2869        }
2870
2871        // Phase 4: Process JOIN tables last (they need child tables to have attributes)
2872        for (TTable table : joinTables) {
2873            if (!processedTables.contains(table)) {
2874                fillTableAttributes(table, processedTables, stmt);
2875                processedTables.add(table);
2876            }
2877        }
2878
2879        // Process nested statements
2880        for (int i = 0; i < stmt.getStatements().size(); i++) {
2881            fillTableAttributesRecursive(stmt.getStatements().get(i), processedTables);
2882        }
2883    }
2884
2885    /**
2886     * Collect nested tables within a JOIN expression.
2887     * This ensures all component tables are processed before the JOIN itself.
2888     */
2889    private void collectNestedJoinTables(TTable joinTable,
2890                                         List<TTable> leafTables,
2891                                         List<TTable> subqueryTables,
2892                                         List<TTable> joinTables,
2893                                         Set<TTable> processedTables) {
2894        if (joinTable == null || joinTable.getJoinExpr() == null) return;
2895
2896        TJoinExpr joinExpr = joinTable.getJoinExpr();
2897
2898        // Process left table
2899        TTable leftTable = joinExpr.getLeftTable();
2900        if (leftTable != null && !processedTables.contains(leftTable)) {
2901            switch (leftTable.getTableType()) {
2902                case join:
2903                    joinTables.add(leftTable);
2904                    collectNestedJoinTables(leftTable, leafTables, subqueryTables, joinTables, processedTables);
2905                    break;
2906                case subquery:
2907                    subqueryTables.add(leftTable);
2908                    break;
2909                default:
2910                    leafTables.add(leftTable);
2911                    break;
2912            }
2913        }
2914
2915        // Process right table
2916        TTable rightTable = joinExpr.getRightTable();
2917        if (rightTable != null && !processedTables.contains(rightTable)) {
2918            switch (rightTable.getTableType()) {
2919                case join:
2920                    joinTables.add(rightTable);
2921                    collectNestedJoinTables(rightTable, leafTables, subqueryTables, joinTables, processedTables);
2922                    break;
2923                case subquery:
2924                    subqueryTables.add(rightTable);
2925                    break;
2926                default:
2927                    leafTables.add(rightTable);
2928                    break;
2929            }
2930        }
2931    }
2932
2933    /**
2934     * Fill TTable.getAttributes() for a single table using namespace data.
2935     * This converts the namespace's columnSources to TAttributeNode objects.
2936     *
2937     * @param table The table to fill attributes for
2938     * @param processedTables Set of already processed tables to avoid duplicates
2939     * @param stmt The statement context (used for UNNEST to get the SELECT statement)
2940     */
2941    private void fillTableAttributes(TTable table, Set<TTable> processedTables, TCustomSqlStatement stmt) {
2942        if (table == null) return;
2943
2944        // Clear existing attributes
2945        table.getAttributes().clear();
2946
2947        String displayName = table.getDisplayName(true);
2948        if (displayName == null || displayName.isEmpty()) {
2949            displayName = table.getAliasName();
2950            if (displayName == null || displayName.isEmpty()) {
2951                displayName = table.getName();
2952            }
2953        }
2954
2955        // First, try to use existing namespace from ScopeBuildResult
2956        // Skip namespace lookup for UNNEST tables - they need special handling via initAttributesForUnnest
2957        INamespace existingNamespace = null;
2958        if (table.getTableType() != ETableSource.unnest) {
2959            existingNamespace = scopeBuildResult != null
2960                ? scopeBuildResult.getNamespaceForTable(table)
2961                : null;
2962        }
2963
2964        if (existingNamespace != null) {
2965            // Use existing namespace's column sources
2966            // Returns false if namespace has no real metadata (only inferred columns)
2967            if (fillAttributesFromNamespace(table, existingNamespace, displayName)) {
2968                return;
2969            }
2970            // Fall through to legacy logic if no real metadata
2971        }
2972
2973        // Fall back to type-specific handling if no namespace found
2974        switch (table.getTableType()) {
2975            case objectname:
2976                if (table.isCTEName()) {
2977                    // CTE reference - use initAttributesFromCTE
2978                    TCTE cte = table.getCTE();
2979                    if (cte != null) {
2980                        table.initAttributesFromCTE(cte);
2981                    }
2982                } else {
2983                    // Physical table - create TableNamespace and extract columns
2984                    fillPhysicalTableAttributes(table, displayName);
2985                }
2986                break;
2987
2988            case subquery:
2989                // Subquery - use initAttributesFromSubquery
2990                if (table.getSubquery() != null) {
2991                    String prefix = "";
2992                    if (table.getAliasClause() != null) {
2993                        prefix = table.getAliasClause().toString() + ".";
2994                    }
2995                    table.initAttributesFromSubquery(table.getSubquery(), prefix);
2996                }
2997                break;
2998
2999            case join:
3000                // JOIN - combine attributes from left and right tables
3001                // First, add USING columns to the left and right tables (if present)
3002                if (table.getJoinExpr() != null) {
3003                    addUsingColumnsToTables(table.getJoinExpr());
3004                    // Then initialize the join expression's attributes (which pulls from left/right tables)
3005                    table.getJoinExpr().initAttributes(0);
3006                }
3007                table.initAttributesForJoin();
3008                break;
3009
3010            case function:
3011                // Table function
3012                table.initAttributeForTableFunction();
3013                break;
3014
3015            case xmltable:
3016                // XML table
3017                table.initAttributeForXMLTable();
3018                break;
3019
3020            case tableExpr:
3021                // Table expression
3022                TAttributeNode.addNodeToList(
3023                    new TAttributeNode(displayName + ".*", table),
3024                    table.getAttributes()
3025                );
3026                break;
3027
3028            case rowList:
3029                // Row list
3030                table.initAttributeForRowList();
3031                break;
3032
3033            case unnest:
3034                // UNNEST - initialize attributes using the SELECT statement context
3035                if (stmt instanceof TSelectSqlStatement) {
3036                    TSelectSqlStatement select = (TSelectSqlStatement) stmt;
3037                    table.initAttributesForUnnest(getSqlEnv(), select);
3038                }
3039                break;
3040
3041            case pivoted_table:
3042                // PIVOT table
3043                table.initAttributesForPivotTable();
3044                break;
3045        }
3046    }
3047
3048    /**
3049     * Fill table attributes from an existing namespace's column sources.
3050     * This uses the namespace data that was collected during ScopeBuilder traversal.
3051     *
3052     * @return true if attributes were successfully filled, false if should fall back to legacy logic
3053     */
3054    private boolean fillAttributesFromNamespace(TTable table, INamespace namespace, String displayName) {
3055        // Ensure namespace is validated
3056        if (!namespace.isValidated()) {
3057            namespace.validate();
3058        }
3059
3060        // For TableNamespace without actual metadata (only inferred columns),
3061        // return false to fall back to legacy logic which uses wildcards
3062        if (namespace instanceof TableNamespace) {
3063            TableNamespace tableNs = (TableNamespace) namespace;
3064            // Check if the namespace has actual metadata by seeing if there are any columns
3065            // with high confidence from metadata sources (not inferred)
3066            Map<String, ColumnSource> columnSources = namespace.getAllColumnSources();
3067            boolean hasRealMetadata = false;
3068            for (ColumnSource source : columnSources.values()) {
3069                if (source.getConfidence() >= 1.0 &&
3070                    !("inferred_from_usage".equals(source.getEvidence()))) {
3071                    hasRealMetadata = true;
3072                    break;
3073                }
3074            }
3075            if (!hasRealMetadata) {
3076                // No real metadata, fall back to legacy logic with wildcards
3077                return false;
3078            }
3079
3080            // Has metadata - use namespace columns
3081            for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) {
3082                String colName = entry.getKey();
3083                ColumnSource source = entry.getValue();
3084                // Only include columns with real metadata, not inferred ones
3085                if (source.getConfidence() >= 1.0 &&
3086                    !("inferred_from_usage".equals(source.getEvidence()))) {
3087                    TAttributeNode.addNodeToList(
3088                        new TAttributeNode(displayName + "." + colName, table),
3089                        table.getAttributes()
3090                    );
3091                }
3092            }
3093
3094            // If no columns after filtering, add wildcard
3095            if (table.getAttributes().isEmpty()) {
3096                TAttributeNode.addNodeToList(
3097                    new TAttributeNode(displayName + ".*", table),
3098                    table.getAttributes()
3099                );
3100            }
3101            return true;
3102        }
3103
3104        // For other namespace types (SubqueryNamespace, CTENamespace, etc.),
3105        // use all column sources
3106        Map<String, ColumnSource> columnSources = namespace.getAllColumnSources();
3107        if (columnSources != null && !columnSources.isEmpty()) {
3108            for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) {
3109                String colName = entry.getKey();
3110                TAttributeNode.addNodeToList(
3111                    new TAttributeNode(displayName + "." + colName, table),
3112                    table.getAttributes()
3113                );
3114            }
3115        }
3116
3117        // If no columns found, add wildcard attribute
3118        if (table.getAttributes().isEmpty()) {
3119            TAttributeNode.addNodeToList(
3120                new TAttributeNode(displayName + ".*", table),
3121                table.getAttributes()
3122            );
3123        }
3124        return true;
3125    }
3126
3127    /**
3128     * Fill attributes for a physical table using TableNamespace.
3129     */
3130    private void fillPhysicalTableAttributes(TTable table, String displayName) {
3131        // Create namespace for this table with sqlEnv and vendor for qualified name resolution
3132        TSQLEnv sqlEnv = globalContext != null ? globalContext.getSqlEnv() : null;
3133        EDbVendor vendor = table.dbvendor != null ? table.dbvendor : EDbVendor.dbvoracle;
3134        TableNamespace namespace = new TableNamespace(table, config.getNameMatcher(), sqlEnv, vendor);
3135
3136        // Validate to populate columnSources
3137        namespace.validate();
3138
3139        // Convert columnSources to TAttributeNode
3140        Map<String, ColumnSource> columnSources = namespace.getAllColumnSources();
3141        if (columnSources != null && !columnSources.isEmpty()) {
3142            for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) {
3143                String colName = entry.getKey();
3144                TAttributeNode.addNodeToList(
3145                    new TAttributeNode(displayName + "." + colName, table),
3146                    table.getAttributes()
3147                );
3148            }
3149        }
3150
3151        // If no columns found from metadata, add wildcard attribute
3152        // (this allows any column to potentially match)
3153        if (table.getAttributes().isEmpty()) {
3154            // Add columns from linkedColumns if available
3155            if (table.getLinkedColumns() != null && table.getLinkedColumns().size() > 0) {
3156                for (TObjectName col : table.getLinkedColumns()) {
3157                    if (col.getCandidateTables() != null && col.getCandidateTables().size() > 1) {
3158                        continue;  // Skip ambiguous columns
3159                    }
3160                    TAttributeNode.addNodeToList(
3161                        new TAttributeNode(displayName + "." + col.getColumnNameOnly(), table),
3162                        table.getAttributes()
3163                    );
3164                }
3165            }
3166            // Add wildcard attribute
3167            TAttributeNode.addNodeToList(
3168                new TAttributeNode(displayName + ".*", table),
3169                table.getAttributes()
3170            );
3171        }
3172    }
3173
3174    /**
3175     * Add USING columns to the left and right tables in a JOIN expression.
3176     * USING columns should appear in both tables' attribute lists before the wildcard.
3177     * This method recursively handles nested JOINs.
3178     */
3179    private void addUsingColumnsToTables(TJoinExpr joinExpr) {
3180        if (joinExpr == null) return;
3181
3182        // Recursively handle nested joins
3183        TTable leftTable = joinExpr.getLeftTable();
3184        TTable rightTable = joinExpr.getRightTable();
3185
3186        if (leftTable != null && leftTable.getTableType() == ETableSource.join && leftTable.getJoinExpr() != null) {
3187            addUsingColumnsToTables(leftTable.getJoinExpr());
3188        }
3189        if (rightTable != null && rightTable.getTableType() == ETableSource.join && rightTable.getJoinExpr() != null) {
3190            addUsingColumnsToTables(rightTable.getJoinExpr());
3191        }
3192
3193        // Handle USING columns in this join
3194        gudusoft.gsqlparser.nodes.TObjectNameList usingColumns = joinExpr.getUsingColumns();
3195        if (usingColumns == null || usingColumns.size() == 0) return;
3196
3197        // Add USING columns to both tables
3198        for (int i = 0; i < usingColumns.size(); i++) {
3199            TObjectName usingCol = usingColumns.getObjectName(i);
3200            if (usingCol == null) continue;
3201            String colName = usingCol.getColumnNameOnly();
3202
3203            // Add to left table (insert before wildcard if possible)
3204            if (leftTable != null && leftTable.getTableType() != ETableSource.join) {
3205                addColumnAttributeBeforeWildcard(leftTable, colName);
3206            }
3207
3208            // Add to right table (insert before wildcard if possible)
3209            if (rightTable != null && rightTable.getTableType() != ETableSource.join) {
3210                addColumnAttributeBeforeWildcard(rightTable, colName);
3211            }
3212        }
3213    }
3214
3215    /**
3216     * Add a column attribute to a table, inserting before the wildcard (*) if present.
3217     * This ensures USING columns appear before the wildcard in the attribute list.
3218     */
3219    private void addColumnAttributeBeforeWildcard(TTable table, String columnName) {
3220        if (table == null || columnName == null) return;
3221
3222        String displayName = table.getDisplayName(true);
3223        if (displayName == null || displayName.isEmpty()) {
3224            displayName = table.getAliasName();
3225            if (displayName == null || displayName.isEmpty()) {
3226                displayName = table.getName();
3227            }
3228        }
3229
3230        String attrName = displayName + "." + columnName;
3231
3232        // Check if attribute already exists
3233        ArrayList<TAttributeNode> attrs = table.getAttributes();
3234        for (TAttributeNode attr : attrs) {
3235            if (attr.getName().equalsIgnoreCase(attrName)) {
3236                return;  // Already exists
3237            }
3238        }
3239
3240        // Find the wildcard position
3241        int wildcardIndex = -1;
3242        for (int i = 0; i < attrs.size(); i++) {
3243            if (attrs.get(i).getName().endsWith(".*")) {
3244                wildcardIndex = i;
3245                break;
3246            }
3247        }
3248
3249        // Insert before wildcard or add to end
3250        TAttributeNode newAttr = new TAttributeNode(attrName, table);
3251        if (wildcardIndex >= 0) {
3252            attrs.add(wildcardIndex, newAttr);
3253        } else {
3254            TAttributeNode.addNodeToList(newAttr, attrs);
3255        }
3256    }
3257
3258    /**
3259     * Sync a single column to legacy structures.
3260     * @return true if column was synced (had a sourceTable)
3261     */
3262    private boolean syncColumnToLegacy(TObjectName column) {
3263        if (column == null) return false;
3264
3265        // Special handling for star columns (SELECT *)
3266        // Star columns represent ALL tables in the FROM clause and should be synced to ALL tables
3267        // in their sourceTableList, not just the first one.
3268        String columnName = column.getColumnNameOnly();
3269        if (columnName != null && columnName.equals("*")) {
3270            java.util.ArrayList<TTable> sourceTableList = column.getSourceTableList();
3271            if (sourceTableList != null && sourceTableList.size() > 0) {
3272                boolean synced = false;
3273                for (TTable starTable : sourceTableList) {
3274                    if (starTable == null) continue;
3275                    // Skip subquery types - the star should be linked to physical tables
3276                    if (starTable.getTableType() == ETableSource.subquery) continue;
3277                    gudusoft.gsqlparser.nodes.TObjectNameList starLinkedColumns = starTable.getLinkedColumns();
3278                    if (starLinkedColumns != null && !containsColumn(starLinkedColumns, column)) {
3279                        starLinkedColumns.addObjectName(column);
3280                        synced = true;
3281                        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3282                            logInfo("syncColumnToLegacy: Synced star column to sourceTableList table: "
3283                                + starTable.getTableName());
3284                        }
3285                    }
3286                }
3287                return synced;
3288            }
3289        }
3290
3291        // Check if column is AMBIGUOUS - don't sync to legacy if it's ambiguous
3292        // Ambiguous columns should be added to orphanColumns, not linkedColumns
3293        // NOTE: Skip this check for star columns (*) since they are handled specially
3294        // via sourceTableList and should be linked to all tables in the FROM clause
3295        ResolutionResult resolution = column.getResolution();
3296        if (resolution != null && resolution.getStatus() == ResolutionStatus.AMBIGUOUS) {
3297            // Don't treat star columns as ambiguous - they're supposed to match all tables
3298            if (columnName != null && columnName.equals("*")) {
3299                if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3300                    logInfo("syncColumnToLegacy: Star column has AMBIGUOUS status, proceeding with normal sync");
3301                }
3302                // Fall through to normal processing
3303            } else {
3304                if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3305                    logInfo("syncColumnToLegacy: Skipping AMBIGUOUS column: " + column.toString()
3306                        + " with " + (resolution.getAmbiguousSource() != null ?
3307                            resolution.getAmbiguousSource().getCandidateCount() : 0) + " candidates");
3308                }
3309                // Clear sourceTable if it was set by Phase 1 (linkColumnToTable)
3310                // This ensures the column will be treated as orphan by TGetTableColumn
3311                if (column.getSourceTable() != null) {
3312                    column.setSourceTable(null);
3313                }
3314                return false;
3315            }
3316        }
3317
3318        TTable sourceTable = column.getSourceTable();
3319        ColumnSource source = column.getColumnSource();
3320
3321        // Handle columns resolved through PlsqlVariableNamespace
3322        // These are stored procedure variables/parameters - mark them as variables
3323        // so they won't be added to orphan columns
3324        if (source != null && source.getSourceNamespace() instanceof gudusoft.gsqlparser.resolver2.namespace.PlsqlVariableNamespace) {
3325            column.setDbObjectTypeDirectly(EDbObjectType.variable);
3326            // Variables don't need to be linked to tables
3327            return false;
3328        }
3329
3330        // Fix for subquery columns: When a column is EXPLICITLY QUALIFIED with a subquery alias
3331        // (e.g., mm.material_id), the old resolver Phase 1 may have incorrectly set sourceTable
3332        // to the physical table inside the subquery. TSQLResolver2 should correct this to point
3333        // to the subquery TTable itself. This preserves the intermediate layer for data lineage:
3334        // mm.material_id -> subquery mm -> physical table
3335        //
3336        // IMPORTANT: Only apply this correction for QUALIFIED columns. Unqualified columns
3337        // (like those inferred from star column expansion) should keep their physical table
3338        // sourceTable for proper data lineage tracing.
3339        if (source != null && column.isQualified()) {
3340            INamespace ns = source.getSourceNamespace();
3341            if (ns instanceof SubqueryNamespace) {
3342                TTable subqueryTable = ns.getSourceTable();
3343                // If the subquery's TTable is different from the current sourceTable,
3344                // use the subquery's TTable to maintain proper semantic layering
3345                if (subqueryTable != null && subqueryTable != sourceTable) {
3346                    if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3347                        logInfo("syncColumnToLegacy: Correcting sourceTable from " +
3348                            (sourceTable != null ? sourceTable.getTableName() : "null") +
3349                            " to subquery " + subqueryTable.getTableName() + " for qualified column " + column.toString());
3350                    }
3351                    sourceTable = subqueryTable;
3352                    column.setSourceTable(sourceTable);
3353                }
3354            }
3355        }
3356
3357        // If sourceTable is null, try to get it from ColumnSource
3358        // This handles columns resolved to derived tables (subqueries with aliases)
3359        // where TSQLResolver2 resolved via ColumnSource but didn't set sourceTable on TObjectName
3360        if (sourceTable == null && source != null) {
3361            TTable finalTable = source.getFinalTable();
3362            if (finalTable != null) {
3363                sourceTable = finalTable;
3364                column.setSourceTable(sourceTable);
3365                if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3366                    logInfo("syncColumnToLegacy: Set sourceTable from ColumnSource.getFinalTable() for "
3367                        + column.toString() + " -> " + finalTable.getTableName());
3368                }
3369            } else {
3370                // Try getAllFinalTables() - this may succeed when getFinalTable() returns null
3371                // For example, columns inferred through star push-down may have overrideTable set
3372                // which getAllFinalTables() will return as a single-element list
3373                java.util.List<TTable> allFinalTables = source.getAllFinalTables();
3374                if (allFinalTables != null && !allFinalTables.isEmpty()) {
3375                    // Use the first non-subquery table from allFinalTables
3376                    for (TTable candidateTable : allFinalTables) {
3377                        if (candidateTable != null && candidateTable.getTableType() != ETableSource.subquery) {
3378                            sourceTable = candidateTable;
3379                            column.setSourceTable(sourceTable);
3380                            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3381                                logInfo("syncColumnToLegacy: Set sourceTable from ColumnSource.getAllFinalTables() for "
3382                                    + column.toString() + " -> " + candidateTable.getTableName());
3383                            }
3384                            break;
3385                        }
3386                    }
3387                }
3388
3389                // Fallback: try overrideTable for cases like derived tables in JOIN ON clauses
3390                if (sourceTable == null) {
3391                    TTable overrideTable = source.getOverrideTable();
3392                    if (overrideTable != null) {
3393                        sourceTable = overrideTable;
3394                        column.setSourceTable(sourceTable);
3395                        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3396                            logInfo("syncColumnToLegacy: Set sourceTable from ColumnSource.getOverrideTable() for "
3397                                + column.toString() + " -> " + overrideTable.getTableName());
3398                        }
3399                    }
3400                }
3401            }
3402        }
3403
3404        if (sourceTable == null) {
3405            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE && source != null) {
3406                logInfo("syncColumnToLegacy: Column " + column.toString()
3407                    + " has ColumnSource but no table. Namespace: "
3408                    + (source.getSourceNamespace() != null ? source.getSourceNamespace().getClass().getSimpleName() : "null")
3409                    + ", evidence: " + source.getEvidence());
3410            }
3411            return false;
3412        }
3413
3414        // For struct-field access (e.g., customer.customer_id in BigQuery),
3415        // create a synthetic column representing the base column (e.g., "customer")
3416        // instead of using the original column which has the field name (e.g., "customer_id")
3417        if (source != null && "struct_field_access".equals(source.getEvidence())) {
3418            String baseColumnName = source.getExposedName();
3419            if (baseColumnName != null && !baseColumnName.isEmpty()) {
3420                // Create synthetic TObjectName for the base column
3421                EDbVendor vendor = config != null ? config.getVendor() : EDbVendor.dbvbigquery;
3422                TObjectName baseColumn = TObjectName.createObjectName(
3423                    vendor, EDbObjectType.column, baseColumnName);
3424                baseColumn.setSourceTable(sourceTable);
3425
3426                // Add the base column to linkedColumns (avoid duplicates by name)
3427                gudusoft.gsqlparser.nodes.TObjectNameList linkedColumns = sourceTable.getLinkedColumns();
3428                if (linkedColumns != null && !containsColumnByName(linkedColumns, baseColumnName)) {
3429                    linkedColumns.addObjectName(baseColumn);
3430                }
3431                return true;  // Skip adding the original column
3432            }
3433        }
3434
3435        // 1. Add to TTable.linkedColumns (avoid duplicates)
3436        gudusoft.gsqlparser.nodes.TObjectNameList linkedColumns = sourceTable.getLinkedColumns();
3437        if (linkedColumns != null && !containsColumn(linkedColumns, column)) {
3438            linkedColumns.addObjectName(column);
3439        }
3440
3441        // 2. For UNION scenarios, also add to all final tables from UNION branches
3442        // This is critical for star column push-down tests that expect columns to be
3443        // linked to ALL tables in a UNION, not just the first one.
3444        if (source != null) {
3445            java.util.List<TTable> allFinalTables = source.getAllFinalTables();
3446            if (allFinalTables != null && allFinalTables.size() > 1) {
3447                for (TTable unionTable : allFinalTables) {
3448                    if (unionTable == null || unionTable == sourceTable) continue;
3449                    // Skip subquery types - only link to physical tables
3450                    if (unionTable.getTableType() == ETableSource.subquery) continue;
3451                    gudusoft.gsqlparser.nodes.TObjectNameList unionLinkedColumns = unionTable.getLinkedColumns();
3452                    if (unionLinkedColumns != null && !containsColumn(unionLinkedColumns, column)) {
3453                        unionLinkedColumns.addObjectName(column);
3454                    }
3455                }
3456            }
3457
3458            // 2b. For CTE columns, also link to the CTE reference table
3459            // When a column is resolved through a CTE, it should be linked to both:
3460            // - The CTE reference table (immediate source)
3461            // - The underlying physical tables (final source)
3462            INamespace ns = source.getSourceNamespace();
3463            if (ns instanceof gudusoft.gsqlparser.resolver2.namespace.CTENamespace) {
3464                gudusoft.gsqlparser.resolver2.namespace.CTENamespace cteNs =
3465                        (gudusoft.gsqlparser.resolver2.namespace.CTENamespace) ns;
3466                TTable cteTable = cteNs.getReferencingTable();
3467                if (cteTable != null && cteTable != sourceTable) {
3468                    gudusoft.gsqlparser.nodes.TObjectNameList cteLinkedColumns = cteTable.getLinkedColumns();
3469                    if (cteLinkedColumns != null && !containsColumn(cteLinkedColumns, column)) {
3470                        cteLinkedColumns.addObjectName(column);
3471                    }
3472                }
3473            }
3474
3475            // 2c. For subquery columns, also link to the underlying physical tables
3476            // When sourceTable is a subquery (e.g., qualified column S.id from MERGE USING subquery),
3477            // TGetTableColumn needs the column to be linked to physical tables for output.
3478            // Use getFinalTable() to trace through to the ultimate physical table.
3479            // IMPORTANT: Only link if a column with the same name doesn't already exist -
3480            // this avoids duplicates when both outer and inner queries reference the same column.
3481            // EXCEPTION: Skip MERGE ON clause columns - they should not be linked to the source
3482            // subquery's underlying table because they may belong to the target table instead.
3483            if (sourceTable.getTableType() == ETableSource.subquery) {
3484                // Skip UNQUALIFIED join condition columns - they should not be traced to the source
3485                // subquery's underlying table via star column expansion.
3486                // This is particularly important for MERGE ON clause columns which may
3487                // belong to the target table rather than the source subquery.
3488                // QUALIFIED columns (like S.id) should still be traced as they explicitly reference
3489                // the source subquery.
3490                // Note: We check location only because ownStmt may be null for unresolved columns.
3491                boolean isUnqualifiedJoinConditionColumn = (column.getLocation() == ESqlClause.joinCondition)
3492                        && (column.getTableString() == null || column.getTableString().isEmpty());
3493                if (isUnqualifiedJoinConditionColumn && TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3494                    logInfo("syncColumnToLegacy: Skipping unqualified join condition column " + column.toString() +
3495                        " - should not be traced to subquery's underlying table");
3496                }
3497
3498                if (!isUnqualifiedJoinConditionColumn) {
3499                    TTable finalTable = source.getFinalTable();
3500                    if (finalTable != null && finalTable != sourceTable &&
3501                        finalTable.getTableType() != ETableSource.subquery) {
3502                        gudusoft.gsqlparser.nodes.TObjectNameList finalLinkedColumns = finalTable.getLinkedColumns();
3503                        if (finalLinkedColumns != null && !containsColumn(finalLinkedColumns, column)
3504                                && !containsColumnByName(finalLinkedColumns, column.getColumnNameOnly())) {
3505                            finalLinkedColumns.addObjectName(column);
3506                            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3507                                logInfo("syncColumnToLegacy: Also linked " + column.toString() +
3508                                    " to underlying physical table " + finalTable.getTableName());
3509                            }
3510                        }
3511                    }
3512                }
3513            }
3514
3515        }
3516
3517        // 3. Sync linkedColumnDef and sourceColumn from ColumnSource
3518        if (source != null) {
3519            Object defNode = source.getDefinitionNode();
3520
3521            // Set linkedColumnDef if definition is a TColumnDefinition
3522            if (defNode instanceof gudusoft.gsqlparser.nodes.TColumnDefinition) {
3523                column.setLinkedColumnDef((gudusoft.gsqlparser.nodes.TColumnDefinition) defNode);
3524            }
3525
3526            // Set sourceColumn if definition is a TResultColumn
3527            // BUT skip for CTE explicit columns - these reference the CTE column name (e.g., "mgr_dept")
3528            // not the underlying SELECT column (e.g., "grp"). The CTE column is a TObjectName,
3529            // not a TResultColumn, so we cannot set it as sourceColumn.
3530            if (defNode instanceof TResultColumn) {
3531                String evidence = source.getEvidence();
3532                boolean isCTEExplicitColumn = evidence != null && evidence.startsWith("cte_explicit_column");
3533                if (!isCTEExplicitColumn) {
3534                    column.setSourceColumn((TResultColumn) defNode);
3535                }
3536            }
3537            // Special case: for star-inferred columns, set sourceColumn to the star column
3538            // The definitionNode is intentionally null to avoid affecting formatter output,
3539            // but we still need to set sourceColumn for legacy API compatibility.
3540            // Use setSourceColumnOnly to avoid changing dbObjectType which affects filtering.
3541            else if (defNode == null && source.getEvidence() != null
3542                    && source.getEvidence().contains("auto_inferred")) {
3543                // This is a star-inferred column - get the star column from the namespace
3544                INamespace namespace = source.getSourceNamespace();
3545                if (namespace != null) {
3546                    TResultColumn starColumn = namespace.getStarColumn();
3547                    if (starColumn != null) {
3548                        column.setSourceColumnOnly(starColumn);
3549                    }
3550                }
3551            }
3552        }
3553
3554        return true;
3555    }
3556
3557    /**
3558     * Check if a column already exists in the list (by identity).
3559     */
3560    private boolean containsColumn(gudusoft.gsqlparser.nodes.TObjectNameList list, TObjectName column) {
3561        for (int i = 0; i < list.size(); i++) {
3562            if (list.getObjectName(i) == column) {
3563                return true;
3564            }
3565        }
3566        return false;
3567    }
3568
3569    /**
3570     * Check if a column with the given name already exists in the list.
3571     * Used for struct-field access where we create synthetic columns.
3572     */
3573    private boolean containsColumnByName(gudusoft.gsqlparser.nodes.TObjectNameList list, String columnName) {
3574        if (columnName == null) return false;
3575        // Normalize by stripping quotes for comparison
3576        String normalizedName = stripQuotes(columnName);
3577        for (int i = 0; i < list.size(); i++) {
3578            TObjectName col = list.getObjectName(i);
3579            if (col != null) {
3580                String existingName = stripQuotes(col.getColumnNameOnly());
3581                if (normalizedName.equalsIgnoreCase(existingName)) {
3582                    return true;
3583                }
3584            }
3585        }
3586        return false;
3587    }
3588
3589    /**
3590     * Strip leading/trailing quote characters from a string.
3591     */
3592    private String stripQuotes(String s) {
3593        if (s == null) return null;
3594        if (s.length() >= 2) {
3595            char first = s.charAt(0);
3596            char last = s.charAt(s.length() - 1);
3597            if ((first == '"' && last == '"') ||
3598                (first == '\'' && last == '\'') ||
3599                (first == '`' && last == '`') ||
3600                (first == '[' && last == ']')) {
3601                return s.substring(1, s.length() - 1);
3602            }
3603        }
3604        return s;
3605    }
3606
3607    /**
3608     * Check if a subquery SELECT statement has an explicit (non-star) column with the given name.
3609     * This is used to determine whether to create traced column clones:
3610     * - If the column matches an explicit column in the subquery, don't clone (stays at subquery level)
3611     * - If the column doesn't match explicit columns (must come from star), clone to physical table
3612     *
3613     * @param subquery the SELECT statement to check
3614     * @param columnName the column name to look for (may have quotes)
3615     * @return true if the subquery has an explicit column matching the name
3616     */
3617    private boolean subqueryHasExplicitColumn(TSelectSqlStatement subquery, String columnName) {
3618        if (subquery == null || columnName == null) {
3619            return false;
3620        }
3621
3622        // For combined queries (UNION/INTERSECT/EXCEPT), check the left branch
3623        if (subquery.isCombinedQuery()) {
3624            return subqueryHasExplicitColumn(subquery.getLeftStmt(), columnName);
3625        }
3626
3627        TResultColumnList resultColumns = subquery.getResultColumnList();
3628        if (resultColumns == null) {
3629            return false;
3630        }
3631
3632        // Normalize the column name for comparison (strip quotes)
3633        String normalizedName = stripQuotes(columnName);
3634
3635        for (int i = 0; i < resultColumns.size(); i++) {
3636            TResultColumn rc = resultColumns.getResultColumn(i);
3637            if (rc == null) {
3638                continue;
3639            }
3640
3641            String colStr = rc.toString();
3642            // Skip star columns - they're not explicit columns
3643            if (colStr != null && (colStr.equals("*") || colStr.endsWith(".*"))) {
3644                continue;
3645            }
3646
3647            // Get the effective column name (alias if present, otherwise the column name)
3648            String effectiveName = null;
3649            if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
3650                effectiveName = rc.getAliasClause().getAliasName().toString();
3651            } else if (rc.getExpr() != null && rc.getExpr().getObjectOperand() != null) {
3652                // For simple column references like "t1.COL1", get the column name
3653                effectiveName = rc.getExpr().getObjectOperand().getColumnNameOnly();
3654            }
3655
3656            if (effectiveName != null) {
3657                String normalizedEffective = stripQuotes(effectiveName);
3658                if (normalizedName.equalsIgnoreCase(normalizedEffective)) {
3659                    return true;
3660                }
3661            }
3662        }
3663
3664        return false;
3665    }
3666
3667    /**
3668     * Expand star columns using push-down inferred columns from namespaces.
3669     *
3670     * This is the core of the star column push-down algorithm:
3671     * 1. Find all star columns in SELECT lists
3672     * 2. For each star column, find its source namespace(s)
3673     * 3. Get inferred columns from the namespace (collected during resolution)
3674     * 4. Expand the star column by populating attributeNodesDerivedFromFromClause
3675     *
3676     * This enables star column expansion without TSQLEnv metadata by using
3677     * columns referenced in outer queries to infer what the star expands to.
3678     */
3679    private void expandStarColumnsUsingPushDown() {
3680        int expandedCount = 0;
3681        Set<TCustomSqlStatement> processedStmts = new HashSet<>();
3682
3683        // Track expanded star columns by their string representation for syncing
3684        Map<String, ArrayList<TAttributeNode>> expandedStarCols = new HashMap<>();
3685
3686        // Process all statements recursively
3687        for (int i = 0; i < sqlStatements.size(); i++) {
3688            expandedCount += expandStarColumnsInStatement(sqlStatements.get(i), processedStmts, expandedStarCols);
3689        }
3690
3691        // Sync expanded attributes to column references in getAllColumnReferences()
3692        // The result column TObjectNames might be different instances than those collected
3693        // during scope building, so we need to copy the expanded attrs
3694        if (scopeBuildResult != null && !expandedStarCols.isEmpty()) {
3695            for (TObjectName colRef : scopeBuildResult.getAllColumnReferences()) {
3696                if (colRef == null) continue;
3697                String colStr = colRef.toString();
3698                if (colStr == null || !colStr.endsWith("*")) continue;
3699
3700                // Skip if already has expanded attrs
3701                ArrayList<TAttributeNode> existingAttrs = colRef.getAttributeNodesDerivedFromFromClause();
3702                if (existingAttrs != null && !existingAttrs.isEmpty()) continue;
3703
3704                // Find matching expanded star column
3705                ArrayList<TAttributeNode> expandedAttrs = expandedStarCols.get(colStr);
3706                if (expandedAttrs != null && !expandedAttrs.isEmpty()) {
3707                    // Copy the expanded attrs to this column reference
3708                    for (TAttributeNode attr : expandedAttrs) {
3709                        TAttributeNode.addNodeToList(attr, existingAttrs);
3710                    }
3711                    if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3712                        logInfo("Synced " + expandedAttrs.size() + " expanded attrs to column reference: " + colStr);
3713                    }
3714                }
3715            }
3716        }
3717
3718        logInfo("Expanded star columns using push-down: " + expandedCount + " columns added");
3719    }
3720
3721    /**
3722     * Recursively expand star columns in a statement and its nested statements.
3723     * Uses processedStmts to track ALL statements (not just SELECTs) to prevent infinite loops.
3724     */
3725    private int expandStarColumnsInStatement(TCustomSqlStatement stmt, Set<TCustomSqlStatement> processedStmts,
3726                                              Map<String, ArrayList<TAttributeNode>> expandedStarCols) {
3727        if (stmt == null) return 0;
3728
3729        // Cycle detection: skip if already processed this statement
3730        if (processedStmts.contains(stmt)) {
3731            return 0;
3732        }
3733        processedStmts.add(stmt);
3734
3735        int count = 0;
3736
3737        // Handle SELECT statements
3738        if (stmt instanceof TSelectSqlStatement) {
3739            TSelectSqlStatement select = (TSelectSqlStatement) stmt;
3740            count += expandStarColumnsInSelect(select, expandedStarCols);
3741
3742            // Handle UNION/INTERSECT/EXCEPT
3743            if (select.isCombinedQuery()) {
3744                if (select.getLeftStmt() != null) {
3745                    count += expandStarColumnsInStatement(select.getLeftStmt(), processedStmts, expandedStarCols);
3746                }
3747                if (select.getRightStmt() != null) {
3748                    count += expandStarColumnsInStatement(select.getRightStmt(), processedStmts, expandedStarCols);
3749                }
3750            }
3751        }
3752
3753        // Handle MERGE statements specially - process the USING clause
3754        if (stmt instanceof gudusoft.gsqlparser.stmt.TMergeSqlStatement) {
3755            gudusoft.gsqlparser.stmt.TMergeSqlStatement merge = (gudusoft.gsqlparser.stmt.TMergeSqlStatement) stmt;
3756            TTable usingTable = merge.getUsingTable();
3757            if (usingTable != null && usingTable.getSubquery() != null) {
3758                count += expandStarColumnsInStatement(usingTable.getSubquery(), processedStmts, expandedStarCols);
3759            }
3760        }
3761
3762        // Process nested statements
3763        if (stmt.getStatements() != null) {
3764            for (int i = 0; i < stmt.getStatements().size(); i++) {
3765                Object nested = stmt.getStatements().get(i);
3766                if (nested instanceof TCustomSqlStatement) {
3767                    count += expandStarColumnsInStatement((TCustomSqlStatement) nested, processedStmts, expandedStarCols);
3768                }
3769            }
3770        }
3771
3772        // Process tables with subqueries
3773        if (stmt.tables != null) {
3774            for (int i = 0; i < stmt.tables.size(); i++) {
3775                TTable table = stmt.tables.getTable(i);
3776                if (table != null && table.getSubquery() != null) {
3777                    count += expandStarColumnsInStatement(table.getSubquery(), processedStmts, expandedStarCols);
3778                }
3779            }
3780        }
3781
3782        // Process CTEs
3783        if (stmt.getCteList() != null) {
3784            for (int i = 0; i < stmt.getCteList().size(); i++) {
3785                TCTE cte = stmt.getCteList().getCTE(i);
3786                if (cte != null && cte.getSubquery() != null) {
3787                    count += expandStarColumnsInStatement(cte.getSubquery(), processedStmts, expandedStarCols);
3788                }
3789            }
3790        }
3791
3792        return count;
3793    }
3794
3795    /**
3796     * Expand star columns in a SELECT statement's result column list.
3797     */
3798    private int expandStarColumnsInSelect(TSelectSqlStatement select, Map<String, ArrayList<TAttributeNode>> expandedStarCols) {
3799        if (select == null || select.getResultColumnList() == null) return 0;
3800
3801        int count = 0;
3802        TResultColumnList resultCols = select.getResultColumnList();
3803
3804        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3805            logInfo("expandStarColumnsInSelect: Processing SELECT with " + resultCols.size() + " result columns");
3806        }
3807
3808        for (int i = 0; i < resultCols.size(); i++) {
3809            TResultColumn rc = resultCols.getResultColumn(i);
3810            if (rc == null || rc.getExpr() == null) continue;
3811
3812            TObjectName objName = rc.getExpr().getObjectOperand();
3813            if (objName == null) continue;
3814
3815            String colStr = objName.toString();
3816            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE && colStr != null) {
3817                logInfo("expandStarColumnsInSelect: Column " + i + ": " + colStr);
3818            }
3819            if (colStr == null || !colStr.endsWith("*")) continue;
3820
3821            // This is a star column - expand it
3822            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3823                logInfo("expandStarColumnsInSelect: Found star column: " + colStr);
3824            }
3825            count += expandSingleStarColumn(objName, select, colStr, rc);
3826
3827            // Track the expanded attrs for syncing to column references
3828            ArrayList<TAttributeNode> attrList = objName.getAttributeNodesDerivedFromFromClause();
3829            if (attrList != null && !attrList.isEmpty()) {
3830                expandedStarCols.put(colStr, attrList);
3831            }
3832        }
3833
3834        return count;
3835    }
3836
3837    /**
3838     * Expand a single star column using push-down inferred columns.
3839     *
3840     * @param starColumn The star column TObjectName (e.g., "*" or "src.*")
3841     * @param select The containing SELECT statement
3842     * @param colStr The string representation of the star column
3843     * @param resultColumn The TResultColumn containing the star (for EXCEPT column list)
3844     * @return Number of columns added
3845     */
3846    private int expandSingleStarColumn(TObjectName starColumn, TSelectSqlStatement select, String colStr, TResultColumn resultColumn) {
3847        ArrayList<TAttributeNode> attrList = starColumn.getAttributeNodesDerivedFromFromClause();
3848
3849        // Skip if already expanded
3850        if (!attrList.isEmpty()) {
3851            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3852                logInfo("expandSingleStarColumn: " + colStr + " already expanded with " + attrList.size() + " attrs");
3853            }
3854            return 0;
3855        }
3856
3857        // Collect EXCEPT column names to exclude from expansion
3858        // (BigQuery: SELECT * EXCEPT (col1, col2) FROM ...)
3859        Set<String> exceptColumns = new HashSet<>();
3860        if (resultColumn != null) {
3861            TObjectNameList exceptList = resultColumn.getExceptColumnList();
3862            if (exceptList != null && exceptList.size() > 0) {
3863                for (int i = 0; i < exceptList.size(); i++) {
3864                    TObjectName exceptCol = exceptList.getObjectName(i);
3865                    if (exceptCol != null) {
3866                        String exceptName = exceptCol.getColumnNameOnly();
3867                        if (exceptName != null && !exceptName.isEmpty()) {
3868                            exceptColumns.add(exceptName.toUpperCase());
3869                        }
3870                    }
3871                }
3872                if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3873                    logInfo("expandSingleStarColumn: Found " + exceptColumns.size() +
3874                            " EXCEPT columns: " + exceptColumns);
3875                }
3876            }
3877        }
3878
3879        int count = 0;
3880        boolean isQualified = colStr.contains(".") && !colStr.equals("*");
3881
3882        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3883            logInfo("expandSingleStarColumn: " + colStr + " isQualified=" + isQualified);
3884        }
3885
3886        if (isQualified) {
3887            // Qualified star (e.g., "src.*") - find the specific table/namespace
3888            String tablePrefix = colStr.substring(0, colStr.lastIndexOf('.'));
3889            count += expandQualifiedStar(starColumn, select, tablePrefix, attrList, exceptColumns);
3890        } else {
3891            // Unqualified star (*) - expand from all tables in FROM clause
3892            count += expandUnqualifiedStar(starColumn, select, attrList, exceptColumns);
3893        }
3894
3895        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3896            logInfo("expandSingleStarColumn: " + colStr + " expanded to " + count + " columns");
3897        }
3898
3899        return count;
3900    }
3901
3902    /**
3903     * Expand a qualified star column (e.g., "src.*") using namespace inferred columns.
3904     *
3905     * @param starColumn The star column TObjectName
3906     * @param select The containing SELECT statement
3907     * @param tablePrefix The table prefix (e.g., "src" from "src.*")
3908     * @param attrList The list to add expanded attributes to
3909     * @param exceptColumns Column names to exclude (from EXCEPT clause), uppercase
3910     */
3911    private int expandQualifiedStar(TObjectName starColumn, TSelectSqlStatement select,
3912                                     String tablePrefix, ArrayList<TAttributeNode> attrList,
3913                                     Set<String> exceptColumns) {
3914        int count = 0;
3915
3916        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3917            logInfo("expandQualifiedStar: tablePrefix=" + tablePrefix +
3918                    ", exceptColumns=" + (exceptColumns != null ? exceptColumns : "none"));
3919        }
3920
3921        // Find the source table by alias or name
3922        TTable sourceTable = findTableByPrefixInSelect(select, tablePrefix);
3923        if (sourceTable == null) {
3924            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3925                logInfo("expandQualifiedStar: No source table found for " + tablePrefix);
3926            }
3927            // Fall back to just adding the qualified star attribute
3928            TAttributeNode.addNodeToList(
3929                new TAttributeNode(tablePrefix + ".*", null),
3930                attrList
3931            );
3932            return 0;
3933        }
3934
3935        // Collect inferred columns from multiple sources:
3936        // 1. The table's own namespace (TableNamespace)
3937        // 2. If the SELECT is a CTE definition, the CTE's namespace
3938        // 3. If the SELECT is a subquery, the containing scope's namespace
3939        Set<String> allInferredCols = new HashSet<>();
3940
3941        // Source 1: Get namespace for this table
3942        INamespace tableNamespace = scopeBuildResult != null
3943            ? scopeBuildResult.getNamespaceForTable(sourceTable)
3944            : null;
3945
3946        if (tableNamespace != null) {
3947            Set<String> inferredCols = tableNamespace.getInferredColumns();
3948            if (inferredCols != null) {
3949                allInferredCols.addAll(inferredCols);
3950            }
3951        }
3952
3953        // Source 2: Check if this SELECT is part of a CTE definition
3954        // If so, the CTE namespace may have inferred columns from outer queries
3955        Set<String> cteInferredCols = getInferredColumnsFromContainingCTE(select);
3956        if (cteInferredCols != null) {
3957            if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3958                logInfo("expandQualifiedStar: Adding " + cteInferredCols.size() +
3959                        " CTE inferred columns for " + tablePrefix);
3960            }
3961            allInferredCols.addAll(cteInferredCols);
3962        }
3963
3964        // Source 3: Check the SELECT's output scope for inferred columns
3965        // IMPORTANT: For qualified star columns (like ta.*), only use scope-level inferred columns
3966        // if they actually exist in this table's namespace. Otherwise we'd incorrectly add columns
3967        // from other tables in the FROM clause to this star's expanded attributes.
3968        IScope selectScope = scopeBuildResult != null
3969            ? scopeBuildResult.getScopeForStatement(select)
3970            : null;
3971        if (selectScope != null) {
3972            Set<String> scopeInferredCols = getInferredColumnsFromScope(selectScope);
3973            if (scopeInferredCols != null && tableNamespace != null) {
3974                // Only add scope-level inferred columns that actually exist in this table's namespace
3975                // This prevents columns from other tables being incorrectly associated with this star
3976                Map<String, ColumnSource> columnSources = tableNamespace.getAllColumnSources();
3977                Set<String> tableInferredCols = tableNamespace.getInferredColumns();
3978                for (String scopeCol : scopeInferredCols) {
3979                    // Check if this column can be resolved within this table's namespace
3980                    boolean hasInNamespace = (columnSources != null && columnSources.containsKey(scopeCol)) ||
3981                                             (tableInferredCols != null && tableInferredCols.contains(scopeCol));
3982                    if (hasInNamespace) {
3983                        allInferredCols.add(scopeCol);
3984                    }
3985                }
3986            } else if (scopeInferredCols != null && tableNamespace == null) {
3987                // No table namespace - add all scope columns (fallback for edge cases)
3988                allInferredCols.addAll(scopeInferredCols);
3989            }
3990        }
3991
3992        if (!allInferredCols.isEmpty()) {
3993            // Expand using inferred columns, filtering out EXCEPT columns
3994            for (String colName : allInferredCols) {
3995                // Skip columns in EXCEPT clause
3996                if (exceptColumns != null && !exceptColumns.isEmpty() &&
3997                    exceptColumns.contains(colName.toUpperCase())) {
3998                    if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
3999                        logInfo("expandQualifiedStar: Skipping EXCEPT column: " + colName);
4000                    }
4001                    continue;
4002                }
4003                String attrName = tablePrefix + "." + colName;
4004                TAttributeNode.addNodeToList(
4005                    new TAttributeNode(attrName, sourceTable),
4006                    attrList
4007                );
4008                count++;
4009            }
4010        } else if (tableNamespace != null) {
4011            // No inferred columns - try to get from namespace's column sources
4012            Map<String, ColumnSource> columnSources = tableNamespace.getAllColumnSources();
4013            if (columnSources != null && !columnSources.isEmpty()) {
4014                for (String colName : columnSources.keySet()) {
4015                    // Skip columns in EXCEPT clause
4016                    if (exceptColumns != null && !exceptColumns.isEmpty() &&
4017                        exceptColumns.contains(colName.toUpperCase())) {
4018                        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
4019                            logInfo("expandQualifiedStar: Skipping EXCEPT column from sources: " + colName);
4020                        }
4021                        continue;
4022                    }
4023                    String attrName = tablePrefix + "." + colName;
4024                    TAttributeNode.addNodeToList(
4025                        new TAttributeNode(attrName, sourceTable),
4026                        attrList
4027                    );
4028                    count++;
4029                }
4030            }
4031        }
4032
4033        // If no columns were added, add the star as fallback
4034        if (count == 0) {
4035            TAttributeNode.addNodeToList(
4036                new TAttributeNode(tablePrefix + ".*", sourceTable),
4037                attrList
4038            );
4039        }
4040
4041        return count;
4042    }
4043
4044    /**
4045     * Get inferred columns from a CTE that contains the given SELECT statement.
4046     * Used for push-down: when outer queries reference columns from a CTE,
4047     * those columns are inferred in the CTE's namespace and should be used
4048     * to expand star columns in the CTE's SELECT.
4049     */
4050    private Set<String> getInferredColumnsFromContainingCTE(TSelectSqlStatement select) {
4051        if (select == null || scopeBuildResult == null || namespaceEnhancer == null) {
4052            return null;
4053        }
4054
4055        // Find the CTE that defines this SELECT
4056        Set<INamespace> starNamespaces = namespaceEnhancer.getStarNamespaces();
4057        if (starNamespaces == null) {
4058            return null;
4059        }
4060
4061        for (INamespace ns : starNamespaces) {
4062            if (ns instanceof CTENamespace) {
4063                CTENamespace cteNs = (CTENamespace) ns;
4064                TSelectSqlStatement cteSelect = cteNs.getSelectStatement();
4065                // Check both by reference and by start token position
4066                if (cteSelect == select ||
4067                    (cteSelect != null && select != null &&
4068                     cteSelect.getStartToken() != null && select.getStartToken() != null &&
4069                     cteSelect.getStartToken().posinlist == select.getStartToken().posinlist)) {
4070                    Set<String> inferredCols = cteNs.getInferredColumns();
4071                    if (inferredCols != null && !inferredCols.isEmpty()) {
4072                        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
4073                            logInfo("getInferredColumnsFromContainingCTE: Found CTE " + cteNs.getDisplayName() +
4074                                    " with " + inferredCols.size() + " inferred columns");
4075                        }
4076                        return inferredCols;
4077                    }
4078                }
4079            } else if (ns instanceof SubqueryNamespace) {
4080                SubqueryNamespace subNs = (SubqueryNamespace) ns;
4081                TSelectSqlStatement subSelect = subNs.getSelectStatement();
4082                if (subSelect == select ||
4083                    (subSelect != null && select != null &&
4084                     subSelect.getStartToken() != null && select.getStartToken() != null &&
4085                     subSelect.getStartToken().posinlist == select.getStartToken().posinlist)) {
4086                    Set<String> inferredCols = subNs.getInferredColumns();
4087                    if (inferredCols != null && !inferredCols.isEmpty()) {
4088                        if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) {
4089                            logInfo("getInferredColumnsFromContainingCTE: Found Subquery with " +
4090                                    inferredCols.size() + " inferred columns");
4091                        }
4092                        return inferredCols;
4093                    }
4094                }
4095            }
4096        }
4097
4098        return null;
4099    }
4100
4101    /**
4102     * Get inferred columns from namespaces in a scope's FROM clause.
4103     */
4104    private Set<String> getInferredColumnsFromScope(IScope scope) {
4105        if (scope == null) {
4106            return null;
4107        }
4108
4109        Set<String> result = new HashSet<>();
4110
4111        // Check all namespaces in the scope's children
4112        for (gudusoft.gsqlparser.resolver2.model.ScopeChild child : scope.getChildren()) {
4113            INamespace ns = child.getNamespace();
4114            if (ns != null) {
4115                Set<String> inferredCols = ns.getInferredColumns();
4116                if (inferredCols != null) {
4117                    result.addAll(inferredCols);
4118                }
4119            }
4120        }
4121
4122        return result.isEmpty() ? null : result;
4123    }
4124
4125    /**
4126     * Expand an unqualified star column (*) using all tables in FROM clause.
4127     *
4128     * @param starColumn The star column TObjectName
4129     * @param select The containing SELECT statement
4130     * @param attrList The list to add expanded attributes to
4131     * @param exceptColumns Column names to exclude (from EXCEPT clause), uppercase
4132     */
4133    private int expandUnqualifiedStar(TObjectName starColumn, TSelectSqlStatement select,
4134                                       ArrayList<TAttributeNode> attrList, Set<String> exceptColumns) {
4135        int count = 0;
4136
4137        if (select.tables == null) return 0;
4138
4139        for (int i = 0; i < select.tables.size(); i++) {
4140            TTable table = select.tables.getTable(i);
4141            if (table == null) continue;
4142
4143            // Skip certain table types
4144            if (table.getTableType() == ETableSource.join) continue;
4145
4146            String tablePrefix = table.getAliasName();
4147            if (tablePrefix == null || tablePrefix.isEmpty()) {
4148                tablePrefix = table.getName();
4149            }
4150            if (tablePrefix == null) continue;
4151
4152            // Get namespace for this table
4153            INamespace namespace = scopeBuildResult != null
4154                ? scopeBuildResult.getNamespaceForTable(table)
4155                : null;
4156
4157            if (namespace != null) {
4158                Set<String> inferredCols = namespace.getInferredColumns();
4159
4160                if (inferredCols != null && !inferredCols.isEmpty()) {
4161                    for (String colName : inferredCols) {
4162                        // Skip columns in EXCEPT clause
4163                        if (exceptColumns != null && !exceptColumns.isEmpty() &&
4164                            exceptColumns.contains(colName.toUpperCase())) {
4165                            continue;
4166                        }
4167                        String attrName = tablePrefix + "." + colName;
4168                        TAttributeNode.addNodeToList(
4169                            new TAttributeNode(attrName, table),
4170                            attrList
4171                        );
4172                        count++;
4173                    }
4174                } else {
4175                    Map<String, ColumnSource> columnSources = namespace.getAllColumnSources();
4176                    if (columnSources != null && !columnSources.isEmpty()) {
4177                        for (String colName : columnSources.keySet()) {
4178                            // Skip columns in EXCEPT clause
4179                            if (exceptColumns != null && !exceptColumns.isEmpty() &&
4180                                exceptColumns.contains(colName.toUpperCase())) {
4181                                continue;
4182                            }
4183                            String attrName = tablePrefix + "." + colName;
4184                            TAttributeNode.addNodeToList(
4185                                new TAttributeNode(attrName, table),
4186                                attrList
4187                            );
4188                            count++;
4189                        }
4190                    }
4191                }
4192            }
4193
4194            // If no columns for this table, add the star as fallback
4195            if (count == 0 || (namespace != null && namespace.getInferredColumns().isEmpty()
4196                              && namespace.getAllColumnSources().isEmpty())) {
4197                TAttributeNode.addNodeToList(
4198                    new TAttributeNode(tablePrefix + ".*", table),
4199                    attrList
4200                );
4201            }
4202        }
4203
4204        return count;
4205    }
4206
4207    /**
4208     * Find a table by its prefix (alias or name) in a SELECT statement.
4209     */
4210    private TTable findTableByPrefixInSelect(TSelectSqlStatement select, String prefix) {
4211        if (select == null || select.tables == null || prefix == null) return null;
4212
4213        // Normalize prefix (remove backticks, quotes, schema prefix for comparison)
4214        String normalizedPrefix = normalizeTablePrefix(prefix);
4215
4216        for (int i = 0; i < select.tables.size(); i++) {
4217            TTable table = select.tables.getTable(i);
4218            if (table == null) continue;
4219
4220            // Check alias first
4221            String alias = table.getAliasName();
4222            if (alias != null && normalizeTablePrefix(alias).equalsIgnoreCase(normalizedPrefix)) {
4223                return table;
4224            }
4225
4226            // Check table name
4227            String name = table.getName();
4228            if (name != null && normalizeTablePrefix(name).equalsIgnoreCase(normalizedPrefix)) {
4229                return table;
4230            }
4231
4232            // Check full table name (with schema)
4233            if (table.getTableName() != null) {
4234                String fullName = table.getTableName().toString();
4235                if (fullName != null && normalizeTablePrefix(fullName).equalsIgnoreCase(normalizedPrefix)) {
4236                    return table;
4237                }
4238            }
4239        }
4240
4241        return null;
4242    }
4243
4244    /**
4245     * Normalize table prefix for comparison (remove quotes, backticks).
4246     */
4247    private String normalizeTablePrefix(String prefix) {
4248        if (prefix == null) return "";
4249        String result = prefix.trim();
4250        // Remove backticks
4251        if (result.startsWith("`") && result.endsWith("`")) {
4252            result = result.substring(1, result.length() - 1);
4253        }
4254        // Remove double quotes
4255        if (result.startsWith("\"") && result.endsWith("\"")) {
4256            result = result.substring(1, result.length() - 1);
4257        }
4258        // Remove brackets
4259        if (result.startsWith("[") && result.endsWith("]")) {
4260            result = result.substring(1, result.length() - 1);
4261        }
4262        return result;
4263    }
4264
4265    /**
4266     * Get resolution statistics
4267     */
4268    public ResolutionStatistics getStatistics() {
4269        return resolutionContext.getStatistics();
4270    }
4271
4272    /**
4273     * Get the resolution context (for advanced queries)
4274     */
4275    public ResolutionContext getContext() {
4276        return resolutionContext;
4277    }
4278
4279    /**
4280     * Get the global scope
4281     */
4282    public GlobalScope getGlobalScope() {
4283        return globalScope;
4284    }
4285
4286    /**
4287     * Get the configuration
4288     */
4289    public TSQLResolverConfig getConfig() {
4290        return config;
4291    }
4292
4293    /**
4294     * Get the pass history (for iterative resolution analysis)
4295     *
4296     * @return list of all resolution passes (empty if non-iterative or not yet resolved)
4297     */
4298    public List<ResolutionPass> getPassHistory() {
4299        return new ArrayList<>(passHistory);
4300    }
4301
4302    /**
4303     * Get the convergence detector (for iterative resolution analysis)
4304     *
4305     * @return convergence detector (null if iterative resolution is disabled)
4306     */
4307    public ConvergenceDetector getConvergenceDetector() {
4308        return convergenceDetector;
4309    }
4310
4311    /**
4312     * Get the scope build result (for testing and analysis)
4313     *
4314     * @return scope build result from ScopeBuilder (null if not yet resolved)
4315     */
4316    public ScopeBuildResult getScopeBuildResult() {
4317        return scopeBuildResult;
4318    }
4319
4320    /**
4321     * Get the resolution result access interface.
4322     * This provides a clean, statement-centric API for accessing resolution results.
4323     *
4324     * <p>Usage example:</p>
4325     * <pre>
4326     * TSQLResolver2 resolver = new TSQLResolver2(null, parser.sqlstatements);
4327     * resolver.resolve();
4328     *
4329     * IResolutionResult result = resolver.getResult();
4330     *
4331     * for (TCustomSqlStatement stmt : parser.sqlstatements) {
4332     *     for (TTable table : result.getTables(stmt)) {
4333     *         System.out.println("Table: " + table.getFullName());
4334     *         for (TObjectName col : result.getColumnsForTable(stmt, table)) {
4335     *             System.out.println("  Column: " + col.getColumnNameOnly());
4336     *         }
4337     *     }
4338     * }
4339     * </pre>
4340     *
4341     * @return resolution result access interface
4342     * @throws IllegalStateException if resolve() has not been called
4343     */
4344    public IResolutionResult getResult() {
4345        if (scopeBuildResult == null) {
4346            throw new IllegalStateException(
4347                "Must call resolve() before getResult()");
4348        }
4349        return new ResolutionResultImpl(scopeBuildResult, sqlStatements);
4350    }
4351
4352    // ===== Star Column Reverse Inference Support (Principle 3) =====
4353
4354    /**
4355     * Star Column push-down context for reverse inference.
4356     * Tracks which columns should be added to which Namespaces based on
4357     * outer layer references.
4358     */
4359    private static class StarPushDownContext {
4360        /** Namespace -> (ColumnName -> Confidence) */
4361        private final Map<INamespace, Map<String, Double>> pushDownMap = new HashMap<>();
4362
4363        /**
4364         * Record that a column should be added to a namespace.
4365         * If the same column is pushed multiple times, keep the highest confidence.
4366         */
4367        public void pushColumn(INamespace namespace, String columnName, double confidence) {
4368            Map<String, Double> columns = pushDownMap.computeIfAbsent(namespace, k -> new HashMap<>());
4369            columns.put(columnName, Math.max(confidence, columns.getOrDefault(columnName, 0.0)));
4370        }
4371
4372        /**
4373         * Get all columns that should be pushed to each namespace.
4374         */
4375        public Map<INamespace, java.util.Set<String>> getAllPushDownColumns() {
4376            Map<INamespace, java.util.Set<String>> result = new HashMap<>();
4377            for (Map.Entry<INamespace, Map<String, Double>> entry : pushDownMap.entrySet()) {
4378                result.put(entry.getKey(), entry.getValue().keySet());
4379            }
4380            return result;
4381        }
4382
4383        /**
4384         * Get the confidence score for a specific column in a namespace.
4385         */
4386        public double getConfidence(INamespace namespace, String columnName) {
4387            return pushDownMap.getOrDefault(namespace, java.util.Collections.emptyMap())
4388                              .getOrDefault(columnName, 0.0);
4389        }
4390
4391        /**
4392         * Get the total number of columns to be pushed down across all namespaces.
4393         */
4394        public int getTotalPushedColumns() {
4395            return pushDownMap.values().stream()
4396                              .mapToInt(Map::size)
4397                              .sum();
4398        }
4399    }
4400
4401    /**
4402     * Represents a star column source (CTE or subquery with SELECT *).
4403     * Used for reverse inference to track which columns are required from the star.
4404     */
4405    private static class StarColumnSource {
4406        private final String name;  // CTE name or subquery alias
4407        private final INamespace namespace;  // The namespace for this source
4408        private final INamespace underlyingTableNamespace;  // Namespace of the table behind SELECT *
4409        private final java.util.Set<String> requiredColumns = new java.util.HashSet<>();
4410
4411        public StarColumnSource(String name, INamespace namespace, INamespace underlyingTableNamespace) {
4412            this.name = name;
4413            this.namespace = namespace;
4414            this.underlyingTableNamespace = underlyingTableNamespace;
4415        }
4416
4417        public String getName() {
4418            return name;
4419        }
4420
4421        public INamespace getNamespace() {
4422            return namespace;
4423        }
4424
4425        public void addRequiredColumn(String columnName) {
4426            requiredColumns.add(columnName);
4427        }
4428
4429        public java.util.Set<String> getRequiredColumns() {
4430            return requiredColumns;
4431        }
4432
4433        public boolean hasUnderlyingTable() {
4434            return underlyingTableNamespace != null;
4435        }
4436
4437        public INamespace getUnderlyingTableNamespace() {
4438            return underlyingTableNamespace;
4439        }
4440
4441        @Override
4442        public String toString() {
4443            return String.format("StarColumnSource[%s, required=%d]", name, requiredColumns.size());
4444        }
4445    }
4446
4447    /**
4448     * Collect all star column sources (CTEs and subqueries with SELECT *).
4449     * Traverses the scope tree to find CTENamespace and SubqueryNamespace
4450     * that use SELECT * in their subqueries.
4451     */
4452    private List<StarColumnSource> collectAllStarColumnSources() {
4453        List<StarColumnSource> sources = new ArrayList<>();
4454
4455        // Traverse global scope tree
4456        if (globalScope != null) {
4457            collectStarSourcesFromScope(globalScope, sources);
4458        }
4459
4460        // Also traverse UPDATE scopes (for Teradata UPDATE...FROM syntax)
4461        if (scopeBuilder != null) {
4462            for (UpdateScope updateScope : scopeBuilder.getUpdateScopeMap().values()) {
4463                collectStarSourcesFromScope(updateScope, sources);
4464            }
4465            for (DeleteScope deleteScope : scopeBuilder.getDeleteScopeMap().values()) {
4466                collectStarSourcesFromScope(deleteScope, sources);
4467            }
4468        }
4469
4470        logDebug("Collected " + sources.size() + " star column sources");
4471        return sources;
4472    }
4473
4474    /**
4475     * Recursively collect star column sources from a scope and its children.
4476     */
4477    private void collectStarSourcesFromScope(IScope scope, List<StarColumnSource> sources) {
4478        // Check all child namespaces in this scope
4479        for (gudusoft.gsqlparser.resolver2.model.ScopeChild child : scope.getChildren()) {
4480            INamespace namespace = child.getNamespace();
4481
4482            // Use the new interface method to check for star columns
4483            if (namespace.hasStarColumn()) {
4484                TSelectSqlStatement selectStmt = namespace.getSelectStatement();
4485                INamespace underlyingNs = selectStmt != null ? getFirstTableNamespace(selectStmt) : null;
4486
4487                StarColumnSource starSource = new StarColumnSource(
4488                    namespace.getDisplayName(),
4489                    namespace,
4490                    underlyingNs
4491                );
4492                sources.add(starSource);
4493
4494                logDebug("Found star source: " + namespace.getDisplayName());
4495            }
4496        }
4497
4498        // Recursively traverse child scopes based on scope type
4499        if (scope instanceof SelectScope) {
4500            SelectScope selectScope = (SelectScope) scope;
4501            if (selectScope.getFromScope() != null) {
4502                collectStarSourcesFromScope(selectScope.getFromScope(), sources);
4503            }
4504        } else if (scope instanceof UpdateScope) {
4505            UpdateScope updateScope = (UpdateScope) scope;
4506            if (updateScope.getFromScope() != null) {
4507                collectStarSourcesFromScope(updateScope.getFromScope(), sources);
4508            }
4509        } else if (scope instanceof DeleteScope) {
4510            DeleteScope deleteScope = (DeleteScope) scope;
4511            if (deleteScope.getFromScope() != null) {
4512                collectStarSourcesFromScope(deleteScope.getFromScope(), sources);
4513            }
4514        }
4515    }
4516
4517
4518    /**
4519     * Get the first table namespace from a SELECT statement's FROM clause.
4520     * Returns the DynamicStarSource if available.
4521     */
4522    private INamespace getFirstTableNamespace(TSelectSqlStatement select) {
4523        if (select == null || select.tables == null || select.tables.size() == 0) {
4524            return null;
4525        }
4526
4527        // Get first table
4528        TTable firstTable = select.tables.getTable(0);
4529        String tableName = firstTable.getAliasName() != null
4530            ? firstTable.getAliasName()
4531            : firstTable.getName();
4532
4533        // Search for corresponding namespace in all dynamic namespaces
4534        List<INamespace> dynamicNamespaces = getAllDynamicNamespaces();
4535        for (INamespace ns : dynamicNamespaces) {
4536            if (ns.getDisplayName().equals(tableName)) {
4537                return ns;
4538            }
4539        }
4540
4541        return null;
4542    }
4543
4544    /**
4545     * Collect all outer references to a star column source.
4546     * Searches through allColumnReferences for columns that reference this star source.
4547     */
4548    private List<TObjectName> collectOuterReferencesToSource(StarColumnSource starSource) {
4549        List<TObjectName> references = new ArrayList<>();
4550
4551        if (starSource == null || starSource.getName() == null) {
4552            return references;
4553        }
4554
4555        String sourceName = starSource.getName();
4556
4557        // Search through all collected column references
4558        for (TObjectName objName : allColumnReferences) {
4559            if (objName == null) {
4560                continue;
4561            }
4562
4563            // Check if this column reference is from the star source
4564            // E.g., for CTE named "my_cte", check if objName is like "my_cte.col1"
4565            String tableQualifier = getTableQualifier(objName);
4566
4567            if (tableQualifier != null && tableQualifier.equalsIgnoreCase(sourceName)) {
4568                references.add(objName);
4569                logDebug("Found outer reference: " + objName + " -> " + sourceName);
4570            }
4571        }
4572
4573        logDebug("Collected " + references.size() + " outer references for: " + sourceName);
4574        return references;
4575    }
4576
4577    /**
4578     * Get the table qualifier from a TObjectName.
4579     * E.g., for "schema.table.column", returns "table"
4580     * E.g., for "table.column", returns "table"
4581     * E.g., for "column", returns null
4582     */
4583    private String getTableQualifier(TObjectName objName) {
4584        if (objName == null) {
4585            return null;
4586        }
4587
4588        // TObjectName has parts like: [schema, table, column]
4589        // or [table, column]
4590        // or [column]
4591
4592        // If there are 3 or more parts, the second-to-last is the table
4593        // If there are 2 parts, the first is the table
4594        // If there is 1 part, there's no table qualifier
4595
4596        String fullName = objName.toString();
4597        String[] parts = fullName.split("\\.");
4598
4599        if (parts.length >= 3) {
4600            // schema.table.column -> return table
4601            return parts[parts.length - 2];
4602        } else if (parts.length == 2) {
4603            // table.column -> return table
4604            return parts[0];
4605        } else {
4606            // Just column name, no qualifier
4607            return null;
4608        }
4609    }
4610
4611    /**
4612     * Get all DynamicStarSource namespaces from the scope tree.
4613     * This is used to apply inference results to namespaces that need enhancement.
4614     */
4615    private List<INamespace> getAllDynamicNamespaces() {
4616        List<INamespace> result = new ArrayList<>();
4617
4618        // Collect from global scope tree
4619        if (globalScope != null) {
4620            collectDynamicNamespacesFromScope(globalScope, result);
4621        }
4622
4623        return result;
4624    }
4625
4626    /**
4627     * Recursively collect DynamicStarSource namespaces from a scope and its children.
4628     */
4629    private void collectDynamicNamespacesFromScope(IScope scope, List<INamespace> result) {
4630        if (scope == null) {
4631            return;
4632        }
4633
4634        // Get all child namespaces from this scope
4635        for (gudusoft.gsqlparser.resolver2.model.ScopeChild child : scope.getChildren()) {
4636            INamespace namespace = child.getNamespace();
4637            if (namespace instanceof gudusoft.gsqlparser.resolver2.namespace.DynamicStarSource) {
4638                result.add(namespace);
4639                logDebug("Found DynamicStarSource: " + namespace.getDisplayName());
4640            }
4641        }
4642
4643        // Recursively traverse child scopes based on scope type
4644        if (scope instanceof SelectScope) {
4645            SelectScope selectScope = (SelectScope) scope;
4646
4647            // Traverse FROM scope
4648            if (selectScope.getFromScope() != null) {
4649                collectDynamicNamespacesFromScope(selectScope.getFromScope(), result);
4650            }
4651        } else if (scope instanceof CTEScope) {
4652            CTEScope cteScope = (CTEScope) scope;
4653
4654            // CTEs are already included in the children check above
4655            // But we need to check their subqueries by traversing nested scopes
4656            // The CTE namespaces themselves contain references to subquery scopes
4657        } else if (scope instanceof FromScope) {
4658            FromScope fromScope = (FromScope) scope;
4659
4660            // FROM scope children are already checked above
4661            // No additional child scopes to traverse
4662        } else if (scope instanceof GroupByScope) {
4663            GroupByScope groupByScope = (GroupByScope) scope;
4664
4665            // GroupBy scope typically doesn't have child scopes
4666        } else if (scope instanceof HavingScope) {
4667            HavingScope havingScope = (HavingScope) scope;
4668
4669            // Having scope typically doesn't have child scopes
4670        } else if (scope instanceof OrderByScope) {
4671            OrderByScope orderByScope = (OrderByScope) scope;
4672
4673            // OrderBy scope typically doesn't have child scopes
4674        }
4675
4676        // Additionally, traverse parent-child scope relationships
4677        // by checking if any of the namespaces contain nested SELECT statements
4678        for (gudusoft.gsqlparser.resolver2.model.ScopeChild child : scope.getChildren()) {
4679            INamespace namespace = child.getNamespace();
4680
4681            // If this is a SubqueryNamespace, it contains a SELECT with its own scope tree
4682            if (namespace instanceof gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) {
4683                // Subquery scopes are processed during scope building
4684                // and would be in statementScopeCache if we tracked them
4685            }
4686        }
4687    }
4688
4689    // ===== Logging helpers =====
4690
4691    private void logInfo(String message) {
4692        TBaseType.log("[TSQLResolver2] " + message, TLog.INFO);
4693    }
4694
4695    private void logDebug(String message) {
4696        TBaseType.log("[TSQLResolver2] " + message, TLog.DEBUG);
4697    }
4698
4699    private void logError(String message) {
4700        TBaseType.log("[TSQLResolver2] " + message, TLog.ERROR);
4701    }
4702}