001package gudusoft.gsqlparser.resolver2;
002
003import gudusoft.gsqlparser.*;
004import gudusoft.gsqlparser.compiler.TContext;
005import gudusoft.gsqlparser.nodes.*;
006import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher;
007import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
008import gudusoft.gsqlparser.resolver2.namespace.CTENamespace;
009import gudusoft.gsqlparser.resolver2.namespace.INamespace;
010import gudusoft.gsqlparser.resolver2.namespace.PivotNamespace;
011import gudusoft.gsqlparser.resolver2.namespace.PlsqlVariableNamespace;
012import gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace;
013import gudusoft.gsqlparser.resolver2.namespace.TableNamespace;
014import gudusoft.gsqlparser.resolver2.namespace.UnionNamespace;
015import gudusoft.gsqlparser.resolver2.namespace.UnnestNamespace;
016import gudusoft.gsqlparser.resolver2.namespace.ValuesNamespace;
017import gudusoft.gsqlparser.resolver2.scope.*;
018import gudusoft.gsqlparser.resolver2.model.ScopeChild;
019import gudusoft.gsqlparser.sqlenv.TSQLEnv;
020import gudusoft.gsqlparser.stmt.TCommonBlock;
021import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement;
022import gudusoft.gsqlparser.stmt.TCreateTriggerStmt;
023import gudusoft.gsqlparser.stmt.TAlterTableStatement;
024import gudusoft.gsqlparser.stmt.TCreateIndexSqlStatement;
025import gudusoft.gsqlparser.stmt.TDeleteSqlStatement;
026import gudusoft.gsqlparser.stmt.TDropIndexSqlStatement;
027import gudusoft.gsqlparser.stmt.TInsertSqlStatement;
028import gudusoft.gsqlparser.stmt.TSelectSqlStatement;
029import gudusoft.gsqlparser.stmt.TUpdateSqlStatement;
030import gudusoft.gsqlparser.stmt.TMergeSqlStatement;
031import gudusoft.gsqlparser.stmt.TExecImmeStmt;
032import gudusoft.gsqlparser.stmt.TVarDeclStmt;
033import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateProcedure;
034import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateFunction;
035import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreateTrigger;
036import gudusoft.gsqlparser.stmt.oracle.TPlsqlCreatePackage;
037import gudusoft.gsqlparser.resolver2.namespace.OraclePackageNamespace;
038import gudusoft.gsqlparser.stmt.TCreateProcedureStmt;
039import gudusoft.gsqlparser.stmt.TCreateFunctionStmt;
040import gudusoft.gsqlparser.stmt.TBlockSqlStatement;
041import gudusoft.gsqlparser.stmt.mysql.TMySQLCreateProcedure;
042import gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare;
043import gudusoft.gsqlparser.stmt.mssql.TMssqlReturn;
044import gudusoft.gsqlparser.stmt.db2.TDb2SqlVariableDeclaration;
045import gudusoft.gsqlparser.stmt.db2.TDb2DeclareCursorStatement;
046import gudusoft.gsqlparser.stmt.db2.TDb2CreateFunction;
047import gudusoft.gsqlparser.stmt.db2.TDb2ReturnStmt;
048import gudusoft.gsqlparser.stmt.TForStmt;
049import gudusoft.gsqlparser.nodes.teradata.TTDUnpivot;
050import gudusoft.gsqlparser.stmt.TLoopStmt;
051import gudusoft.gsqlparser.stmt.TCursorDeclStmt;
052import gudusoft.gsqlparser.stmt.TOpenforStmt;
053import gudusoft.gsqlparser.stmt.snowflake.TCreateFileFormatStmt;
054import gudusoft.gsqlparser.stmt.snowflake.TCreateStageStmt;
055import gudusoft.gsqlparser.stmt.snowflake.TCreatePipeStmt;
056import gudusoft.gsqlparser.nodes.TDeclareVariable;
057import gudusoft.gsqlparser.nodes.mssql.TMssqlCreateTriggerUpdateColumn;
058import gudusoft.gsqlparser.util.TBuiltFunctionUtil;
059import gudusoft.gsqlparser.util.TNiladicFunctionUtil;
060
061import java.util.*;
062
063/**
064 * Builds a complete scope tree using the Visitor pattern.
065 *
066 * <p>This class traverses the AST and creates a properly nested scope tree
067 * that reflects the SQL structure. All SELECT statements (including subqueries
068 * and CTEs) get their own SelectScope with correct parent-child relationships.
069 *
070 * <p>Key features:
071 * <ul>
072 *   <li>Handles nested subqueries with correct parent scope</li>
073 *   <li>Handles CTEs with forward reference support</li>
074 *   <li>Collects all column references with their scope mappings</li>
075 *   <li>Supports complex SQL patterns (CTE + subquery combinations)</li>
076 * </ul>
077 *
078 * <p>Usage:
079 * <pre>
080 * ScopeBuilder builder = new ScopeBuilder(context, nameMatcher);
081 * ScopeBuildResult result = builder.build(statements);
082 * </pre>
083 */
084public class ScopeBuilder extends TParseTreeVisitor {
085
086    // ========== Configuration ==========
087
088    /** Global context from parser */
089    private final TContext globalContext;
090
091    /** Name matcher for case sensitivity */
092    private final INameMatcher nameMatcher;
093
094    /** SQL environment for table metadata lookup */
095    private TSQLEnv sqlEnv;
096
097    /** Database vendor */
098    private EDbVendor dbVendor = EDbVendor.dbvoracle;
099
100    /**
101     * Strategy for handling ambiguous columns.
102     * -1 means use global TBaseType.GUESS_COLUMN_STRATEGY.
103     * This value is passed to namespaces for config-based isolation.
104     */
105    private int guessColumnStrategy = -1;
106
107    // ========== Scope Stack ==========
108
109    /** Scope stack - tracks current scope during traversal */
110    private final Stack<IScope> scopeStack = new Stack<>();
111
112    /** Global scope - root of scope tree */
113    private GlobalScope globalScope;
114
115    // ========== Result Collection ==========
116
117    /** Column reference -> Scope mapping */
118    private final Map<TObjectName, IScope> columnToScopeMap = new LinkedHashMap<>();
119
120    /** All column references in traversal order */
121    private final List<TObjectName> allColumnReferences = new ArrayList<>();
122
123    /** Statement -> SelectScope mapping */
124    private final Map<TSelectSqlStatement, SelectScope> statementScopeMap = new LinkedHashMap<>();
125
126    /** Statement -> UpdateScope mapping */
127    private final Map<TUpdateSqlStatement, UpdateScope> updateScopeMap = new LinkedHashMap<>();
128
129    /** Statement -> MergeScope mapping */
130    private final Map<TMergeSqlStatement, MergeScope> mergeScopeMap = new LinkedHashMap<>();
131
132    /** Statement -> DeleteScope mapping */
133    private final Map<TDeleteSqlStatement, DeleteScope> deleteScopeMap = new LinkedHashMap<>();
134
135    // ========== Current State ==========
136
137    /** Current SelectScope being built */
138    private SelectScope currentSelectScope;
139
140    /** Current UpdateScope being built */
141    private UpdateScope currentUpdateScope;
142
143    /** Current MergeScope being built */
144    private MergeScope currentMergeScope;
145
146    /** Current DeleteScope being built */
147    private DeleteScope currentDeleteScope;
148
149    /** Current FromScope being built */
150    private FromScope currentFromScope;
151
152    /** Stack to save/restore FromScope when processing nested subqueries */
153    private final Stack<FromScope> fromScopeStack = new Stack<>();
154
155    /** Current CTEScope being built */
156    private CTEScope currentCTEScope;
157
158    /** Depth counter for CTE definition processing.
159     *  Incremented in preVisit(TCTE), decremented in postVisit(TCTE).
160     *  When > 0, we're inside a CTE body, so CTAS target column handling should be skipped. */
161    private int cteDefinitionDepth = 0;
162
163    /** Set of TObjectName nodes that are table references (not column references) */
164    private final Set<TObjectName> tableNameReferences = new HashSet<>();
165
166    /** Set of TObjectName nodes that are SQL Server proprietary column aliases (not column references) */
167    private final Set<TObjectName> sqlServerProprietaryAliases = new HashSet<>();
168
169    /** Set of TObjectName nodes that are tuple alias columns (CTAS output columns, not references) */
170    private final Set<TObjectName> tupleAliasColumns = new HashSet<>();
171
172    /** Set of TObjectName nodes that are CTAS target columns (columns created by CREATE TABLE AS SELECT).
173     *  These include standard alias columns and simple column reference columns.
174     *  They are column DEFINITIONS that create new output columns in the target table, NOT column references.
175     *  They should NOT be re-resolved through name resolution. */
176    private final Set<TObjectName> ctasTargetColumns = new HashSet<>();
177
178    /** Set of TObjectName nodes that are VALUES table alias column definitions.
179     *  These are column NAME definitions in VALUES table alias clauses like:
180     *  VALUES (1, 'a') AS t(id, name) - 'id' and 'name' are definitions, not references.
181     *  They should NOT be collected as column references. */
182    private final Set<TObjectName> valuesTableAliasColumns = new HashSet<>();
183
184    /** Set of TObjectName nodes that are result column alias names (not column references).
185     *  These include standard AS aliases and Teradata NAMED aliases like "COUNT(1)(NAMED CNT_LOGIN)". */
186    private final Set<TObjectName> resultColumnAliasNames = new HashSet<>();
187
188    /** Set of TObjectName nodes that are SET clause target columns (UPDATE SET left-side columns).
189     *  These columns already have sourceTable correctly set to the UPDATE target table
190     *  and should NOT be re-resolved through star column push-down. */
191    private final Set<TObjectName> setClauseTargetColumns = new HashSet<>();
192
193    /** Set of TObjectName nodes that are INSERT ALL target columns (from TInsertIntoValue columnList).
194     *  These columns already have sourceTable correctly set to the INSERT target table
195     *  and should NOT be re-resolved against the subquery scope. */
196    private final Set<TObjectName> insertAllTargetColumns = new HashSet<>();
197
198    /** Set of TObjectName nodes that are function keyword arguments (e.g., SECOND in TIMESTAMP_DIFF).
199     *  These are marked during TFunctionCall traversal so they can be skipped during column collection. */
200    private final Set<TObjectName> functionKeywordArguments = new HashSet<>();
201
202    /** Set of TObjectName nodes that are PIVOT IN clause items.
203     *  These define pivot column names and should NOT be resolved as column references to the source table.
204     *  Their sourceTable is already correctly set to the pivot table by addPivotInClauseColumns(). */
205    private final Set<TObjectName> pivotInClauseColumns = new HashSet<>();
206
207    /** Set of TObjectName nodes that are UNPIVOT definition columns (value and FOR columns).
208     *  These are column DEFINITIONS that create new output columns, NOT column references.
209     *  Example: UNPIVOT (yearly_total FOR order_mode IN (...))
210     *  - yearly_total is a value column definition
211     *  - order_mode is a FOR column definition
212     *  Both should NOT be collected as column references. */
213    private final Set<TObjectName> unpivotDefinitionColumns = new HashSet<>();
214
215    /** Variable declaration names (from DECLARE statements) - these are NOT column references */
216    private final Set<TObjectName> variableDeclarationNames = new HashSet<>();
217
218    /** Lambda parameter names - these are NOT column references but local function parameters
219     *  e.g., in "transform(array, x -> x + 1)", x is a lambda parameter, not a table column */
220    private final Set<TObjectName> lambdaParameters = new HashSet<>();
221
222    /** DDL target object names - these are NOT column references but object names in DDL statements.
223     *  Examples: file format name in CREATE FILE FORMAT, stage name in CREATE STAGE, pipe name in CREATE PIPE.
224     *  These should be skipped during column collection. */
225    private final Set<TObjectName> ddlTargetNames = new HashSet<>();
226
227    /** Stack of lambda parameter name sets - used to track current lambda context during traversal.
228     *  When inside a lambda expression, the parameter names are pushed onto this stack.
229     *  This allows checking if a TObjectName is a lambda parameter without walking up the AST. */
230    private final Stack<Set<String>> lambdaParameterStack = new Stack<>();
231
232    /** Map of TSelectSqlStatement -> Set of lateral column alias names defined in its SELECT list.
233     *  Used to detect lateral column aliases (Snowflake, BigQuery) where aliases defined earlier
234     *  in the SELECT list can be referenced by later columns. */
235    private final Map<TSelectSqlStatement, Set<String>> selectLateralAliases = new LinkedHashMap<>();
236
237    /** The alias of the current result column being processed (normalized, lowercase).
238     *  Used to exclude the current result column's own alias from lateral alias matching,
239     *  since a column reference inside the expression that DEFINES an alias cannot be
240     *  a reference TO that alias - it must be a reference to the source table column.
241     *  e.g., in "CASE WHEN Model_ID = '' THEN NULL ELSE TRIM(Model_ID) END AS Model_ID",
242     *  the Model_ID references inside CASE are source table columns, not lateral alias references. */
243    private String currentResultColumnAlias = null;
244
245    /** Flag indicating if we're currently inside a result column context.
246     *  Lateral alias matching should ONLY apply inside result columns (SELECT list),
247     *  NOT in FROM clause, WHERE clause, etc. Column references in those places
248     *  are source table columns, not lateral alias references. */
249    private boolean inResultColumnContext = false;
250
251    /** Map of TTable to its SubqueryNamespace for deferred validation */
252    private final Map<TTable, SubqueryNamespace> pendingSubqueryValidation = new LinkedHashMap<>();
253
254    /** Map of TTable to its INamespace - used for legacy compatibility to fill TTable.getAttributes() */
255    private final Map<TTable, INamespace> tableToNamespaceMap = new LinkedHashMap<>();
256
257    /** Map of USING column -> right-side TTable for JOIN...USING resolution priority */
258    private final Map<TObjectName, TTable> usingColumnToRightTable = new LinkedHashMap<>();
259
260    /** Map of USING column -> left-side TTable for JOIN...USING (both tables should be reported) */
261    private final Map<TObjectName, TTable> usingColumnToLeftTable = new LinkedHashMap<>();
262
263    /** Current right-side table of a JOIN...USING clause being processed */
264    private TTable currentUsingJoinRightTable = null;
265
266    /** Current trigger target table (for SQL Server deleted/inserted virtual table resolution) */
267    private TTable currentTriggerTargetTable = null;
268
269    /** Current PL/SQL block scope being built */
270    private PlsqlBlockScope currentPlsqlBlockScope = null;
271
272    /** Stack of PL/SQL block scopes for nested blocks */
273    private final Stack<PlsqlBlockScope> plsqlBlockScopeStack = new Stack<>();
274
275    /** Stack to save/restore trigger target table for nested triggers */
276    private final Stack<TTable> triggerTargetTableStack = new Stack<>();
277
278    /** Set of TTable objects that are virtual trigger tables (deleted/inserted) that should be skipped in output */
279    private final Set<TTable> virtualTriggerTables = new HashSet<>();
280
281    /** Set of TFunctionCall objects that are table-valued functions (from FROM clause).
282     *  These should NOT be treated as column method calls in preVisit(TFunctionCall).
283     *  For example, [exce].[sampleTable]() is a table function, not column.method(). */
284    private final Set<TFunctionCall> tableValuedFunctionCalls = new HashSet<>();
285
286    /** Last table processed in the current FROM clause - used to find left side of JOIN...USING */
287    private TTable lastProcessedFromTable = null;
288
289    /** All tables processed so far in the current FROM clause - used for chained USING joins.
290     *  In a query like "t1 JOIN t2 USING (c1) JOIN t3 USING (c2)", when processing USING (c2),
291     *  both t1 and t2 should be considered as left-side tables, not just t2.
292     *  This list is reset when entering a new FROM clause. */
293    private List<TTable> currentJoinChainTables = new ArrayList<>();
294
295    /** Current CTAS target table - set when processing CREATE TABLE AS SELECT */
296    private TTable currentCTASTargetTable = null;
297
298    /** The main SELECT scope for CTAS - only result columns at this level become CTAS target columns.
299     *  This prevents subquery result columns from being incorrectly registered as CTAS target columns. */
300    private SelectScope ctasMainSelectScope = null;
301
302    /** Current UPDATE target table - set when processing UPDATE statement */
303    private TTable currentUpdateTargetTable = null;
304
305    /** Current MERGE target table - set when processing MERGE statement */
306    private TTable currentMergeTargetTable = null;
307
308    /** Current INSERT target table - set when processing INSERT statement */
309    private TTable currentInsertTargetTable = null;
310
311    /** Current DELETE target table - set when processing DELETE statement */
312    private TTable currentDeleteTargetTable = null;
313
314    /** All CTAS target tables collected during scope building */
315    private Set<TTable> allCtasTargetTables = new HashSet<>();
316
317    /** Flag to track when we're inside EXECUTE IMMEDIATE dynamic string expression */
318    private boolean insideExecuteImmediateDynamicExpr = false;
319
320    /** Counter to track when we're processing PIVOT/UNPIVOT source relations.
321     *  When > 0, subqueries should NOT be added to FromScope because they are
322     *  source tables for PIVOT/UNPIVOT, not directly visible in the outer query.
323     *  PIVOT/UNPIVOT transforms the source data and only the PIVOT/UNPIVOT table
324     *  should be visible, not the underlying source subquery. */
325    private int pivotSourceProcessingDepth = 0;
326
327    /** Set of cursor FOR loop record names (e.g., "rec" in "for rec in (SELECT ...)") */
328    private final Set<String> cursorForLoopRecordNames = new HashSet<>();
329
330    // ========== Oracle Package Support ==========
331
332    /** Registry of Oracle packages for cross-package resolution */
333    private OraclePackageRegistry packageRegistry;
334
335    /** Current package scope (when inside package body) */
336    private OraclePackageScope currentPackageScope = null;
337
338    /** Stack of package scopes for nested package handling */
339    private final Stack<OraclePackageScope> packageScopeStack = new Stack<>();
340
341    // ========== Cursor Variable Tracking ==========
342
343    /** Set of known cursor variable names (lowercase) in current scope chain */
344    private final Set<String> cursorVariableNames = new HashSet<>();
345
346    // ========== Constructor ==========
347
348    public ScopeBuilder(TContext globalContext, INameMatcher nameMatcher) {
349        this.globalContext = globalContext;
350        this.nameMatcher = nameMatcher != null ? nameMatcher : new DefaultNameMatcher();
351        // Get TSQLEnv from global context if available
352        if (globalContext != null) {
353            this.sqlEnv = globalContext.getSqlEnv();
354        }
355    }
356
357    public ScopeBuilder(TContext globalContext) {
358        this(globalContext, new DefaultNameMatcher());
359    }
360
361    /**
362     * Set the TSQLEnv for table metadata lookup.
363     * This can be used to override the TSQLEnv from globalContext.
364     *
365     * @param sqlEnv the SQL environment containing table metadata
366     */
367    public void setSqlEnv(TSQLEnv sqlEnv) {
368        this.sqlEnv = sqlEnv;
369    }
370
371    /**
372     * Get the TSQLEnv used for table metadata lookup.
373     *
374     * @return the SQL environment, or null if not set
375     */
376    public TSQLEnv getSqlEnv() {
377        return sqlEnv;
378    }
379
380    /**
381     * Set the strategy for handling ambiguous columns.
382     * This value will be passed to namespaces for config-based isolation.
383     *
384     * @param strategy One of TBaseType.GUESS_COLUMN_STRATEGY_* constants, or -1 to use global default
385     */
386    public void setGuessColumnStrategy(int strategy) {
387        this.guessColumnStrategy = strategy;
388    }
389
390    /**
391     * Get the strategy for handling ambiguous columns.
392     *
393     * @return The strategy constant, or -1 if not set (use global default)
394     */
395    public int getGuessColumnStrategy() {
396        return guessColumnStrategy;
397    }
398
399    // ========== Main Entry Point ==========
400
401    /**
402     * Build scope tree for the given SQL statements.
403     *
404     * @param statements SQL statements to process
405     * @return Build result containing scope tree and column mappings
406     */
407    public ScopeBuildResult build(TStatementList statements) {
408        // Initialize
409        reset();
410
411        // Create global scope
412        globalScope = new GlobalScope(globalContext, nameMatcher);
413        scopeStack.push(globalScope);
414
415        // Detect database vendor
416        if (statements != null && statements.size() > 0) {
417            dbVendor = statements.get(0).dbvendor;
418        }
419
420        // Pre-traversal: Build package registry for Oracle
421        if (dbVendor == EDbVendor.dbvoracle && statements != null) {
422            packageRegistry = new OraclePackageRegistry();
423            packageRegistry.setDebug(DEBUG_SCOPE_BUILD);
424            packageRegistry.buildFromStatements(statements);
425            if (DEBUG_SCOPE_BUILD && packageRegistry.size() > 0) {
426                System.out.println("[DEBUG] Built package registry with " +
427                    packageRegistry.size() + " packages");
428            }
429        }
430
431        // Process each statement
432        if (statements != null) {
433            for (int i = 0; i < statements.size(); i++) {
434                Object stmt = statements.get(i);
435                if (DEBUG_SCOPE_BUILD) {
436                    System.out.println("[DEBUG] build(): Processing statement " + i + " of type " + stmt.getClass().getName());
437                }
438                if (stmt instanceof TParseTreeNode) {
439                    // For certain statement types, we need to call accept() to trigger preVisit()
440                    // - TCommonBlock: to set up the PL/SQL block scope
441                    // - TDb2CreateFunction: to set up the function scope and handle parameters/variables
442                    // - TPlsqlCreatePackage: to set up the package scope for package body
443                    // For other statements, use acceptChildren() to maintain original behavior
444                    if (stmt instanceof gudusoft.gsqlparser.stmt.TCommonBlock ||
445                        stmt instanceof TDb2CreateFunction ||
446                        stmt instanceof TPlsqlCreatePackage) {
447                        ((TParseTreeNode) stmt).accept(this);
448                    } else {
449                        ((TParseTreeNode) stmt).acceptChildren(this);
450                    }
451                }
452            }
453        }
454
455        // Post-process: remove function keyword arguments that were marked during traversal
456        // This is necessary because TFunctionCall is visited AFTER its child TObjectName nodes
457        if (!functionKeywordArguments.isEmpty()) {
458            allColumnReferences.removeAll(functionKeywordArguments);
459            for (TObjectName keywordArg : functionKeywordArguments) {
460                columnToScopeMap.remove(keywordArg);
461            }
462            if (DEBUG_SCOPE_BUILD) {
463                System.out.println("[DEBUG] Post-process: removed " + functionKeywordArguments.size() +
464                    " function keyword arguments from column references");
465            }
466        }
467
468        // Build result
469        return new ScopeBuildResult(
470            globalScope,
471            columnToScopeMap,
472            allColumnReferences,
473            statementScopeMap,
474            usingColumnToRightTable,
475            usingColumnToLeftTable,
476            tableToNamespaceMap,
477            allCtasTargetTables
478        );
479    }
480
481    /**
482     * Reset all state for a new build
483     */
484    private void reset() {
485        scopeStack.clear();
486        columnToScopeMap.clear();
487        allColumnReferences.clear();
488        statementScopeMap.clear();
489        updateScopeMap.clear();
490        mergeScopeMap.clear();
491        deleteScopeMap.clear();
492        tableNameReferences.clear();
493        tableValuedFunctionCalls.clear();
494        tupleAliasColumns.clear();
495        ctasTargetColumns.clear();
496        valuesTableAliasColumns.clear();
497        resultColumnAliasNames.clear();
498        functionKeywordArguments.clear();
499        pivotInClauseColumns.clear();
500        insertAllTargetColumns.clear();
501        ddlTargetNames.clear();
502        selectLateralAliases.clear();
503        fromScopeStack.clear();
504        pendingSubqueryValidation.clear();
505        tableToNamespaceMap.clear();
506        currentSelectScope = null;
507        currentUpdateScope = null;
508        currentMergeScope = null;
509        currentDeleteScope = null;
510        currentFromScope = null;
511        currentCTEScope = null;
512        cteDefinitionDepth = 0;
513        currentMergeTargetTable = null;
514        currentCTASTargetTable = null;
515        ctasMainSelectScope = null;
516        allCtasTargetTables.clear();
517        currentPlsqlBlockScope = null;
518        plsqlBlockScopeStack.clear();
519        cursorForLoopRecordNames.clear();
520        // Package support
521        if (packageRegistry != null) {
522            packageRegistry.clear();
523        }
524        packageRegistry = null;
525        currentPackageScope = null;
526        packageScopeStack.clear();
527        // Cursor variable tracking
528        cursorVariableNames.clear();
529        globalScope = null;
530    }
531
532    // ========== CREATE TABLE AS SELECT Statement ==========
533
534    @Override
535    public void preVisit(TCreateTableSqlStatement stmt) {
536        // Track CTAS target table for registering output columns
537        if (stmt.getSubQuery() != null && stmt.getTargetTable() != null) {
538            currentCTASTargetTable = stmt.getTargetTable();
539            // Also add to the set of all CTAS target tables for filtering in formatter
540            allCtasTargetTables.add(currentCTASTargetTable);
541        }
542    }
543
544    @Override
545    public void postVisit(TCreateTableSqlStatement stmt) {
546        // Clear CTAS context after processing
547        currentCTASTargetTable = null;
548        ctasMainSelectScope = null;
549    }
550
551    // ========== Constraint Columns ==========
552
553    @Override
554    public void preVisit(TConstraint constraint) {
555        // Collect FOREIGN KEY constraint column list as column references
556        // These columns belong to the table being created/altered
557        // Example: FOREIGN KEY (ProductID, SpecialOfferID) REFERENCES ...
558        // The columns ProductID, SpecialOfferID in the FK list are references to the current table
559        //
560        // NOTE: We do NOT collect the referencedColumnList (columns from REFERENCES clause)
561        // because those are already added to the referenced table's linkedColumns during
562        // TConstraint.doParse() and will be output correctly from there.
563        if (constraint.getConstraint_type() == EConstraintType.foreign_key ||
564            constraint.getConstraint_type() == EConstraintType.reference) {
565
566            TPTNodeList<TColumnWithSortOrder> columnList = constraint.getColumnList();
567            if (columnList != null) {
568                for (int i = 0; i < columnList.size(); i++) {
569                    TColumnWithSortOrder col = columnList.getElement(i);
570                    if (col != null && col.getColumnName() != null) {
571                        TObjectName colName = col.getColumnName();
572                        // Add to column references - the ownerTable is set by TConstraint.doParse()
573                        // We need to also set sourceTable for proper output
574                        if (colName.getSourceTable() == null && col.getOwnerTable() != null) {
575                            colName.setSourceTable(col.getOwnerTable());
576                        }
577                        allColumnReferences.add(colName);
578                    }
579                }
580            }
581        }
582    }
583
584    // ========== SELECT Statement ==========
585
586    // Debug flag
587    private static final boolean DEBUG_SCOPE_BUILD = false;
588
589    // Stack to save/restore pivotSourceProcessingDepth when entering subqueries
590    // This ensures subquery's own tables are added to its FromScope even when inside PIVOT source
591    private java.util.Deque<Integer> pivotSourceDepthStack = new java.util.ArrayDeque<>();
592
593    @Override
594    public void preVisit(TSelectSqlStatement stmt) {
595        // Save and reset pivotSourceProcessingDepth for subqueries inside PIVOT source.
596        // This ensures that when a PIVOT source contains a subquery, the subquery's own tables
597        // (like #sample in: SELECT * FROM (SELECT col1 FROM #sample) p UNPIVOT ...)
598        // are added to the subquery's FromScope, not filtered out by pivotSourceProcessingDepth.
599        // The depth will be restored in postVisit(TSelectSqlStatement).
600        pivotSourceDepthStack.push(pivotSourceProcessingDepth);
601        pivotSourceProcessingDepth = 0;
602
603        // Determine parent scope
604        IScope parentScope = determineParentScopeForSelect(stmt);
605
606        // Create SelectScope
607        SelectScope selectScope = new SelectScope(parentScope, stmt);
608        statementScopeMap.put(stmt, selectScope);
609
610        // Push to stack
611        scopeStack.push(selectScope);
612        currentSelectScope = selectScope;
613
614        // Track the main SELECT scope for CTAS - only the first SELECT in CTAS context
615        // Subqueries within CTAS should NOT register their result columns as CTAS target columns
616        if (currentCTASTargetTable != null && ctasMainSelectScope == null) {
617            ctasMainSelectScope = selectScope;
618        }
619
620        if (DEBUG_SCOPE_BUILD) {
621            String stmtPreview = stmt.toString().length() > 50
622                ? stmt.toString().substring(0, 50) + "..."
623                : stmt.toString();
624            System.out.println("[DEBUG] preVisit(SELECT): " + stmtPreview.replace("\n", " "));
625        }
626
627        // Collect lateral column aliases from the SELECT list (for Snowflake, BigQuery lateral alias support)
628        // These are aliases that can be referenced by later columns in the same SELECT list
629        collectLateralAliases(stmt);
630
631        // Note: FROM scope is created in preVisit(TFromClause)
632        // TSelectSqlStatement.acceptChildren() DOES visit TFromClause when
633        // TBaseType.USE_JOINEXPR_INSTEAD_OF_JOIN is true (which is the default)
634    }
635
636    @Override
637    public void postVisit(TSelectSqlStatement stmt) {
638        // Pop CTEScope if present (it was left on stack in postVisit(TCTEList))
639        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof CTEScope) {
640            scopeStack.pop();
641            currentCTEScope = findEnclosingCTEScope();
642        }
643
644        // Handle Teradata implicit derived tables for SELECT with no explicit FROM clause
645        // According to teradata_implicit_derived_tables_zh.md:
646        // When there are NO explicit tables AND exactly 1 implicit derived table,
647        // unqualified columns should be linked to that implicit derived table
648        SelectScope selectScope = statementScopeMap.get(stmt);
649        // Check if FROM scope is null OR empty (no children)
650        // The parser may create an empty FROM scope for implicit derived tables
651        boolean fromScopeEmpty = selectScope == null ||
652                                  selectScope.getFromScope() == null ||
653                                  selectScope.getFromScope().getChildren().isEmpty();
654        if (selectScope != null && fromScopeEmpty &&
655            dbVendor == EDbVendor.dbvteradata && stmt.tables != null) {
656
657            // Collect implicit derived tables (created by Phase 1 old resolver)
658            List<TTable> implicitDerivedTables = new ArrayList<>();
659            for (int i = 0; i < stmt.tables.size(); i++) {
660                TTable table = stmt.tables.getTable(i);
661                if (table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
662                    implicitDerivedTables.add(table);
663                }
664            }
665
666            // If there are implicit derived tables and FROM scope is empty,
667            // add them to the FROM scope so unqualified columns can resolve
668            if (!implicitDerivedTables.isEmpty()) {
669                // Use existing FROM scope if present, otherwise create new one
670                FromScope fromScope = selectScope.getFromScope();
671                if (fromScope == null) {
672                    fromScope = new FromScope(selectScope, stmt);
673                    selectScope.setFromScope(fromScope);
674                }
675
676                // Use a Set to avoid adding duplicate tables (same name)
677                Set<String> addedTableNames = new HashSet<>();
678                for (TTable implicitTable : implicitDerivedTables) {
679                    String tableName = implicitTable.getName();
680                    if (addedTableNames.contains(tableName.toLowerCase())) {
681                        continue; // Skip duplicate table
682                    }
683                    addedTableNames.add(tableName.toLowerCase());
684
685                    // Create TableNamespace for the implicit derived table
686                    TableNamespace tableNs = new TableNamespace(implicitTable, nameMatcher, sqlEnv);
687                    tableNs.validate();
688                    String alias = implicitTable.getAliasName();
689                    if (alias == null || alias.isEmpty()) {
690                        alias = implicitTable.getName();
691                    }
692                    fromScope.addChild(tableNs, alias, false);
693                    tableToNamespaceMap.put(implicitTable, tableNs);
694
695                    if (DEBUG_SCOPE_BUILD) {
696                        System.out.println("[DEBUG] Added Teradata implicit derived table: " + implicitTable.getName());
697                    }
698                }
699            }
700        }
701
702        // Pop SelectScope
703        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof SelectScope) {
704            scopeStack.pop();
705        }
706
707        // Restore current SelectScope
708        currentSelectScope = findEnclosingSelectScope();
709
710        // Restore pivotSourceProcessingDepth (saved in preVisit)
711        // This ensures that after exiting a subquery, we return to the correct PIVOT processing state
712        if (!pivotSourceDepthStack.isEmpty()) {
713            pivotSourceProcessingDepth = pivotSourceDepthStack.pop();
714        }
715
716        // Clear currentFromScope when exiting a truly top-level SELECT statement.
717        // This prevents the FROM scope from leaking into subsequent statements like INSERT.
718        // A truly top-level SELECT is one where:
719        // 1. The fromScopeStack is empty (no nested subquery to restore)
720        // 2. The currentSelectScope is null (no enclosing SELECT to reference)
721        // 3. We're not inside an UPDATE, DELETE, or MERGE statement that has its own FROM scope
722        // For subqueries, the FROM scope is restored via fromScopeStack in postVisit(TFromClause).
723        if (fromScopeStack.isEmpty() && currentSelectScope == null &&
724            currentUpdateScope == null && currentDeleteScope == null && currentMergeScope == null) {
725            currentFromScope = null;
726        }
727
728        // Clean up lateral aliases for this statement
729        selectLateralAliases.remove(stmt);
730    }
731
732    /**
733     * Collect lateral column aliases from a SELECT statement's result column list.
734     * Lateral column aliases are aliases that can be referenced by later columns
735     * in the same SELECT list (supported by Snowflake, BigQuery, etc.)
736     */
737    private void collectLateralAliases(TSelectSqlStatement stmt) {
738        TResultColumnList resultCols = stmt.getResultColumnList();
739        if (resultCols == null || resultCols.size() == 0) {
740            return;
741        }
742
743        Set<String> aliases = new HashSet<>();
744        for (int i = 0; i < resultCols.size(); i++) {
745            TResultColumn rc = resultCols.getResultColumn(i);
746            if (rc == null || rc.getAliasClause() == null) {
747                continue;
748            }
749
750            // Get the alias name
751            TAliasClause aliasClause = rc.getAliasClause();
752            if (aliasClause.getAliasName() != null) {
753                String aliasName = aliasClause.getAliasName().toString();
754                // Normalize the alias name (strip quotes for matching)
755                aliasName = normalizeAliasName(aliasName);
756                if (aliasName != null && !aliasName.isEmpty()) {
757                    aliases.add(aliasName.toLowerCase());
758                    if (DEBUG_SCOPE_BUILD) {
759                        System.out.println("[DEBUG] Collected lateral alias: " + aliasName);
760                    }
761                }
762            }
763        }
764
765        if (!aliases.isEmpty()) {
766            selectLateralAliases.put(stmt, aliases);
767        }
768    }
769
770    /**
771     * Normalize an alias name by stripping surrounding quotes.
772     */
773    private String normalizeAliasName(String name) {
774        if (name == null || name.isEmpty()) return name;
775        // Remove surrounding double quotes
776        if (name.startsWith("\"") && name.endsWith("\"") && name.length() > 2) {
777            return name.substring(1, name.length() - 1);
778        }
779        // Remove surrounding backticks
780        if (name.startsWith("`") && name.endsWith("`") && name.length() > 2) {
781            return name.substring(1, name.length() - 1);
782        }
783        // Remove surrounding square brackets
784        if (name.startsWith("[") && name.endsWith("]") && name.length() > 2) {
785            return name.substring(1, name.length() - 1);
786        }
787        return name;
788    }
789
790    /**
791     * Check if a column name matches a lateral alias in the current SELECT scope.
792     * This is used to filter out references to lateral column aliases.
793     */
794    private boolean isLateralColumnAlias(String columnName) {
795        if (columnName == null || columnName.isEmpty()) {
796            return false;
797        }
798
799        // Lateral aliases should ONLY apply inside result column context (SELECT list).
800        // Column references in FROM clause, WHERE clause, etc. are source table columns,
801        // not lateral alias references.
802        if (!inResultColumnContext) {
803            return false;
804        }
805
806        // Only check for lateral aliases in vendors that support them
807        // and where we want to filter them out from column references.
808        // Note: Redshift supports lateral aliases but test expects them to be reported as columns
809        if (dbVendor != EDbVendor.dbvsnowflake &&
810            dbVendor != EDbVendor.dbvbigquery &&
811            dbVendor != EDbVendor.dbvdatabricks &&
812            dbVendor != EDbVendor.dbvsparksql) {
813            return false;
814        }
815
816        // Check if current SELECT has this alias
817        if (currentSelectScope != null && currentSelectScope.getNode() instanceof TSelectSqlStatement) {
818            TSelectSqlStatement stmt = (TSelectSqlStatement) currentSelectScope.getNode();
819            Set<String> aliases = selectLateralAliases.get(stmt);
820            if (aliases != null) {
821                String normalizedName = normalizeAliasName(columnName).toLowerCase();
822                if (aliases.contains(normalizedName)) {
823                    // IMPORTANT: Exclude the current result column's own alias from lateral alias matching.
824                    // A column reference inside the expression that DEFINES an alias cannot be
825                    // a reference TO that alias - it must be a reference to the source table column.
826                    // e.g., in "CASE WHEN Model_ID = '' THEN NULL ELSE TRIM(Model_ID) END AS Model_ID",
827                    // Model_ID inside the CASE is a source table column, not a lateral alias reference.
828                    if (currentResultColumnAlias != null && normalizedName.equals(currentResultColumnAlias)) {
829                        if (DEBUG_SCOPE_BUILD) {
830                            System.out.println("[DEBUG] Skipping lateral alias check for current result column alias: " + columnName);
831                        }
832                        return false;
833                    }
834                    if (DEBUG_SCOPE_BUILD) {
835                        System.out.println("[DEBUG] Found lateral alias reference: " + columnName);
836                    }
837                    return true;
838                }
839            }
840        }
841
842        return false;
843    }
844
845    /**
846     * Determine the parent scope for a SELECT statement.
847     *
848     * <p>Rules:
849     * <ul>
850     *   <li>CTE subquery: parent is CTEScope</li>
851     *   <li>FROM subquery: parent is enclosing SelectScope</li>
852     *   <li>Scalar subquery: parent is enclosing SelectScope</li>
853     *   <li>Top-level: parent is GlobalScope</li>
854     * </ul>
855     */
856    private IScope determineParentScopeForSelect(TSelectSqlStatement stmt) {
857        // If we have a CTE scope on the stack and this is a CTE subquery,
858        // use the CTE scope as parent
859        if (currentCTEScope != null && isQueryOfCTE(stmt)) {
860            return currentCTEScope;
861        }
862
863        // Otherwise, find appropriate parent from stack
864        // Skip any non-SELECT scopes (FromScope, etc.)
865        for (int i = scopeStack.size() - 1; i >= 0; i--) {
866            IScope scope = scopeStack.get(i);
867            if (scope instanceof SelectScope || scope instanceof CTEScope ||
868                scope instanceof PlsqlBlockScope || scope instanceof GlobalScope) {
869                return scope;
870            }
871        }
872
873        return scopeStack.isEmpty() ? globalScope : scopeStack.peek();
874    }
875
876    /**
877     * Check if a SELECT statement is the query of a CTE
878     */
879    private boolean isQueryOfCTE(TSelectSqlStatement stmt) {
880        TParseTreeNode parent = stmt.getParentStmt();
881        return parent instanceof TCTE;
882    }
883
884    /**
885     * Find the enclosing SelectScope in the stack
886     */
887    private SelectScope findEnclosingSelectScope() {
888        for (int i = scopeStack.size() - 1; i >= 0; i--) {
889            IScope scope = scopeStack.get(i);
890            if (scope instanceof SelectScope) {
891                return (SelectScope) scope;
892            }
893        }
894        return null;
895    }
896
897    // ========== UPDATE Statement ==========
898
899    @Override
900    public void preVisit(TUpdateSqlStatement stmt) {
901        // Determine parent scope
902        IScope parentScope = determineParentScopeForUpdate(stmt);
903
904        // Create UpdateScope
905        UpdateScope updateScope = new UpdateScope(parentScope, stmt);
906        updateScopeMap.put(stmt, updateScope);
907
908        // Push to stack
909        scopeStack.push(updateScope);
910        currentUpdateScope = updateScope;
911
912        // Set the current UPDATE target table for SET clause column linking
913        currentUpdateTargetTable = stmt.getTargetTable();
914
915        if (DEBUG_SCOPE_BUILD) {
916            String stmtPreview = stmt.toString().length() > 50
917                ? stmt.toString().substring(0, 50) + "..."
918                : stmt.toString();
919            System.out.println("[DEBUG] preVisit(UPDATE): " + stmtPreview.replace("\n", " ") + ", parentScope=" + parentScope);
920        }
921
922        // Create FromScope for UPDATE's tables (target table + FROM clause tables)
923        // The tables will be added when the visitor visits TTable nodes via acceptChildren()
924        if (stmt.tables != null && stmt.tables.size() > 0) {
925            // Save current FROM scope if any
926            if (currentFromScope != null) {
927                fromScopeStack.push(currentFromScope);
928            }
929
930            // Create FromScope for UPDATE's tables
931            FromScope fromScope = new FromScope(updateScope, stmt.tables);
932            updateScope.setFromScope(fromScope);
933            currentFromScope = fromScope;
934
935            // Tables will be processed when acceptChildren() visits TTable nodes
936            // via preVisit(TTable) which checks currentFromScope
937        }
938
939        // Visit JOIN ON conditions by traversing relations with type ETableSource.join
940        // This ensures columns in ON clauses are collected into allColumnReferences
941        // (Similar to DELETE statement handling)
942        for (TTable relation : stmt.getRelations()) {
943            if (relation.getTableType() == ETableSource.join && relation.getJoinExpr() != null) {
944                visitJoinExprConditions(relation.getJoinExpr());
945            }
946        }
947    }
948
949    @Override
950    public void postVisit(TUpdateSqlStatement stmt) {
951        // Pop scope
952        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof UpdateScope) {
953            scopeStack.pop();
954        }
955
956        // Restore current UpdateScope
957        currentUpdateScope = findEnclosingUpdateScope();
958
959        // Clear current UPDATE target table
960        currentUpdateTargetTable = null;
961
962        // Restore FROM scope
963        if (!fromScopeStack.isEmpty()) {
964            currentFromScope = fromScopeStack.pop();
965        } else {
966            currentFromScope = null;
967        }
968    }
969
970    // ========== INSERT Statement ==========
971
972    /** Tracks the SelectScope for INSERT ALL subquery, so VALUES columns can resolve to it */
973    private SelectScope currentInsertAllSubqueryScope = null;
974
975    @Override
976    public void preVisit(TInsertSqlStatement stmt) {
977        // Set the current INSERT target table for OUTPUT clause column linking
978        currentInsertTargetTable = stmt.getTargetTable();
979
980        // Handle INSERT ALL (Oracle multi-table insert)
981        // The subquery provides source columns that are referenced in VALUES clauses
982        // We need to pre-build the subquery scope so VALUES columns can resolve
983        if (stmt.isInsertAll() && stmt.getSubQuery() != null) {
984            TSelectSqlStatement subQuery = stmt.getSubQuery();
985
986            // Determine parent scope
987            IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
988
989            // Create SelectScope for the subquery
990            SelectScope subqueryScope = new SelectScope(parentScope, subQuery);
991            currentInsertAllSubqueryScope = subqueryScope;
992
993            // Build FromScope with the subquery's tables
994            FromScope fromScope = new FromScope(subqueryScope, subQuery);
995            buildInsertAllFromScope(fromScope, subQuery);
996            subqueryScope.setFromScope(fromScope);
997
998            // Push the scope so VALUES columns can resolve against it
999            scopeStack.push(subqueryScope);
1000            currentSelectScope = subqueryScope;
1001
1002            if (DEBUG_SCOPE_BUILD) {
1003                System.out.println("[DEBUG] preVisit(INSERT ALL): Created subquery scope with " +
1004                    fromScope.getChildren().size() + " tables");
1005            }
1006        }
1007
1008        if (DEBUG_SCOPE_BUILD) {
1009            String stmtPreview = stmt.toString().length() > 50
1010                ? stmt.toString().substring(0, 50) + "..."
1011                : stmt.toString();
1012            System.out.println("[DEBUG] preVisit(INSERT): " + stmtPreview.replace("\n", " ") +
1013                ", targetTable=" + (currentInsertTargetTable != null ? currentInsertTargetTable.getName() : "null") +
1014                ", isInsertAll=" + stmt.isInsertAll());
1015        }
1016    }
1017
1018    @Override
1019    public void postVisit(TInsertSqlStatement stmt) {
1020        // Pop INSERT ALL subquery scope if we pushed one
1021        if (stmt.isInsertAll() && currentInsertAllSubqueryScope != null) {
1022            if (!scopeStack.isEmpty() && scopeStack.peek() == currentInsertAllSubqueryScope) {
1023                scopeStack.pop();
1024            }
1025            currentInsertAllSubqueryScope = null;
1026            currentSelectScope = findEnclosingSelectScope();
1027        }
1028
1029        // Clear current INSERT target table
1030        currentInsertTargetTable = null;
1031    }
1032
1033    /**
1034     * Build FromScope for INSERT ALL subquery.
1035     * This adds the subquery's FROM tables to the scope so VALUES columns can resolve.
1036     */
1037    private void buildInsertAllFromScope(FromScope fromScope, TSelectSqlStatement select) {
1038        if (select == null || select.tables == null) {
1039            return;
1040        }
1041
1042        // Add namespace for each table in the FROM clause
1043        for (int i = 0; i < select.tables.size(); i++) {
1044            TTable table = select.tables.getTable(i);
1045            if (table == null) {
1046                continue;
1047            }
1048
1049            // Skip INSERT target tables (they're in the tables list but not part of FROM)
1050            if (table.getEffectType() == ETableEffectType.tetInsert) {
1051                continue;
1052            }
1053
1054            // Create TableNamespace for this table
1055            TableNamespace tableNs = new TableNamespace(table, nameMatcher, sqlEnv);
1056            tableNs.validate();
1057
1058            // Determine alias - use table alias if available, otherwise table name
1059            String alias = table.getAliasName();
1060            if (alias == null || alias.isEmpty()) {
1061                alias = table.getName();
1062            }
1063
1064            fromScope.addChild(tableNs, alias, false);
1065            tableToNamespaceMap.put(table, tableNs);
1066
1067            if (DEBUG_SCOPE_BUILD) {
1068                System.out.println("[DEBUG] buildInsertAllFromScope: Added table " +
1069                    table.getName() + " (alias: " + alias + ") to INSERT ALL subquery scope");
1070            }
1071        }
1072    }
1073
1074    // ========== INSERT ALL Target Columns - Track columns that already have sourceTable ==========
1075
1076    @Override
1077    public void preVisit(TInsertIntoValue insertIntoValue) {
1078        // Track INSERT ALL target columns (from columnList) that already have sourceTable set
1079        // These should NOT be re-resolved against the subquery scope
1080        TObjectNameList columnList = insertIntoValue.getColumnList();
1081        if (columnList != null) {
1082            for (int i = 0; i < columnList.size(); i++) {
1083                TObjectName column = columnList.getObjectName(i);
1084                if (column != null && column.getSourceTable() != null) {
1085                    insertAllTargetColumns.add(column);
1086                    if (DEBUG_SCOPE_BUILD) {
1087                        System.out.println("[DEBUG] preVisit(TInsertIntoValue): Tracked INSERT ALL target column: " +
1088                            column.toString() + " -> " + column.getSourceTable().getName());
1089                    }
1090                }
1091            }
1092        }
1093    }
1094
1095    /**
1096     * Determine the parent scope for an UPDATE statement.
1097     */
1098    private IScope determineParentScopeForUpdate(TUpdateSqlStatement stmt) {
1099        // If we have a CTE scope on the stack, use it
1100        if (currentCTEScope != null) {
1101            return currentCTEScope;
1102        }
1103
1104        // Otherwise, find appropriate parent from stack
1105        for (int i = scopeStack.size() - 1; i >= 0; i--) {
1106            IScope scope = scopeStack.get(i);
1107            if (scope instanceof SelectScope || scope instanceof UpdateScope ||
1108                scope instanceof CTEScope || scope instanceof PlsqlBlockScope ||
1109                scope instanceof GlobalScope) {
1110                return scope;
1111            }
1112        }
1113
1114        return scopeStack.isEmpty() ? globalScope : scopeStack.peek();
1115    }
1116
1117    /**
1118     * Find the enclosing UpdateScope in the stack
1119     */
1120    private UpdateScope findEnclosingUpdateScope() {
1121        for (int i = scopeStack.size() - 1; i >= 0; i--) {
1122            IScope scope = scopeStack.get(i);
1123            if (scope instanceof UpdateScope) {
1124                return (UpdateScope) scope;
1125            }
1126        }
1127        return null;
1128    }
1129
1130    // ========== DELETE Statement ==========
1131
1132    @Override
1133    public void preVisit(TDeleteSqlStatement stmt) {
1134        // Determine parent scope
1135        IScope parentScope = determineParentScopeForDelete(stmt);
1136
1137        // Create DeleteScope
1138        DeleteScope deleteScope = new DeleteScope(parentScope, stmt);
1139        deleteScopeMap.put(stmt, deleteScope);
1140
1141        // Push to stack
1142        scopeStack.push(deleteScope);
1143        currentDeleteScope = deleteScope;
1144
1145        // Set the current DELETE target table for OUTPUT clause column linking
1146        currentDeleteTargetTable = stmt.getTargetTable();
1147
1148        if (DEBUG_SCOPE_BUILD) {
1149            String stmtPreview = stmt.toString().length() > 50
1150                ? stmt.toString().substring(0, 50) + "..."
1151                : stmt.toString();
1152            System.out.println("[DEBUG] preVisit(DELETE): " + stmtPreview.replace("\n", " ") +
1153                ", targetTable=" + (currentDeleteTargetTable != null ? currentDeleteTargetTable.getName() : "null"));
1154        }
1155
1156        // Create FromScope for DELETE's tables (target table + FROM clause tables)
1157        // The tables will be added when the visitor visits TTable nodes via acceptChildren()
1158        if (stmt.tables != null && stmt.tables.size() > 0) {
1159            // Save current FROM scope if any
1160            if (currentFromScope != null) {
1161                fromScopeStack.push(currentFromScope);
1162            }
1163
1164            // Create FromScope for DELETE's tables
1165            FromScope fromScope = new FromScope(deleteScope, stmt.tables);
1166            deleteScope.setFromScope(fromScope);
1167            currentFromScope = fromScope;
1168
1169            // Tables will be processed when acceptChildren() visits TTable nodes
1170            // via preVisit(TTable) which checks currentFromScope
1171        }
1172
1173        // Visit JOIN ON conditions by traversing relations with type ETableSource.join
1174        // Use getRelations() and getJoinExpr() instead of deprecated getReferenceJoins()/TJoin
1175        for (TTable relation : stmt.getRelations()) {
1176            if (relation.getTableType() == ETableSource.join && relation.getJoinExpr() != null) {
1177                visitJoinExprConditions(relation.getJoinExpr());
1178            }
1179        }
1180    }
1181
1182    /**
1183     * Recursively visit a TJoinExpr tree to collect column references from ON conditions.
1184     * This handles nested joins where left/right tables can themselves be join expressions.
1185     */
1186    private void visitJoinExprConditions(TJoinExpr joinExpr) {
1187        if (joinExpr == null) {
1188            return;
1189        }
1190
1191        // Visit the ON condition if present
1192        if (joinExpr.getOnCondition() != null) {
1193            joinExpr.getOnCondition().acceptChildren(this);
1194        }
1195
1196        // Recursively handle left table if it's a join
1197        TTable leftTable = joinExpr.getLeftTable();
1198        if (leftTable != null && leftTable.getTableType() == ETableSource.join && leftTable.getJoinExpr() != null) {
1199            visitJoinExprConditions(leftTable.getJoinExpr());
1200        }
1201
1202        // Recursively handle right table if it's a join
1203        TTable rightTable = joinExpr.getRightTable();
1204        if (rightTable != null && rightTable.getTableType() == ETableSource.join && rightTable.getJoinExpr() != null) {
1205            visitJoinExprConditions(rightTable.getJoinExpr());
1206        }
1207    }
1208
1209    @Override
1210    public void postVisit(TDeleteSqlStatement stmt) {
1211        // Pop scope
1212        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof DeleteScope) {
1213            scopeStack.pop();
1214        }
1215
1216        // Restore current DeleteScope
1217        currentDeleteScope = findEnclosingDeleteScope();
1218
1219        // Clear current DELETE target table
1220        currentDeleteTargetTable = null;
1221
1222        // Restore FROM scope
1223        if (!fromScopeStack.isEmpty()) {
1224            currentFromScope = fromScopeStack.pop();
1225        } else {
1226            currentFromScope = null;
1227        }
1228    }
1229
1230    // ========== OUTPUT Clause ==========
1231
1232    @Override
1233    public void preVisit(TOutputClause outputClause) {
1234        if (outputClause == null || outputClause.getSelectItemList() == null) {
1235            return;
1236        }
1237
1238        // Only process for SQL Server / Azure SQL
1239        if (dbVendor != EDbVendor.dbvmssql && dbVendor != EDbVendor.dbvazuresql) {
1240            return;
1241        }
1242
1243        // Process each column in the OUTPUT clause
1244        // These columns may reference inserted/deleted pseudo-tables
1245        // which need to be resolved to the DML statement's target table
1246        //
1247        // Parser structure for "inserted.Nurse":
1248        //   partToken = "inserted" (getColumnNameOnly() returns this)
1249        //   propertyToken = "Nurse" (the actual column name)
1250        //   objectToken = null, tableToken = null
1251        //
1252        // We need to:
1253        // 1. Detect that partToken is INSERTED/DELETED
1254        // 2. Swap tokens so partToken = "Nurse" and objectToken = "inserted"
1255        // 3. Set sourceTable to the DML target table
1256        TResultColumnList selectList = outputClause.getSelectItemList();
1257        for (int i = 0; i < selectList.size(); i++) {
1258            TResultColumn resultColumn = selectList.getResultColumn(i);
1259            if (resultColumn != null && resultColumn.getFieldAttr() != null) {
1260                TObjectName columnRef = resultColumn.getFieldAttr();
1261
1262                // Check if partToken (what parser sees as "column name") is INSERTED/DELETED
1263                TSourceToken partToken = columnRef.getPartToken();
1264                TSourceToken propertyToken = columnRef.getPropertyToken();
1265
1266                if (partToken != null && propertyToken != null) {
1267                    String partName = partToken.toString().toUpperCase();
1268                    if ("INSERTED".equals(partName) || "DELETED".equals(partName)) {
1269                        // This is an OUTPUT clause pseudo-table column like "inserted.Nurse"
1270                        // Determine which DML target table to use
1271                        TTable targetTable = null;
1272                        if (currentTriggerTargetTable != null) {
1273                            targetTable = currentTriggerTargetTable;
1274                        } else if (currentInsertTargetTable != null) {
1275                            targetTable = currentInsertTargetTable;
1276                        } else if (currentUpdateTargetTable != null) {
1277                            targetTable = currentUpdateTargetTable;
1278                        } else if (currentDeleteTargetTable != null) {
1279                            targetTable = currentDeleteTargetTable;
1280                        }
1281
1282                        if (targetTable != null) {
1283                            // Swap the tokens:
1284                            // - Move propertyToken ("Nurse") to partToken (so getColumnNameOnly() returns "Nurse")
1285                            // - Move old partToken ("inserted") to objectToken (so getTableString() returns "inserted")
1286                            // - Clear propertyToken
1287                            columnRef.setObjectToken(partToken);
1288                            columnRef.setPartToken(propertyToken);
1289                            columnRef.setPropertyToken(null);
1290
1291                            // Set the source table for this column reference
1292                            columnRef.setSourceTable(targetTable);
1293                            allColumnReferences.add(columnRef);
1294
1295                            if (DEBUG_SCOPE_BUILD) {
1296                                System.out.println("[DEBUG] OUTPUT clause column '" + columnRef.toString() +
1297                                    "' (column=" + columnRef.getColumnNameOnly() + ") linked to target table '" +
1298                                    targetTable.getName() + "'");
1299                            }
1300                        }
1301                    }
1302                }
1303            }
1304        }
1305    }
1306
1307    /**
1308     * Determine the parent scope for a DELETE statement.
1309     */
1310    private IScope determineParentScopeForDelete(TDeleteSqlStatement stmt) {
1311        // If we have a CTE scope on the stack, use it
1312        if (currentCTEScope != null) {
1313            return currentCTEScope;
1314        }
1315
1316        // Otherwise, find appropriate parent from stack
1317        for (int i = scopeStack.size() - 1; i >= 0; i--) {
1318            IScope scope = scopeStack.get(i);
1319            if (scope instanceof SelectScope || scope instanceof UpdateScope ||
1320                scope instanceof DeleteScope || scope instanceof CTEScope ||
1321                scope instanceof PlsqlBlockScope ||
1322                scope instanceof GlobalScope) {
1323                return scope;
1324            }
1325        }
1326
1327        return scopeStack.isEmpty() ? globalScope : scopeStack.peek();
1328    }
1329
1330    /**
1331     * Find the enclosing DeleteScope in the stack
1332     */
1333    private DeleteScope findEnclosingDeleteScope() {
1334        for (int i = scopeStack.size() - 1; i >= 0; i--) {
1335            IScope scope = scopeStack.get(i);
1336            if (scope instanceof DeleteScope) {
1337                return (DeleteScope) scope;
1338            }
1339        }
1340        return null;
1341    }
1342
1343    // ========== CREATE TRIGGER Statement ==========
1344
1345    @Override
1346    public void preVisit(TCreateTriggerStmt stmt) {
1347        // For SQL Server triggers, track the target table so we can resolve
1348        // deleted/inserted virtual tables to the actual trigger target table
1349        if (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) {
1350            TTable targetTable = stmt.getOnTable();
1351            if (targetTable != null) {
1352                // Save current trigger target if any (for nested triggers, though rare)
1353                if (currentTriggerTargetTable != null) {
1354                    triggerTargetTableStack.push(currentTriggerTargetTable);
1355                }
1356                currentTriggerTargetTable = targetTable;
1357
1358                if (DEBUG_SCOPE_BUILD) {
1359                    System.out.println("[DEBUG] preVisit(CREATE TRIGGER): target table = " + targetTable.getName());
1360                }
1361            }
1362        }
1363    }
1364
1365    @Override
1366    public void postVisit(TCreateTriggerStmt stmt) {
1367        // Restore previous trigger target table
1368        if (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) {
1369            if (!triggerTargetTableStack.isEmpty()) {
1370                currentTriggerTargetTable = triggerTargetTableStack.pop();
1371            } else {
1372                currentTriggerTargetTable = null;
1373            }
1374        }
1375    }
1376
1377    @Override
1378    public void preVisit(TPlsqlCreateTrigger stmt) {
1379        // Oracle trigger: track FOLLOWS trigger list names to avoid collecting as column references
1380        // These are trigger names, not column references
1381        if (stmt.getFollowsTriggerList() != null) {
1382            TObjectNameList followsList = stmt.getFollowsTriggerList();
1383            for (int i = 0; i < followsList.size(); i++) {
1384                TObjectName triggerName = followsList.getObjectName(i);
1385                if (triggerName != null) {
1386                    ddlTargetNames.add(triggerName);
1387                    if (DEBUG_SCOPE_BUILD) {
1388                        System.out.println("[DEBUG] preVisit(TPlsqlCreateTrigger): marked FOLLOWS trigger = " + triggerName);
1389                    }
1390                }
1391            }
1392        }
1393    }
1394
1395    // ========== CREATE INDEX Statement ==========
1396
1397    @Override
1398    public void preVisit(TCreateIndexSqlStatement stmt) {
1399        // Mark the table name as a table reference (not column)
1400        TObjectName tableName = stmt.getTableName();
1401        if (tableName != null) {
1402            ddlTargetNames.add(tableName);
1403            if (DEBUG_SCOPE_BUILD) {
1404                System.out.println("[DEBUG] preVisit(CREATE INDEX): marked table reference = " + tableName);
1405            }
1406        }
1407
1408        // Handle the columns in the index - these should be linked to the table
1409        if (stmt.tables != null && stmt.tables.size() > 0) {
1410            TTable targetTable = stmt.tables.getTable(0);
1411            TOrderByItemList columnList = stmt.getColumnNameList();
1412            if (columnList != null && targetTable != null) {
1413                for (int i = 0; i < columnList.size(); i++) {
1414                    TOrderByItem orderByItem = columnList.getOrderByItem(i);
1415                    if (orderByItem != null && orderByItem.getSortKey() != null) {
1416                        TExpression sortKey = orderByItem.getSortKey();
1417                        if (sortKey.getExpressionType() == EExpressionType.simple_object_name_t) {
1418                            TObjectName columnName = sortKey.getObjectOperand();
1419                            if (columnName != null) {
1420                                columnName.setSourceTable(targetTable);
1421                                allColumnReferences.add(columnName);
1422                                if (DEBUG_SCOPE_BUILD) {
1423                                    System.out.println("[DEBUG] CREATE INDEX column '" + columnName +
1424                                        "' linked to table '" + targetTable.getName() + "'");
1425                                }
1426                            }
1427                        }
1428                    }
1429                }
1430            }
1431        }
1432    }
1433
1434    // ========== DROP INDEX Statement ==========
1435
1436    @Override
1437    public void preVisit(TDropIndexSqlStatement stmt) {
1438        // Mark the table name as a table reference (not column)
1439        TObjectName tableName = stmt.getTableName();
1440        if (tableName != null) {
1441            ddlTargetNames.add(tableName);
1442            if (DEBUG_SCOPE_BUILD) {
1443                System.out.println("[DEBUG] preVisit(DROP INDEX): marked table reference = " + tableName);
1444            }
1445        }
1446
1447        // For SQL Server, also handle TDropIndexItem list
1448        TDropIndexItemList dropIndexItems = stmt.getDropIndexItemList();
1449        if (dropIndexItems != null) {
1450            for (int i = 0; i < dropIndexItems.size(); i++) {
1451                TDropIndexItem item = dropIndexItems.getDropIndexItem(i);
1452                if (item != null) {
1453                    // Mark index name to avoid being collected as column
1454                    TObjectName indexName = item.getIndexName();
1455                    if (indexName != null) {
1456                        ddlTargetNames.add(indexName);
1457                    }
1458                    // Mark table name (objectName in TDropIndexItem)
1459                    TObjectName objectName = item.getObjectName();
1460                    if (objectName != null) {
1461                        ddlTargetNames.add(objectName);
1462                        if (DEBUG_SCOPE_BUILD) {
1463                            System.out.println("[DEBUG] preVisit(DROP INDEX): marked table reference from item = " + objectName);
1464                        }
1465                    }
1466                }
1467            }
1468        }
1469    }
1470
1471    // ========== EXECUTE IMMEDIATE - Skip variable references in dynamic SQL expression ==========
1472
1473    @Override
1474    public void preVisit(TExecImmeStmt stmt) {
1475        // Set flag to indicate we're inside EXECUTE IMMEDIATE
1476        // This prevents the dynamic SQL variable from being collected as a column reference
1477        insideExecuteImmediateDynamicExpr = true;
1478        if (DEBUG_SCOPE_BUILD) {
1479            System.out.println("[DEBUG] preVisit(TExecImmeStmt): entering EXECUTE IMMEDIATE");
1480        }
1481    }
1482
1483    @Override
1484    public void postVisit(TExecImmeStmt stmt) {
1485        // Clear the flag when leaving EXECUTE IMMEDIATE
1486        insideExecuteImmediateDynamicExpr = false;
1487        if (DEBUG_SCOPE_BUILD) {
1488            System.out.println("[DEBUG] postVisit(TExecImmeStmt): leaving EXECUTE IMMEDIATE");
1489        }
1490    }
1491
1492    // ========== Cursor FOR Loop - Track record names to avoid false column detection ==========
1493
1494    @Override
1495    public void preVisit(TLoopStmt stmt) {
1496        // Track cursor FOR loop record names (e.g., "rec" in "for rec in (SELECT ...)")
1497        // These are implicitly declared record variables, and their field access like "rec.field"
1498        // should not be collected as column references
1499        if (stmt.getKind() == TLoopStmt.cursor_for_loop) {
1500            TObjectName recordName = stmt.getRecordName();
1501            if (recordName != null) {
1502                String recNameStr = recordName.toString();
1503                if (recNameStr != null && !recNameStr.isEmpty()) {
1504                    cursorForLoopRecordNames.add(recNameStr.toLowerCase(Locale.ROOT));
1505                    if (DEBUG_SCOPE_BUILD) {
1506                        System.out.println("[DEBUG] preVisit(TLoopStmt): registered cursor FOR loop record = " + recNameStr);
1507                    }
1508                }
1509            }
1510        }
1511    }
1512
1513    @Override
1514    public void postVisit(TLoopStmt stmt) {
1515        // Remove cursor FOR loop record name when leaving the loop scope
1516        if (stmt.getKind() == TLoopStmt.cursor_for_loop) {
1517            TObjectName recordName = stmt.getRecordName();
1518            if (recordName != null) {
1519                String recNameStr = recordName.toString();
1520                if (recNameStr != null && !recNameStr.isEmpty()) {
1521                    cursorForLoopRecordNames.remove(recNameStr.toLowerCase(Locale.ROOT));
1522                    if (DEBUG_SCOPE_BUILD) {
1523                        System.out.println("[DEBUG] postVisit(TLoopStmt): removed cursor FOR loop record = " + recNameStr);
1524                    }
1525                }
1526            }
1527        }
1528    }
1529
1530    // ========== Snowflake DDL Statements - Track target names to avoid false column detection ==========
1531
1532    @Override
1533    public void preVisit(TCreateFileFormatStmt stmt) {
1534        // Track file format name to avoid collecting it as a column reference
1535        if (stmt.getFileFormatName() != null) {
1536            ddlTargetNames.add(stmt.getFileFormatName());
1537            if (DEBUG_SCOPE_BUILD) {
1538                System.out.println("[DEBUG] preVisit(TCreateFileFormatStmt): marked DDL target = " + stmt.getFileFormatName());
1539            }
1540        }
1541    }
1542
1543    @Override
1544    public void preVisit(TCreateStageStmt stmt) {
1545        // Track stage name to avoid collecting it as a column reference
1546        if (stmt.getStageName() != null) {
1547            ddlTargetNames.add(stmt.getStageName());
1548            if (DEBUG_SCOPE_BUILD) {
1549                System.out.println("[DEBUG] preVisit(TCreateStageStmt): marked DDL target = " + stmt.getStageName());
1550            }
1551        }
1552    }
1553
1554    @Override
1555    public void preVisit(TCreatePipeStmt stmt) {
1556        // Track pipe name to avoid collecting it as a column reference
1557        if (stmt.getPipeName() != null) {
1558            ddlTargetNames.add(stmt.getPipeName());
1559            if (DEBUG_SCOPE_BUILD) {
1560                System.out.println("[DEBUG] preVisit(TCreatePipeStmt): marked DDL target = " + stmt.getPipeName());
1561            }
1562        }
1563    }
1564
1565    @Override
1566    public void preVisit(TAlterTableStatement stmt) {
1567        if (DEBUG_SCOPE_BUILD) {
1568            System.out.println("[DEBUG] preVisit(TAlterTableStatement): " + stmt.sqlstatementtype);
1569        }
1570        // Track columns being modified in ALTER TABLE as DDL targets
1571        // These are not column references - they're column definitions/targets
1572        if (stmt.getAlterTableOptionList() != null) {
1573            for (int i = 0; i < stmt.getAlterTableOptionList().size(); i++) {
1574                TAlterTableOption opt = stmt.getAlterTableOptionList().getAlterTableOption(i);
1575                trackAlterTableOptionColumns(opt);
1576            }
1577        }
1578    }
1579
1580    /**
1581     * Track column names in ALTER TABLE options as DDL targets.
1582     * These include: ALTER COLUMN, DROP COLUMN, CHANGE COLUMN, RENAME COLUMN, etc.
1583     */
1584    private void trackAlterTableOptionColumns(TAlterTableOption opt) {
1585        if (opt == null) return;
1586
1587        // Single column name (ALTER COLUMN, RENAME COLUMN, etc.)
1588        if (opt.getColumnName() != null) {
1589            ddlTargetNames.add(opt.getColumnName());
1590            if (DEBUG_SCOPE_BUILD) {
1591                System.out.println("[DEBUG] trackAlterTableOptionColumns: marked DDL target = " + opt.getColumnName());
1592            }
1593        }
1594
1595        // Column name list (DROP COLUMN, SET UNUSED, etc.)
1596        if (opt.getColumnNameList() != null) {
1597            for (int i = 0; i < opt.getColumnNameList().size(); i++) {
1598                TObjectName colName = opt.getColumnNameList().getObjectName(i);
1599                ddlTargetNames.add(colName);
1600                if (DEBUG_SCOPE_BUILD) {
1601                    System.out.println("[DEBUG] trackAlterTableOptionColumns: marked DDL target (list) = " + colName);
1602                }
1603            }
1604        }
1605
1606        // New column name in RENAME COLUMN (rename TO new_name)
1607        if (opt.getNewColumnName() != null) {
1608            ddlTargetNames.add(opt.getNewColumnName());
1609            if (DEBUG_SCOPE_BUILD) {
1610                System.out.println("[DEBUG] trackAlterTableOptionColumns: marked DDL target (new) = " + opt.getNewColumnName());
1611            }
1612        }
1613
1614        // Column definitions (ADD COLUMN, MODIFY COLUMN, etc.)
1615        if (opt.getColumnDefinitionList() != null) {
1616            for (int i = 0; i < opt.getColumnDefinitionList().size(); i++) {
1617                TColumnDefinition colDef = opt.getColumnDefinitionList().getColumn(i);
1618                if (colDef.getColumnName() != null) {
1619                    ddlTargetNames.add(colDef.getColumnName());
1620                    if (DEBUG_SCOPE_BUILD) {
1621                        System.out.println("[DEBUG] trackAlterTableOptionColumns: marked DDL target (def) = " + colDef.getColumnName());
1622                    }
1623                }
1624            }
1625        }
1626    }
1627
1628    // ========== SQL Server Trigger UPDATE(column) Function ==========
1629
1630    /**
1631     * Handle SQL Server trigger UPDATE(column) function.
1632     * The column inside UPDATE() should be resolved to the trigger target table.
1633     * Example: IF UPDATE(Zip) - Zip column belongs to the trigger's ON table.
1634     */
1635    @Override
1636    public void preVisit(TMssqlCreateTriggerUpdateColumn node) {
1637        if (currentTriggerTargetTable == null) {
1638            return; // Not inside a trigger context
1639        }
1640
1641        TObjectName columnName = node.getColumnName();
1642        if (columnName != null) {
1643            // Link the column to the trigger target table
1644            columnName.setSourceTable(currentTriggerTargetTable);
1645
1646            // Add to allColumnReferences if not already there
1647            if (!allColumnReferences.contains(columnName)) {
1648                allColumnReferences.add(columnName);
1649            }
1650
1651            // Map the column to the current scope
1652            IScope currentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
1653            columnToScopeMap.put(columnName, currentScope);
1654
1655            if (DEBUG_SCOPE_BUILD) {
1656                System.out.println("[DEBUG] preVisit(TMssqlCreateTriggerUpdateColumn): " +
1657                    columnName + " -> " + currentTriggerTargetTable.getFullName());
1658            }
1659        }
1660    }
1661
1662    // ========== PL/SQL Block Statement ==========
1663
1664    @Override
1665    public void preVisit(TCommonBlock stmt) {
1666        // TCommonBlock wraps TBlockSqlNode - we need to explicitly visit it
1667        TBlockSqlNode blockBody = stmt.getBlockBody();
1668        if (blockBody != null) {
1669            blockBody.accept(this);
1670        }
1671    }
1672
1673    // ========== Oracle Package Handling ==========
1674
1675    @Override
1676    public void preVisit(TPlsqlCreatePackage pkg) {
1677        // Only create scope for package body (kind_create_body)
1678        if (pkg.getKind() == TBaseType.kind_create_body) {
1679            String pkgName = pkg.getPackageName() != null
1680                ? pkg.getPackageName().getObjectString()
1681                : null;
1682            if (pkgName == null && pkg.getPackageName() != null) {
1683                pkgName = pkg.getPackageName().toString();
1684            }
1685
1686            if (pkgName != null && packageRegistry != null) {
1687                OraclePackageNamespace pkgNs = packageRegistry.getPackage(pkgName);
1688                if (pkgNs != null) {
1689                    // Determine parent scope
1690                    IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
1691
1692                    // Create and push package scope
1693                    OraclePackageScope pkgScope = new OraclePackageScope(parentScope, pkg, pkgNs);
1694                    scopeStack.push(pkgScope);
1695                    currentPackageScope = pkgScope;
1696                    packageScopeStack.push(pkgScope);
1697
1698                    if (DEBUG_SCOPE_BUILD) {
1699                        System.out.println("[DEBUG] preVisit(TPlsqlCreatePackage): Entered package body: " + pkgName +
1700                            ", members=" + pkgNs.getMembers().size());
1701                    }
1702                }
1703            }
1704        }
1705
1706        // Manually traverse child elements to avoid recursive preVisit call
1707        // (TPlsqlCreatePackage.acceptChildren calls preVisit again)
1708        if (pkg.getDeclareStatements() != null) {
1709            for (int i = 0; i < pkg.getDeclareStatements().size(); i++) {
1710                TCustomSqlStatement decl = pkg.getDeclareStatements().get(i);
1711                if (decl != null) {
1712                    decl.acceptChildren(this);
1713                }
1714            }
1715        }
1716        if (pkg.getBodyStatements() != null) {
1717            for (int i = 0; i < pkg.getBodyStatements().size(); i++) {
1718                TCustomSqlStatement bodyStmt = pkg.getBodyStatements().get(i);
1719                if (bodyStmt != null) {
1720                    bodyStmt.acceptChildren(this);
1721                }
1722            }
1723        }
1724    }
1725
1726    @Override
1727    public void postVisit(TPlsqlCreatePackage pkg) {
1728        if (pkg.getKind() == TBaseType.kind_create_body) {
1729            if (!packageScopeStack.isEmpty() &&
1730                !scopeStack.isEmpty() &&
1731                scopeStack.peek() == packageScopeStack.peek()) {
1732                scopeStack.pop();
1733                packageScopeStack.pop();
1734                currentPackageScope = packageScopeStack.isEmpty() ? null : packageScopeStack.peek();
1735
1736                if (DEBUG_SCOPE_BUILD) {
1737                    String pkgName = pkg.getPackageName() != null
1738                        ? pkg.getPackageName().getObjectString()
1739                        : "unknown";
1740                    if (pkgName == null && pkg.getPackageName() != null) {
1741                        pkgName = pkg.getPackageName().toString();
1742                    }
1743                    System.out.println("[DEBUG] postVisit(TPlsqlCreatePackage): Exited package body: " + pkgName);
1744                }
1745            }
1746        }
1747    }
1748
1749    @Override
1750    public void preVisit(TBlockSqlNode node) {
1751        // Save current PL/SQL block scope if nested
1752        if (currentPlsqlBlockScope != null) {
1753            plsqlBlockScopeStack.push(currentPlsqlBlockScope);
1754        }
1755
1756        // Determine parent scope
1757        IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
1758
1759        // Create PL/SQL block scope
1760        PlsqlBlockScope blockScope = new PlsqlBlockScope(parentScope, node);
1761        currentPlsqlBlockScope = blockScope;
1762
1763        // Push to scope stack
1764        scopeStack.push(blockScope);
1765
1766        if (DEBUG_SCOPE_BUILD) {
1767            String label = blockScope.getBlockLabel();
1768            System.out.println("[DEBUG] preVisit(TBlockSqlNode): label=" +
1769                (label != null ? label : "(anonymous)") +
1770                ", parent=" + parentScope);
1771        }
1772
1773        // Process declare statements to collect variables
1774        // Use acceptChildren to traverse into statements like cursor declarations
1775        // that have nested SELECT statements
1776        for (TCustomSqlStatement decl : node.getDeclareStatements()) {
1777            decl.acceptChildren(this);
1778        }
1779
1780        // Process body statements
1781        for (TCustomSqlStatement bodyStmt : node.getBodyStatements()) {
1782            // Use acceptChildren to traverse into the statement and collect column references
1783            bodyStmt.acceptChildren(this);
1784        }
1785    }
1786
1787    @Override
1788    public void postVisit(TBlockSqlNode node) {
1789        // Pop from scope stack
1790        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof PlsqlBlockScope) {
1791            scopeStack.pop();
1792        }
1793
1794        // Restore previous PL/SQL block scope
1795        if (!plsqlBlockScopeStack.isEmpty()) {
1796            currentPlsqlBlockScope = plsqlBlockScopeStack.pop();
1797        } else {
1798            currentPlsqlBlockScope = null;
1799        }
1800
1801        if (DEBUG_SCOPE_BUILD) {
1802            System.out.println("[DEBUG] postVisit(TBlockSqlNode): restored scope");
1803        }
1804    }
1805
1806    @Override
1807    public void preVisit(TVarDeclStmt stmt) {
1808        if (DEBUG_SCOPE_BUILD) {
1809            String varName = stmt.getElementName() != null ? stmt.getElementName().toString() : "(unnamed)";
1810            System.out.println("[DEBUG] preVisit(TVarDeclStmt): var=" + varName +
1811                ", currentPlsqlBlockScope=" + (currentPlsqlBlockScope != null ? "exists" : "null"));
1812        }
1813
1814        // Add variable to current PL/SQL block's variable namespace
1815        if (currentPlsqlBlockScope != null) {
1816            currentPlsqlBlockScope.getVariableNamespace().addVariable(stmt);
1817
1818            // Mark the element name TObjectName as a variable declaration (not a column reference)
1819            // This prevents it from being collected in allColumnReferences
1820            if (stmt.getElementName() != null) {
1821                variableDeclarationNames.add(stmt.getElementName());
1822            }
1823        }
1824    }
1825
1826    // ========== Oracle Cursor Variable Tracking ==========
1827
1828    @Override
1829    public void preVisit(TCursorDeclStmt cursorDecl) {
1830        // Track cursor variable name for filtering during column resolution
1831        TObjectName cursorName = cursorDecl.getCursorName();
1832        if (cursorName != null) {
1833            String name = cursorName.toString();
1834            if (name != null && !name.isEmpty()) {
1835                cursorVariableNames.add(name.toLowerCase(Locale.ROOT));
1836
1837                // Also add to current PlsqlBlockScope's namespace if available
1838                if (currentPlsqlBlockScope != null) {
1839                    PlsqlVariableNamespace varNs = currentPlsqlBlockScope.getVariableNamespace();
1840                    if (varNs != null) {
1841                        // Add cursor as a parameter-like entry (not a regular variable)
1842                        varNs.addParameter(name);
1843                    }
1844                }
1845
1846                // Mark the cursor name TObjectName as not a column reference
1847                variableDeclarationNames.add(cursorName);
1848
1849                if (DEBUG_SCOPE_BUILD) {
1850                    System.out.println("[DEBUG] preVisit(TCursorDeclStmt): Registered cursor variable: " + name);
1851                }
1852            }
1853        }
1854
1855        // Process nested SELECT statement in cursor declaration
1856        if (cursorDecl.getSubquery() != null) {
1857            cursorDecl.getSubquery().acceptChildren(this);
1858        }
1859    }
1860
1861    @Override
1862    public void preVisit(TOpenforStmt openFor) {
1863        // Track the cursor variable in OPEN...FOR
1864        TObjectName cursorVar = openFor.getCursorVariableName();
1865        if (cursorVar != null) {
1866            String name = cursorVar.toString();
1867            if (name != null && !name.isEmpty()) {
1868                cursorVariableNames.add(name.toLowerCase(Locale.ROOT));
1869                if (DEBUG_SCOPE_BUILD) {
1870                    System.out.println("[DEBUG] preVisit(TOpenforStmt): OPEN FOR cursor variable: " + name);
1871                }
1872            }
1873        }
1874
1875        // Process the FOR subquery
1876        if (openFor.getSubquery() != null) {
1877            openFor.getSubquery().acceptChildren(this);
1878        }
1879    }
1880
1881    // ========== MySQL/MSSQL DECLARE Statement ==========
1882
1883    @Override
1884    public void preVisit(TMssqlDeclare stmt) {
1885        // Handle DECLARE statements for MySQL/MSSQL
1886        // These are in bodyStatements (not declareStatements) for MySQL procedures
1887        if (currentPlsqlBlockScope != null && stmt.getVariables() != null) {
1888            for (int i = 0; i < stmt.getVariables().size(); i++) {
1889                TDeclareVariable declVar = stmt.getVariables().getDeclareVariable(i);
1890                if (declVar != null && declVar.getVariableName() != null) {
1891                    // Mark the variable name as a declaration so it won't be collected as a column reference
1892                    variableDeclarationNames.add(declVar.getVariableName());
1893                    // Also add the variable name to the namespace
1894                    currentPlsqlBlockScope.getVariableNamespace().addParameter(declVar.getVariableName().toString());
1895
1896                    if (DEBUG_SCOPE_BUILD) {
1897                        System.out.println("[DEBUG] preVisit(TMssqlDeclare): added var=" + declVar.getVariableName().toString());
1898                    }
1899                }
1900            }
1901        }
1902    }
1903
1904    // ========== DB2 DECLARE Statement ==========
1905
1906    @Override
1907    public void preVisit(TDb2SqlVariableDeclaration stmt) {
1908        // Handle DECLARE statements for DB2 procedures
1909        // These are in declareStatements for DB2 procedures
1910        if (currentPlsqlBlockScope != null && stmt.getVariables() != null) {
1911            for (int i = 0; i < stmt.getVariables().size(); i++) {
1912                TDeclareVariable declVar = stmt.getVariables().getDeclareVariable(i);
1913                if (declVar != null && declVar.getVariableName() != null) {
1914                    // Mark the variable name as a declaration so it won't be collected as a column reference
1915                    variableDeclarationNames.add(declVar.getVariableName());
1916                    // Also add the variable name to the namespace
1917                    currentPlsqlBlockScope.getVariableNamespace().addParameter(declVar.getVariableName().toString());
1918
1919                    if (DEBUG_SCOPE_BUILD) {
1920                        System.out.println("[DEBUG] preVisit(TDb2SqlVariableDeclaration): added var=" + declVar.getVariableName().toString());
1921                    }
1922                }
1923            }
1924        }
1925    }
1926
1927    // ========== DB2 DECLARE CURSOR Statement ==========
1928
1929    @Override
1930    public void preVisit(TDb2DeclareCursorStatement stmt) {
1931        // DB2 DECLARE CURSOR statements contain a SELECT subquery
1932        // The default acceptChildren only calls subquery.accept() which doesn't traverse the SELECT's children
1933        // We need to manually traverse the subquery's children to collect column references
1934        if (stmt.getSubquery() != null) {
1935            if (DEBUG_SCOPE_BUILD) {
1936                System.out.println("[DEBUG] preVisit(TDb2DeclareCursorStatement): traversing subquery");
1937            }
1938            stmt.getSubquery().acceptChildren(this);
1939        }
1940    }
1941
1942    // ========== DB2 CREATE FUNCTION Statement ==========
1943
1944    @Override
1945    public void preVisit(TDb2CreateFunction stmt) {
1946        // Save current PL/SQL block scope if nested
1947        if (currentPlsqlBlockScope != null) {
1948            plsqlBlockScopeStack.push(currentPlsqlBlockScope);
1949        }
1950
1951        // Determine parent scope
1952        IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
1953
1954        // Get function name for the scope label
1955        String functionName = null;
1956        if (stmt.getFunctionName() != null) {
1957            functionName = stmt.getFunctionName().toString();
1958        }
1959
1960        // Create PL/SQL block scope using the function name as the label
1961        PlsqlBlockScope blockScope = new PlsqlBlockScope(parentScope, stmt, functionName);
1962        currentPlsqlBlockScope = blockScope;
1963
1964        // Push to scope stack
1965        scopeStack.push(blockScope);
1966
1967        // Add function parameters to the variable namespace
1968        if (stmt.getParameterDeclarations() != null) {
1969            for (int i = 0; i < stmt.getParameterDeclarations().size(); i++) {
1970                TParameterDeclaration param = stmt.getParameterDeclarations().getParameterDeclarationItem(i);
1971                if (param != null && param.getParameterName() != null) {
1972                    variableDeclarationNames.add(param.getParameterName());
1973                    blockScope.getVariableNamespace().addParameter(param.getParameterName().toString());
1974                }
1975            }
1976        }
1977
1978        if (DEBUG_SCOPE_BUILD) {
1979            System.out.println("[DEBUG] preVisit(TDb2CreateFunction): name=" +
1980                (functionName != null ? functionName : "anonymous"));
1981        }
1982
1983        // Note: We do NOT manually process declareStatements and bodyStatements here.
1984        // The natural visitor flow (acceptChildren) will visit them after preVisit returns.
1985        // When TDb2SqlVariableDeclaration nodes are visited, preVisit(TDb2SqlVariableDeclaration)
1986        // will add them to currentPlsqlBlockScope's namespace.
1987    }
1988
1989    @Override
1990    public void postVisit(TDb2CreateFunction stmt) {
1991        // Pop from scope stack
1992        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof PlsqlBlockScope) {
1993            scopeStack.pop();
1994        }
1995
1996        // Restore parent PL/SQL block scope
1997        if (!plsqlBlockScopeStack.isEmpty()) {
1998            currentPlsqlBlockScope = plsqlBlockScopeStack.pop();
1999        } else {
2000            currentPlsqlBlockScope = null;
2001        }
2002
2003        if (DEBUG_SCOPE_BUILD) {
2004            System.out.println("[DEBUG] postVisit(TDb2CreateFunction)");
2005        }
2006    }
2007
2008    // ========== Generic CREATE FUNCTION Statement ==========
2009
2010    @Override
2011    public void preVisit(TCreateFunctionStmt stmt) {
2012        // Save current PL/SQL block scope if nested
2013        if (currentPlsqlBlockScope != null) {
2014            plsqlBlockScopeStack.push(currentPlsqlBlockScope);
2015        }
2016
2017        // Determine parent scope
2018        IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
2019
2020        // Get function name for the scope label
2021        String functionName = null;
2022        if (stmt.getFunctionName() != null) {
2023            functionName = stmt.getFunctionName().toString();
2024
2025            // Register the function in SQLEnv so it can be looked up later
2026            // This allows distinguishing schema.function() calls from column.method() calls
2027            if (sqlEnv != null) {
2028                sqlEnv.addFunction(stmt.getFunctionName(), true);
2029                if (DEBUG_SCOPE_BUILD) {
2030                    System.out.println("[DEBUG] preVisit(TCreateFunctionStmt): Registered function in SQLEnv: " + functionName);
2031                }
2032            }
2033        }
2034
2035        // Create PL/SQL block scope using the function name as the label
2036        PlsqlBlockScope blockScope = new PlsqlBlockScope(parentScope, stmt, functionName);
2037        currentPlsqlBlockScope = blockScope;
2038
2039        // Push to scope stack
2040        scopeStack.push(blockScope);
2041
2042        // Add function parameters to the variable namespace
2043        if (stmt.getParameterDeclarations() != null) {
2044            for (int i = 0; i < stmt.getParameterDeclarations().size(); i++) {
2045                TParameterDeclaration param = stmt.getParameterDeclarations().getParameterDeclarationItem(i);
2046                if (param != null && param.getParameterName() != null) {
2047                    variableDeclarationNames.add(param.getParameterName());
2048                    blockScope.getVariableNamespace().addParameter(param.getParameterName().toString());
2049                }
2050            }
2051        }
2052
2053        // Handle variable declarations in the function body
2054        // TCreateFunctionStmt.acceptChildren() doesn't visit declareStatements, so we need to do it manually
2055        if (stmt.getDeclareStatements() != null && stmt.getDeclareStatements().size() > 0) {
2056            for (int i = 0; i < stmt.getDeclareStatements().size(); i++) {
2057                TCustomSqlStatement decl = stmt.getDeclareStatements().get(i);
2058                if (decl instanceof TDb2SqlVariableDeclaration) {
2059                    TDb2SqlVariableDeclaration db2VarDecl = (TDb2SqlVariableDeclaration) decl;
2060                    if (db2VarDecl.getVariables() != null) {
2061                        for (int j = 0; j < db2VarDecl.getVariables().size(); j++) {
2062                            TDeclareVariable declVar = db2VarDecl.getVariables().getDeclareVariable(j);
2063                            if (declVar != null && declVar.getVariableName() != null) {
2064                                variableDeclarationNames.add(declVar.getVariableName());
2065                                blockScope.getVariableNamespace().addParameter(declVar.getVariableName().toString());
2066                                if (DEBUG_SCOPE_BUILD) {
2067                                    System.out.println("[DEBUG] preVisit(TCreateFunctionStmt): added var=" + declVar.getVariableName().toString());
2068                                }
2069                            }
2070                        }
2071                    }
2072                }
2073            }
2074        }
2075
2076        if (DEBUG_SCOPE_BUILD) {
2077            System.out.println("[DEBUG] preVisit(TCreateFunctionStmt): name=" +
2078                (functionName != null ? functionName : "anonymous") +
2079                ", bodyStatements=" + stmt.getBodyStatements().size() +
2080                ", blockBody=" + (stmt.getBlockBody() != null) +
2081                ", declareStatements=" + (stmt.getDeclareStatements() != null ? stmt.getDeclareStatements().size() : 0) +
2082                ", returnStmt=" + (stmt.getReturnStmt() != null));
2083        }
2084    }
2085
2086    @Override
2087    public void postVisit(TCreateFunctionStmt stmt) {
2088        // Pop from scope stack
2089        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof PlsqlBlockScope) {
2090            scopeStack.pop();
2091        }
2092
2093        // Restore parent PL/SQL block scope
2094        if (!plsqlBlockScopeStack.isEmpty()) {
2095            currentPlsqlBlockScope = plsqlBlockScopeStack.pop();
2096        } else {
2097            currentPlsqlBlockScope = null;
2098        }
2099
2100        if (DEBUG_SCOPE_BUILD) {
2101            System.out.println("[DEBUG] postVisit(TCreateFunctionStmt)");
2102        }
2103    }
2104
2105    // ========== DB2 RETURN Statement ==========
2106
2107    @Override
2108    public void preVisit(TDb2ReturnStmt stmt) {
2109        // DB2 RETURN statements can contain a SELECT subquery (for table-valued functions)
2110        // The default accept only calls subquery.accept() which doesn't traverse the SELECT's children
2111        // We need to manually traverse the subquery's children to collect column references
2112        if (stmt.getSubquery() != null) {
2113            if (DEBUG_SCOPE_BUILD) {
2114                System.out.println("[DEBUG] preVisit(TDb2ReturnStmt): traversing subquery");
2115            }
2116            stmt.getSubquery().acceptChildren(this);
2117        }
2118    }
2119
2120    // ========== MSSQL RETURN Statement (also used for DB2) ==========
2121
2122    @Override
2123    public void preVisit(TMssqlReturn stmt) {
2124        // TMssqlReturn can contain a SELECT subquery (used for DB2 table-valued functions too)
2125        // We need to traverse the subquery's children to collect column references
2126        if (stmt.getSubquery() != null) {
2127            if (DEBUG_SCOPE_BUILD) {
2128                System.out.println("[DEBUG] preVisit(TMssqlReturn): traversing subquery");
2129            }
2130            stmt.getSubquery().acceptChildren(this);
2131        }
2132    }
2133
2134    // ========== PL/SQL CREATE PROCEDURE Statement ==========
2135
2136    @Override
2137    public void preVisit(TPlsqlCreateProcedure stmt) {
2138        // Save current PL/SQL block scope if nested
2139        if (currentPlsqlBlockScope != null) {
2140            plsqlBlockScopeStack.push(currentPlsqlBlockScope);
2141        }
2142
2143        // Determine parent scope
2144        IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
2145
2146        // Get procedure name for the scope label
2147        String procedureName = null;
2148        if (stmt.getProcedureName() != null) {
2149            procedureName = stmt.getProcedureName().toString();
2150        }
2151
2152        // Create PL/SQL block scope using the procedure name as the label
2153        PlsqlBlockScope blockScope = new PlsqlBlockScope(parentScope, stmt, procedureName);
2154        currentPlsqlBlockScope = blockScope;
2155
2156        // Push to scope stack
2157        scopeStack.push(blockScope);
2158
2159        // Register procedure parameters in the variable namespace
2160        // This allows ScopeBuilder to filter them out during column reference collection
2161        TParameterDeclarationList params = stmt.getParameterDeclarations();
2162        if (params != null) {
2163            for (int i = 0; i < params.size(); i++) {
2164                TParameterDeclaration param = params.getParameterDeclarationItem(i);
2165                if (param != null && param.getParameterName() != null) {
2166                    blockScope.getVariableNamespace().addParameter(param.getParameterName().toString());
2167                }
2168            }
2169        }
2170
2171        if (DEBUG_SCOPE_BUILD) {
2172            System.out.println("[DEBUG] preVisit(TPlsqlCreateProcedure): name=" +
2173                (procedureName != null ? procedureName : "(unnamed)") +
2174                ", parent=" + parentScope +
2175                ", params=" + (params != null ? params.size() : 0));
2176        }
2177
2178        // Note: We do NOT manually process declareStatements and bodyStatements here.
2179        // The natural visitor flow (acceptChildren) will visit them after preVisit returns.
2180        // When TVarDeclStmt nodes are visited, preVisit(TVarDeclStmt) will add them
2181        // to currentPlsqlBlockScope's namespace.
2182    }
2183
2184    @Override
2185    public void postVisit(TPlsqlCreateProcedure stmt) {
2186        // Pop from scope stack
2187        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof PlsqlBlockScope) {
2188            scopeStack.pop();
2189        }
2190
2191        // Restore previous PL/SQL block scope
2192        if (!plsqlBlockScopeStack.isEmpty()) {
2193            currentPlsqlBlockScope = plsqlBlockScopeStack.pop();
2194        } else {
2195            currentPlsqlBlockScope = null;
2196        }
2197
2198        if (DEBUG_SCOPE_BUILD) {
2199            System.out.println("[DEBUG] postVisit(TPlsqlCreateProcedure): restored scope");
2200        }
2201    }
2202
2203    // ========== PL/SQL CREATE FUNCTION Statement ==========
2204
2205    @Override
2206    public void preVisit(TPlsqlCreateFunction stmt) {
2207        // Save current PL/SQL block scope if nested
2208        if (currentPlsqlBlockScope != null) {
2209            plsqlBlockScopeStack.push(currentPlsqlBlockScope);
2210        }
2211
2212        // Determine parent scope
2213        IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
2214
2215        // Get function name for the scope label
2216        String functionName = null;
2217        if (stmt.getFunctionName() != null) {
2218            functionName = stmt.getFunctionName().toString();
2219        }
2220
2221        // Create PL/SQL block scope using the function name as the label
2222        PlsqlBlockScope blockScope = new PlsqlBlockScope(parentScope, stmt, functionName);
2223        currentPlsqlBlockScope = blockScope;
2224
2225        // Push to scope stack
2226        scopeStack.push(blockScope);
2227
2228        // Register function parameters in the variable namespace
2229        // This allows ScopeBuilder to filter them out during column reference collection
2230        TParameterDeclarationList params = stmt.getParameterDeclarations();
2231        if (params != null) {
2232            for (int i = 0; i < params.size(); i++) {
2233                TParameterDeclaration param = params.getParameterDeclarationItem(i);
2234                if (param != null && param.getParameterName() != null) {
2235                    blockScope.getVariableNamespace().addParameter(param.getParameterName().toString());
2236                }
2237            }
2238        }
2239
2240        if (DEBUG_SCOPE_BUILD) {
2241            System.out.println("[DEBUG] preVisit(TPlsqlCreateFunction): name=" +
2242                (functionName != null ? functionName : "(unnamed)") +
2243                ", parent=" + parentScope +
2244                ", params=" + (params != null ? params.size() : 0));
2245        }
2246
2247        // Note: We do NOT manually process declareStatements and bodyStatements here.
2248        // The natural visitor flow (acceptChildren) will visit them after preVisit returns.
2249        // When TVarDeclStmt nodes are visited, preVisit(TVarDeclStmt) will add them
2250        // to currentPlsqlBlockScope's namespace.
2251    }
2252
2253    @Override
2254    public void postVisit(TPlsqlCreateFunction stmt) {
2255        // Pop from scope stack
2256        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof PlsqlBlockScope) {
2257            scopeStack.pop();
2258        }
2259
2260        // Restore previous PL/SQL block scope
2261        if (!plsqlBlockScopeStack.isEmpty()) {
2262            currentPlsqlBlockScope = plsqlBlockScopeStack.pop();
2263        } else {
2264            currentPlsqlBlockScope = null;
2265        }
2266
2267        if (DEBUG_SCOPE_BUILD) {
2268            System.out.println("[DEBUG] postVisit(TPlsqlCreateFunction): restored scope");
2269        }
2270    }
2271
2272    // ========== CREATE PROCEDURE Statement (generic, including MySQL) ==========
2273
2274    @Override
2275    public void preVisit(TCreateProcedureStmt stmt) {
2276        // Save current PL/SQL block scope if nested
2277        if (currentPlsqlBlockScope != null) {
2278            plsqlBlockScopeStack.push(currentPlsqlBlockScope);
2279        }
2280
2281        // Determine parent scope
2282        IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
2283
2284        // Get procedure name for the scope label
2285        String procedureName = null;
2286        if (stmt.getProcedureName() != null) {
2287            procedureName = stmt.getProcedureName().toString();
2288        }
2289
2290        // Create PL/SQL block scope using the procedure name as the label
2291        PlsqlBlockScope blockScope = new PlsqlBlockScope(parentScope, stmt, procedureName);
2292        currentPlsqlBlockScope = blockScope;
2293
2294        // Push to scope stack
2295        scopeStack.push(blockScope);
2296
2297        // Register procedure parameters in the variable namespace
2298        // This allows ScopeBuilder to filter them out during column reference collection
2299        TParameterDeclarationList params = stmt.getParameterDeclarations();
2300        if (params != null) {
2301            for (int i = 0; i < params.size(); i++) {
2302                TParameterDeclaration param = params.getParameterDeclarationItem(i);
2303                if (param != null && param.getParameterName() != null) {
2304                    blockScope.getVariableNamespace().addParameter(param.getParameterName().toString());
2305                }
2306            }
2307        }
2308
2309        if (DEBUG_SCOPE_BUILD) {
2310            System.out.println("[DEBUG] preVisit(TCreateProcedureStmt): name=" +
2311                (procedureName != null ? procedureName : "(unnamed)") +
2312                ", parent=" + parentScope +
2313                ", params=" + (params != null ? params.size() : 0));
2314        }
2315
2316        // Process declareStatements manually since TCreateProcedureStmt.acceptChildren()
2317        // doesn't traverse them. This adds variable declarations to the namespace
2318        // and marks their element names so they won't be collected as column references.
2319        for (TCustomSqlStatement decl : stmt.getDeclareStatements()) {
2320            if (decl instanceof TVarDeclStmt) {
2321                TVarDeclStmt varDecl = (TVarDeclStmt) decl;
2322                blockScope.getVariableNamespace().addVariable(varDecl);
2323                if (varDecl.getElementName() != null) {
2324                    variableDeclarationNames.add(varDecl.getElementName());
2325                }
2326                if (DEBUG_SCOPE_BUILD) {
2327                    String varName = varDecl.getElementName() != null ? varDecl.getElementName().toString() : "(unnamed)";
2328                    System.out.println("[DEBUG] TCreateProcedureStmt: added declare var=" + varName);
2329                }
2330            } else if (decl instanceof TDb2SqlVariableDeclaration) {
2331                // Handle DB2 variable declarations (DECLARE var_name TYPE)
2332                TDb2SqlVariableDeclaration db2VarDecl = (TDb2SqlVariableDeclaration) decl;
2333                if (db2VarDecl.getVariables() != null) {
2334                    for (int i = 0; i < db2VarDecl.getVariables().size(); i++) {
2335                        TDeclareVariable declVar = db2VarDecl.getVariables().getDeclareVariable(i);
2336                        if (declVar != null && declVar.getVariableName() != null) {
2337                            variableDeclarationNames.add(declVar.getVariableName());
2338                            blockScope.getVariableNamespace().addParameter(declVar.getVariableName().toString());
2339                            if (DEBUG_SCOPE_BUILD) {
2340                                System.out.println("[DEBUG] TCreateProcedureStmt (DB2): added declare var=" + declVar.getVariableName().toString());
2341                            }
2342                        }
2343                    }
2344                }
2345            }
2346            // For any declaration that might contain embedded statements (like cursor declarations),
2347            // traverse them so that column references inside are collected
2348            decl.acceptChildren(this);
2349        }
2350    }
2351
2352    @Override
2353    public void postVisit(TCreateProcedureStmt stmt) {
2354        // Pop from scope stack
2355        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof PlsqlBlockScope) {
2356            scopeStack.pop();
2357        }
2358
2359        // Restore previous PL/SQL block scope
2360        if (!plsqlBlockScopeStack.isEmpty()) {
2361            currentPlsqlBlockScope = plsqlBlockScopeStack.pop();
2362        } else {
2363            currentPlsqlBlockScope = null;
2364        }
2365
2366        if (DEBUG_SCOPE_BUILD) {
2367            System.out.println("[DEBUG] postVisit(TCreateProcedureStmt): restored scope");
2368        }
2369    }
2370
2371    // ========== MySQL CREATE PROCEDURE Statement (deprecated, but still in use) ==========
2372
2373    @Override
2374    public void preVisit(TMySQLCreateProcedure stmt) {
2375        // Save current PL/SQL block scope if nested
2376        if (currentPlsqlBlockScope != null) {
2377            plsqlBlockScopeStack.push(currentPlsqlBlockScope);
2378        }
2379
2380        // Determine parent scope
2381        IScope parentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
2382
2383        // Get procedure name for the scope label
2384        String procedureName = null;
2385        if (stmt.getProcedureName() != null) {
2386            procedureName = stmt.getProcedureName().toString();
2387        }
2388
2389        // Create PL/SQL block scope using the procedure name as the label
2390        PlsqlBlockScope blockScope = new PlsqlBlockScope(parentScope, stmt, procedureName);
2391        currentPlsqlBlockScope = blockScope;
2392
2393        // Push to scope stack
2394        scopeStack.push(blockScope);
2395
2396        // Register procedure parameters in the variable namespace
2397        TParameterDeclarationList params = stmt.getParameterDeclarations();
2398        if (params != null) {
2399            for (int i = 0; i < params.size(); i++) {
2400                TParameterDeclaration param = params.getParameterDeclarationItem(i);
2401                if (param != null && param.getParameterName() != null) {
2402                    blockScope.getVariableNamespace().addParameter(param.getParameterName().toString());
2403                }
2404            }
2405        }
2406
2407        if (DEBUG_SCOPE_BUILD) {
2408            System.out.println("[DEBUG] preVisit(TMySQLCreateProcedure): name=" +
2409                (procedureName != null ? procedureName : "(unnamed)") +
2410                ", parent=" + parentScope +
2411                ", params=" + (params != null ? params.size() : 0));
2412        }
2413    }
2414
2415    @Override
2416    public void postVisit(TMySQLCreateProcedure stmt) {
2417        // Pop from scope stack
2418        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof PlsqlBlockScope) {
2419            scopeStack.pop();
2420        }
2421
2422        // Restore previous PL/SQL block scope
2423        if (!plsqlBlockScopeStack.isEmpty()) {
2424            currentPlsqlBlockScope = plsqlBlockScopeStack.pop();
2425        } else {
2426            currentPlsqlBlockScope = null;
2427        }
2428
2429        if (DEBUG_SCOPE_BUILD) {
2430            System.out.println("[DEBUG] postVisit(TMySQLCreateProcedure): restored scope");
2431        }
2432    }
2433
2434    // ========== Nested BEGIN...END Block (TBlockSqlStatement) ==========
2435    // This handles nested BEGIN blocks within stored procedures.
2436    // The nested block inherits the parent's variable namespace.
2437
2438    @Override
2439    public void preVisit(TBlockSqlStatement stmt) {
2440        // For nested blocks, we don't create a new PlsqlBlockScope.
2441        // The nested block inherits the enclosing PlsqlBlockScope's variable namespace.
2442        // This ensures variables declared in the parent block are visible in nested blocks.
2443        if (DEBUG_SCOPE_BUILD) {
2444            System.out.println("[DEBUG] preVisit(TBlockSqlStatement): nested block, " +
2445                "currentPlsqlBlockScope=" + (currentPlsqlBlockScope != null ? "exists" : "null"));
2446        }
2447        // No new scope is created - we just inherit the parent's scope
2448    }
2449
2450    @Override
2451    public void postVisit(TBlockSqlStatement stmt) {
2452        // Nothing to pop since we didn't push a new scope
2453        if (DEBUG_SCOPE_BUILD) {
2454            System.out.println("[DEBUG] postVisit(TBlockSqlStatement): exiting nested block");
2455        }
2456    }
2457
2458    // ========== FOR Loop Statement (DB2, PL/SQL) ==========
2459    // FOR loop statements have a cursor subquery that needs explicit traversal.
2460    // The TForStmt.acceptChildren() calls subquery.accept() which only calls preVisit/postVisit
2461    // but doesn't traverse the SELECT statement's children. We need to explicitly traverse it.
2462
2463    @Override
2464    public void preVisit(TForStmt stmt) {
2465        // Explicitly traverse the FOR loop's cursor subquery
2466        // This is needed because TForStmt.acceptChildren() calls subquery.accept()
2467        // which doesn't traverse the SELECT's children (tables, columns, etc.)
2468        if (stmt.getSubquery() != null) {
2469            stmt.getSubquery().acceptChildren(this);
2470        }
2471
2472        if (DEBUG_SCOPE_BUILD) {
2473            System.out.println("[DEBUG] preVisit(TForStmt): traversed cursor subquery");
2474        }
2475    }
2476
2477    // ========== MERGE Statement ==========
2478
2479    @Override
2480    public void preVisit(TMergeSqlStatement stmt) {
2481        // Determine parent scope
2482        IScope parentScope = determineParentScopeForMerge(stmt);
2483
2484        // Create MergeScope
2485        MergeScope mergeScope = new MergeScope(parentScope, stmt);
2486        mergeScopeMap.put(stmt, mergeScope);
2487
2488        // Push to stack
2489        scopeStack.push(mergeScope);
2490        currentMergeScope = mergeScope;
2491
2492        // Set the current MERGE target table for UPDATE SET and INSERT column linking
2493        currentMergeTargetTable = stmt.getTargetTable();
2494
2495        if (DEBUG_SCOPE_BUILD) {
2496            String stmtPreview = stmt.toString().length() > 50
2497                ? stmt.toString().substring(0, 50) + "..."
2498                : stmt.toString();
2499            System.out.println("[DEBUG] preVisit(MERGE): " + stmtPreview.replace("\n", " ") +
2500                ", targetTable=" + (currentMergeTargetTable != null ? currentMergeTargetTable.getName() : "null"));
2501        }
2502
2503        // Create FromScope for MERGE's tables (target table + using table)
2504        if (stmt.tables != null && stmt.tables.size() > 0) {
2505            // Save current FROM scope if any
2506            if (currentFromScope != null) {
2507                fromScopeStack.push(currentFromScope);
2508            }
2509
2510            // Create FromScope for MERGE's tables
2511            FromScope fromScope = new FromScope(mergeScope, stmt.tables);
2512            mergeScope.setFromScope(fromScope);
2513            currentFromScope = fromScope;
2514
2515            // Tables will be processed when acceptChildren() visits TTable nodes
2516        }
2517
2518        // Process MERGE ON clause condition for Teradata only
2519        // In Teradata, unqualified columns on the left side of MERGE ON clause comparisons
2520        // should be linked to the target table (not the source subquery)
2521        if (dbVendor == EDbVendor.dbvteradata && stmt.getCondition() != null && currentMergeTargetTable != null) {
2522            processMergeOnCondition(stmt.getCondition());
2523        }
2524    }
2525
2526    /**
2527     * Process MERGE ON clause condition for Teradata.
2528     * For comparison expressions like "x1=10" or "x1=s.col", if the left operand
2529     * is an unqualified column (no table prefix), link it to the MERGE target table.
2530     *
2531     * This implements the Teradata-specific rule: in MERGE/USING/ON clause, unqualified columns
2532     * on the left side of comparisons should be linked to the target table.
2533     * This behavior is NOT applied to other databases as their column resolution rules differ.
2534     */
2535    private void processMergeOnCondition(TExpression condition) {
2536        if (condition == null) {
2537            return;
2538        }
2539
2540        if (DEBUG_SCOPE_BUILD) {
2541            System.out.println("[DEBUG] processMergeOnCondition: type=" + condition.getExpressionType() +
2542                    ", expr=" + condition.toString().replace("\n", " "));
2543        }
2544
2545        // Handle comparison expressions (e.g., x1=10, x1=s.col)
2546        if (condition.getExpressionType() == EExpressionType.simple_comparison_t) {
2547            TExpression leftOperand = condition.getLeftOperand();
2548            if (leftOperand != null &&
2549                leftOperand.getExpressionType() == EExpressionType.simple_object_name_t &&
2550                leftOperand.getObjectOperand() != null) {
2551
2552                TObjectName leftColumn = leftOperand.getObjectOperand();
2553                // Check if column is unqualified (no table prefix)
2554                if (leftColumn.getTableToken() == null) {
2555                    // Link to MERGE target table
2556                    leftColumn.setSourceTable(currentMergeTargetTable);
2557                    // Add to target table's linked columns directly
2558                    currentMergeTargetTable.getLinkedColumns().addObjectName(leftColumn);
2559                    allColumnReferences.add(leftColumn);
2560                    // Mark as ON clause target column - should NOT be re-resolved through name resolution
2561                    // This prevents the column from being incorrectly linked to the USING subquery
2562                    setClauseTargetColumns.add(leftColumn);
2563                    // Add to columnToScopeMap with the current MergeScope
2564                    if (currentMergeScope != null) {
2565                        columnToScopeMap.put(leftColumn, currentMergeScope);
2566                    }
2567                    if (DEBUG_SCOPE_BUILD) {
2568                        System.out.println("[DEBUG] Linked MERGE ON clause left-side column: " +
2569                                leftColumn.toString() + " -> " + currentMergeTargetTable.getFullName());
2570                    }
2571                }
2572            }
2573        }
2574
2575        // Recursively process compound conditions (AND, OR)
2576        if (condition.getExpressionType() == EExpressionType.logical_and_t ||
2577            condition.getExpressionType() == EExpressionType.logical_or_t ||
2578            condition.getExpressionType() == EExpressionType.parenthesis_t) {
2579            processMergeOnCondition(condition.getLeftOperand());
2580            processMergeOnCondition(condition.getRightOperand());
2581        }
2582    }
2583
2584    @Override
2585    public void postVisit(TMergeSqlStatement stmt) {
2586        // Pop scope
2587        if (!scopeStack.isEmpty() && scopeStack.peek() instanceof MergeScope) {
2588            scopeStack.pop();
2589        }
2590
2591        // Restore current MergeScope
2592        currentMergeScope = findEnclosingMergeScope();
2593
2594        // Clear MERGE target table
2595        currentMergeTargetTable = null;
2596
2597        // Restore FROM scope
2598        if (!fromScopeStack.isEmpty()) {
2599            currentFromScope = fromScopeStack.pop();
2600        } else {
2601            currentFromScope = null;
2602        }
2603    }
2604
2605    /**
2606     * Determine the parent scope for a MERGE statement.
2607     */
2608    private IScope determineParentScopeForMerge(TMergeSqlStatement stmt) {
2609        // If we have a CTE scope on the stack, use it
2610        if (currentCTEScope != null) {
2611            return currentCTEScope;
2612        }
2613
2614        // Otherwise, find appropriate parent from stack
2615        for (int i = scopeStack.size() - 1; i >= 0; i--) {
2616            IScope scope = scopeStack.get(i);
2617            if (scope instanceof SelectScope || scope instanceof UpdateScope ||
2618                scope instanceof MergeScope || scope instanceof CTEScope ||
2619                scope instanceof PlsqlBlockScope ||
2620                scope instanceof GlobalScope) {
2621                return scope;
2622            }
2623        }
2624
2625        return scopeStack.isEmpty() ? globalScope : scopeStack.peek();
2626    }
2627
2628    /**
2629     * Find the enclosing MergeScope in the stack
2630     */
2631    private MergeScope findEnclosingMergeScope() {
2632        for (int i = scopeStack.size() - 1; i >= 0; i--) {
2633            IScope scope = scopeStack.get(i);
2634            if (scope instanceof MergeScope) {
2635                return (MergeScope) scope;
2636            }
2637        }
2638        return null;
2639    }
2640
2641    // ========== MERGE UPDATE Clause ==========
2642
2643    @Override
2644    public void preVisit(TMergeUpdateClause updateClause) {
2645        // Handle MERGE UPDATE SET clause columns
2646        // The left-hand side of SET assignments (e.g., SET col = expr)
2647        // should be linked to the MERGE target table
2648        if (currentMergeTargetTable != null && updateClause.getUpdateColumnList() != null) {
2649            TResultColumnList updateColumns = updateClause.getUpdateColumnList();
2650            for (int i = 0; i < updateColumns.size(); i++) {
2651                TResultColumn rc = updateColumns.getResultColumn(i);
2652                if (rc != null && rc.getExpr() != null) {
2653                    TExpression expr = rc.getExpr();
2654                    // SET clause uses assignment_t for "column = value" assignments
2655                    // or simple_comparison_t in some databases
2656                    if ((expr.getExpressionType() == EExpressionType.assignment_t ||
2657                         expr.getExpressionType() == EExpressionType.simple_comparison_t) &&
2658                        expr.getLeftOperand() != null &&
2659                        expr.getLeftOperand().getExpressionType() == EExpressionType.simple_object_name_t &&
2660                        expr.getLeftOperand().getObjectOperand() != null) {
2661                        TObjectName leftColumn = expr.getLeftOperand().getObjectOperand();
2662                        // Link the SET clause column to the MERGE target table
2663                        leftColumn.setSourceTable(currentMergeTargetTable);
2664                        allColumnReferences.add(leftColumn);
2665                        // Mark as SET clause target - should NOT be re-resolved through star column
2666                        setClauseTargetColumns.add(leftColumn);
2667                        // Add to columnToScopeMap with the current MergeScope
2668                        if (currentMergeScope != null) {
2669                            columnToScopeMap.put(leftColumn, currentMergeScope);
2670                        }
2671                        if (DEBUG_SCOPE_BUILD) {
2672                            System.out.println("[DEBUG] Linked MERGE UPDATE SET column: " +
2673                                    leftColumn.toString() + " -> " + currentMergeTargetTable.getFullName());
2674                        }
2675                    }
2676                }
2677            }
2678        }
2679    }
2680
2681    // ========== MERGE INSERT Clause ==========
2682
2683    @Override
2684    public void preVisit(TMergeInsertClause insertClause) {
2685        // Handle MERGE INSERT clause columns
2686        // The column list in INSERT (column_list) should be linked to the MERGE target table
2687        if (currentMergeTargetTable != null && insertClause.getColumnList() != null) {
2688            TObjectNameList columnList = insertClause.getColumnList();
2689            for (int i = 0; i < columnList.size(); i++) {
2690                TObjectName column = columnList.getObjectName(i);
2691                if (column != null) {
2692                    // Link the INSERT column to the MERGE target table
2693                    column.setSourceTable(currentMergeTargetTable);
2694                    allColumnReferences.add(column);
2695                    // Mark as target column - should NOT be re-resolved through name resolution
2696                    // (same as UPDATE SET clause left-side columns)
2697                    setClauseTargetColumns.add(column);
2698                    // Add to columnToScopeMap with the current MergeScope
2699                    if (currentMergeScope != null) {
2700                        columnToScopeMap.put(column, currentMergeScope);
2701                    }
2702                    if (DEBUG_SCOPE_BUILD) {
2703                        System.out.println("[DEBUG] Linked MERGE INSERT column: " +
2704                                column.toString() + " -> " + currentMergeTargetTable.getFullName());
2705                    }
2706                }
2707            }
2708        }
2709
2710        // Handle MERGE INSERT VALUES clause columns
2711        // The VALUES list columns (e.g., VALUES(product, quantity)) should be linked to the USING table (source table)
2712        if (currentMergeScope != null && insertClause.getValuelist() != null) {
2713            TTable usingTable = currentMergeScope.getMergeStatement().getUsingTable();
2714            if (usingTable != null) {
2715                TResultColumnList valueList = insertClause.getValuelist();
2716                for (int i = 0; i < valueList.size(); i++) {
2717                    TResultColumn rc = valueList.getResultColumn(i);
2718                    if (rc != null && rc.getExpr() != null) {
2719                        TExpression expr = rc.getExpr();
2720                        // Handle simple column references in VALUES clause
2721                        if (expr.getExpressionType() == EExpressionType.simple_object_name_t &&
2722                            expr.getObjectOperand() != null) {
2723                            TObjectName valueColumn = expr.getObjectOperand();
2724                            // Link the VALUES column to the USING table (source)
2725                            valueColumn.setSourceTable(usingTable);
2726                            allColumnReferences.add(valueColumn);
2727                            columnToScopeMap.put(valueColumn, currentMergeScope);
2728                            if (DEBUG_SCOPE_BUILD) {
2729                                System.out.println("[DEBUG] Linked MERGE VALUES column: " +
2730                                        valueColumn.toString() + " -> " + usingTable.getFullName());
2731                            }
2732                        }
2733                    }
2734                }
2735            }
2736        }
2737    }
2738
2739    // ========== CTE (WITH Clause) ==========
2740
2741    @Override
2742    public void preVisit(TCTEList cteList) {
2743        // Create CTEScope
2744        IScope parentScope = scopeStack.peek();
2745        CTEScope cteScope = new CTEScope(parentScope, cteList);
2746
2747        // Push to stack
2748        scopeStack.push(cteScope);
2749        currentCTEScope = cteScope;
2750    }
2751
2752    @Override
2753    public void postVisit(TCTEList cteList) {
2754        // DON'T pop CTEScope here - leave it on stack so the main SELECT
2755        // can reference CTEs in its FROM clause. CTEScope will be popped
2756        // in postVisit(TSelectSqlStatement) after the entire SELECT is processed.
2757        //
2758        // Only clear currentCTEScope so new CTEs aren't added to it
2759        // (but it remains accessible via the stack for CTE lookups)
2760    }
2761
2762    @Override
2763    public void preVisit(TCTE cte) {
2764        // Track CTE definition depth for CTAS handling
2765        cteDefinitionDepth++;
2766
2767        if (currentCTEScope == null) {
2768            return;
2769        }
2770
2771        // Get CTE name
2772        String cteName = cte.getTableName() != null ? cte.getTableName().toString() : null;
2773        if (cteName == null) {
2774            return;
2775        }
2776
2777        // Mark CTE table name as a table reference (not column)
2778        if (cte.getTableName() != null) {
2779            tableNameReferences.add(cte.getTableName());
2780        }
2781
2782        // Create CTENamespace and add to CTEScope BEFORE processing subquery
2783        // This allows later CTEs to reference earlier ones
2784        TSelectSqlStatement subquery = cte.getSubquery();
2785        CTENamespace cteNamespace = new CTENamespace(cte, cteName, subquery, nameMatcher);
2786
2787        // Add to CTE scope immediately (enables forward references)
2788        currentCTEScope.addCTE(cteName, cteNamespace);
2789
2790        // Note: The subquery will be processed when acceptChildren traverses into it
2791        // At that point, preVisit(TSelectSqlStatement) will be called with currentCTEScope set
2792    }
2793
2794    @Override
2795    public void postVisit(TCTE cte) {
2796        // Track CTE definition depth for CTAS handling
2797        if (cteDefinitionDepth > 0) {
2798            cteDefinitionDepth--;
2799        }
2800
2801        if (currentCTEScope == null) {
2802            return;
2803        }
2804
2805        // Validate the CTENamespace after subquery processing is complete
2806        String cteName = cte.getTableName() != null ? cte.getTableName().toString() : null;
2807        if (cteName != null) {
2808            CTENamespace cteNamespace = currentCTEScope.getCTE(cteName);
2809            if (cteNamespace != null) {
2810                cteNamespace.validate();
2811            }
2812        }
2813    }
2814
2815    /**
2816     * Find the enclosing CTEScope in the stack
2817     */
2818    private CTEScope findEnclosingCTEScope() {
2819        for (int i = scopeStack.size() - 1; i >= 0; i--) {
2820            IScope scope = scopeStack.get(i);
2821            if (scope instanceof CTEScope) {
2822                return (CTEScope) scope;
2823            }
2824        }
2825        return null;
2826    }
2827
2828    // ========== FROM Clause ==========
2829
2830    @Override
2831    public void preVisit(TFromClause fromClause) {
2832        if (DEBUG_SCOPE_BUILD) {
2833            System.out.println("[DEBUG] preVisit(TFromClause): currentSelectScope=" +
2834                (currentSelectScope != null ? "exists" : "NULL"));
2835        }
2836
2837        if (currentSelectScope == null) {
2838            return;
2839        }
2840
2841        // Save current FROM scope for nested subqueries (e.g., in JOINs)
2842        // This is critical: when processing a JOIN, the left subquery's FROM clause
2843        // will be visited before the right subquery. We need to restore the outer
2844        // FROM scope after processing each inner subquery.
2845        if (currentFromScope != null) {
2846            fromScopeStack.push(currentFromScope);
2847        }
2848
2849        // Reset join chain tables for this FROM clause
2850        // This tracks ALL tables in chained JOINs for proper USING column resolution
2851        currentJoinChainTables.clear();
2852
2853        // Create FromScope
2854        FromScope fromScope = new FromScope(currentSelectScope, fromClause);
2855        currentSelectScope.setFromScope(fromScope);
2856
2857        // Track current FromScope
2858        currentFromScope = fromScope;
2859
2860        if (DEBUG_SCOPE_BUILD) {
2861            System.out.println("[DEBUG]   Created FromScope, linked to SelectScope");
2862        }
2863    }
2864
2865    @Override
2866    public void postVisit(TFromClause fromClause) {
2867        // Restore previous FROM scope (for nested subqueries in JOINs)
2868        // IMPORTANT: Do NOT clear currentFromScope if stack is empty!
2869        // We need to keep the FROM scope available for the SELECT list expressions
2870        // (e.g., function calls, column references) which are visited after FROM clause.
2871        // The FROM scope will be cleared when the SELECT statement ends.
2872        if (!fromScopeStack.isEmpty()) {
2873            currentFromScope = fromScopeStack.pop();
2874        }
2875        // Note: If stack is empty, we keep currentFromScope as is - it will be cleared
2876        // in postVisit(TSelectSqlStatement) or when the enclosing statement ends.
2877    }
2878
2879    // ========== Table ==========
2880
2881    @Override
2882    public void preVisit(TTable table) {
2883        // Mark table name as a table reference (not column)
2884        if (table.getTableName() != null) {
2885            tableNameReferences.add(table.getTableName());
2886        }
2887
2888        if (DEBUG_SCOPE_BUILD) {
2889            System.out.println("[DEBUG] preVisit(TTable): " + table.getDisplayName() +
2890                " type=" + table.getTableType() +
2891                " currentFromScope=" + (currentFromScope != null ? "exists" : "NULL"));
2892        }
2893
2894        // Only process if we have a FROM scope
2895        if (currentFromScope == null) {
2896            return;
2897        }
2898
2899        // Handle based on table type
2900        ETableSource tableType = table.getTableType();
2901
2902        switch (tableType) {
2903            case objectname:
2904                processPhysicalTable(table);
2905                break;
2906
2907            case subquery:
2908                processSubqueryTable(table);
2909                break;
2910
2911            case join:
2912                // JOIN is handled via TJoinExpr
2913                // Left and right tables will be visited separately
2914                break;
2915
2916            case function:
2917                // Table-valued function - treat similar to table
2918                processTableFunction(table);
2919                break;
2920
2921            case pivoted_table:
2922                // PIVOT table - creates new columns from IN clause
2923                processPivotTable(table);
2924                break;
2925
2926            case unnest:
2927                // UNNEST table - creates virtual table from array
2928                processUnnestTable(table);
2929                break;
2930
2931            case rowList:
2932                // VALUES table - inline data with optional column aliases
2933                // e.g., VALUES (1, 'a'), (2, 'b') AS t(id, name)
2934                processValuesTable(table);
2935                break;
2936
2937            case stageReference:
2938                // Snowflake stage file reference (e.g., @stage/path)
2939                // Treat similar to a regular table but without metadata
2940                processStageTable(table);
2941                break;
2942
2943            case td_unpivot:
2944                // Teradata TD_UNPIVOT table function - collect columns from parameters
2945                processTDUnpivotTable(table);
2946                break;
2947
2948            default:
2949                // Other types (CTE reference, etc.)
2950                processCTEReference(table);
2951                break;
2952        }
2953    }
2954
2955    @Override
2956    public void postVisit(TTable table) {
2957        // Decrement the PIVOT source processing depth when exiting a pivot table.
2958        // This must happen BEFORE SubqueryNamespace validation so the flag is correctly
2959        // restored for any subsequent tables.
2960        if (table.getTableType() == ETableSource.pivoted_table) {
2961            if (pivotSourceProcessingDepth > 0) {
2962                pivotSourceProcessingDepth--;
2963            }
2964        }
2965
2966        // Validate SubqueryNamespace after subquery content is fully processed
2967        // This is critical because SubqueryNamespace.doValidate() needs to read
2968        // the subquery's SELECT list, which is only complete after traversal
2969        SubqueryNamespace subNs = pendingSubqueryValidation.remove(table);
2970        if (subNs != null) {
2971            subNs.validate();
2972        }
2973    }
2974
2975    /**
2976     * Process a physical table reference
2977     */
2978    private void processPhysicalTable(TTable table) {
2979        // Check if this table references a CTE
2980        String tableName = table.getName();
2981        CTENamespace cteNamespace = findCTEByName(tableName);
2982
2983        if (cteNamespace != null) {
2984            // This is a CTE reference, use the existing CTENamespace
2985            String alias = getTableAlias(table);
2986            cteNamespace.setReferencingTable(table);  // Set the referencing TTable for getFinalTable() fallback
2987            currentFromScope.addChild(cteNamespace, alias, false);
2988            lastProcessedFromTable = table;  // Track for JOIN...USING left table detection
2989            currentJoinChainTables.add(table);  // Track ALL tables in join chain for chained USING
2990            if (DEBUG_SCOPE_BUILD) {
2991                System.out.println("[DEBUG] Added CTE to FromScope: alias=" + alias);
2992            }
2993        } else {
2994            // Check if this is a SQL Server virtual table (deleted/inserted)
2995            // These can appear in:
2996            // 1. CREATE TRIGGER bodies - should reference the trigger's target table
2997            // 2. OUTPUT clauses of INSERT/UPDATE/DELETE - handled by preVisit(TOutputClause)
2998            //
2999            // IMPORTANT: Only substitute deleted/inserted when inside a TRIGGER context.
3000            // In DML (INSERT/UPDATE/DELETE) context, deleted/inserted in FROM clause
3001            // are regular table references (could be trigger pseudo-tables from an outer trigger).
3002            // The OUTPUT clause pseudo-columns are handled separately in preVisit(TOutputClause).
3003            TTable effectiveTable = table;
3004            if (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) {
3005                String upperName = tableName != null ? tableName.toUpperCase() : "";
3006                if ("DELETED".equals(upperName) || "INSERTED".equals(upperName)) {
3007                    // Only substitute when inside a TRIGGER (not just any DML statement)
3008                    if (currentTriggerTargetTable != null) {
3009                        effectiveTable = currentTriggerTargetTable;
3010                        // Track this table as a virtual trigger table (to be skipped in table output)
3011                        virtualTriggerTables.add(table);
3012                        if (DEBUG_SCOPE_BUILD) {
3013                            System.out.println("[DEBUG] Substituting virtual table '" + tableName +
3014                                "' with trigger target table '" + currentTriggerTargetTable.getName() + "'");
3015                        }
3016                    }
3017                }
3018            }
3019
3020            // Regular physical table - pass TSQLEnv for metadata lookup
3021            TableNamespace tableNs = new TableNamespace(effectiveTable, nameMatcher, sqlEnv);
3022            tableNs.validate();
3023            String alias = getTableAlias(table);
3024
3025            // Add to FROM scope ONLY if not inside a PIVOT/UNPIVOT source processing context.
3026            // When a table is the source of a PIVOT/UNPIVOT, only the PIVOT/UNPIVOT table
3027            // should be visible in the outer query, not the source table itself.
3028            if (pivotSourceProcessingDepth == 0) {
3029                currentFromScope.addChild(tableNs, alias, false);
3030                if (DEBUG_SCOPE_BUILD) {
3031                    System.out.println("[DEBUG] Added table to FromScope: alias=" + alias + " tableName=" + tableName +
3032                        " hasMetadata=" + (tableNs.getResolvedTable() != null));
3033                }
3034            } else {
3035                if (DEBUG_SCOPE_BUILD) {
3036                    System.out.println("[DEBUG] Skipping table from FromScope (inside PIVOT source): alias=" + alias);
3037                }
3038            }
3039
3040            lastProcessedFromTable = table;  // Track for JOIN...USING left table detection
3041            currentJoinChainTables.add(table);  // Track ALL tables in join chain for chained USING
3042            tableToNamespaceMap.put(table, tableNs);  // Store for legacy compatibility
3043        }
3044    }
3045
3046    /**
3047     * Process a subquery in FROM clause
3048     */
3049    private void processSubqueryTable(TTable table) {
3050        TSelectSqlStatement subquery = table.getSubquery();
3051        if (subquery == null) {
3052            return;
3053        }
3054
3055        // Track column name definitions from the alias clause
3056        // These are column DEFINITIONS, not references - should NOT be collected as column refs
3057        // e.g., in "FROM (SELECT ...) AS t(id, name)", 'id' and 'name' are definitions
3058        TAliasClause aliasClause = table.getAliasClause();
3059        if (aliasClause != null) {
3060            TObjectNameList columns = aliasClause.getColumns();
3061            if (columns != null && columns.size() > 0) {
3062                for (int i = 0; i < columns.size(); i++) {
3063                    TObjectName colName = columns.getObjectName(i);
3064                    if (colName != null) {
3065                        valuesTableAliasColumns.add(colName);
3066                        if (DEBUG_SCOPE_BUILD) {
3067                            System.out.println("[DEBUG] Tracked subquery alias column definition (will skip): " + colName.toString());
3068                        }
3069                    }
3070                }
3071            }
3072        }
3073
3074        String alias = table.getAliasName();
3075        INamespace namespace;
3076
3077        // Check if this is a TABLE function (Oracle TABLE(SELECT ...) syntax)
3078        // When isTableKeyword() is true, the subquery is wrapped in a TABLE() function
3079        boolean isTableFunction = table.isTableKeyword();
3080
3081        // Check if this is a UNION/INTERSECT/EXCEPT query
3082        if (subquery.isCombinedQuery()) {
3083            // Create UnionNamespace for set operations
3084            UnionNamespace unionNs = new UnionNamespace(subquery, alias, nameMatcher);
3085            namespace = unionNs;
3086
3087            if (DEBUG_SCOPE_BUILD) {
3088                System.out.println("[DEBUG]   Detected UNION query with " +
3089                    unionNs.getBranchCount() + " branches");
3090            }
3091        } else {
3092            // Regular subquery - create SubqueryNamespace
3093            // Pass isTableFunction to mark TABLE function subqueries for expression alias filtering
3094            SubqueryNamespace subNs = new SubqueryNamespace(subquery, alias, nameMatcher, isTableFunction);
3095            // Pass guessColumnStrategy for config-based isolation (prevents test side effects)
3096            if (guessColumnStrategy >= 0) {
3097                subNs.setGuessColumnStrategy(guessColumnStrategy);
3098            }
3099            // Pass sqlEnv for metadata lookup during star column resolution
3100            if (sqlEnv != null) {
3101                subNs.setSqlEnv(sqlEnv);
3102            }
3103            // Set the owning TTable for legacy sync support
3104            subNs.setSourceTable(table);
3105            namespace = subNs;
3106
3107            // Register for deferred validation
3108            // SubqueryNamespace needs to be validated AFTER its subquery's SELECT list is processed
3109            // because the column names come from the SELECT list
3110            pendingSubqueryValidation.put(table, subNs);
3111        }
3112
3113        // Add to FROM scope ONLY if not inside a PIVOT/UNPIVOT source processing context.
3114        // When a subquery is the source of a PIVOT/UNPIVOT, only the PIVOT/UNPIVOT table
3115        // should be visible in the outer query, not the source subquery itself.
3116        // Example: FROM (SELECT ...) p UNPIVOT (...) AS unpvt
3117        //   - Only 'unpvt' should be visible, not 'p'
3118        //   - Column references in outer SELECT should resolve to 'unpvt', not ambiguously to both
3119        if (pivotSourceProcessingDepth == 0) {
3120            currentFromScope.addChild(namespace, alias != null ? alias : "<subquery>", false);
3121            if (DEBUG_SCOPE_BUILD) {
3122                System.out.println("[DEBUG] Added subquery to FromScope: alias=" + alias);
3123            }
3124        } else {
3125            if (DEBUG_SCOPE_BUILD) {
3126                System.out.println("[DEBUG] Skipping subquery from FromScope (inside PIVOT source): alias=" + alias);
3127            }
3128        }
3129        tableToNamespaceMap.put(table, namespace);  // Store for legacy compatibility
3130
3131        // Note: The subquery SELECT will be processed when acceptChildren traverses into it
3132        // For UnionNamespace, validation happens during construction
3133    }
3134
3135    /**
3136     * Process a table-valued function
3137     *
3138     * TABLE functions can contain subqueries as arguments, e.g.:
3139     * TABLE (SELECT AVG(E.SALARY) AS AVGSAL, COUNT(*) AS EMPCOUNT FROM EMP E) AS EMPINFO
3140     *
3141     * In this case, we create a SubqueryNamespace to expose the subquery's SELECT list
3142     * columns (AVGSAL, EMPCOUNT) to the outer query.
3143     */
3144    private void processTableFunction(TTable table) {
3145        // Track this function call as a table-valued function (not a column method call)
3146        if (table.getFuncCall() != null) {
3147            tableValuedFunctionCalls.add(table.getFuncCall());
3148        }
3149
3150        String alias = getTableAlias(table);
3151
3152        // Check if the TABLE function has a subquery argument
3153        TFunctionCall funcCall = table.getFuncCall();
3154        if (funcCall != null && funcCall.getArgs() != null && funcCall.getArgs().size() > 0) {
3155            TExpression firstArg = funcCall.getArgs().getExpression(0);
3156            if (firstArg != null && firstArg.getSubQuery() != null) {
3157                // TABLE function contains a subquery - create SubqueryNamespace
3158                TSelectSqlStatement subquery = firstArg.getSubQuery();
3159
3160                // Check if this is a UNION/INTERSECT/EXCEPT query
3161                INamespace namespace;
3162                if (subquery.isCombinedQuery()) {
3163                    // Create UnionNamespace for set operations
3164                    UnionNamespace unionNs = new UnionNamespace(subquery, alias, nameMatcher);
3165                    namespace = unionNs;
3166                } else {
3167                    // Create SubqueryNamespace for regular subquery, marked as from TABLE function
3168                    SubqueryNamespace subNs = new SubqueryNamespace(subquery, alias, nameMatcher, true);
3169                    // Pass guessColumnStrategy for config-based isolation (prevents test side effects)
3170                    if (guessColumnStrategy >= 0) {
3171                        subNs.setGuessColumnStrategy(guessColumnStrategy);
3172                    }
3173                    // Pass sqlEnv for metadata lookup during star column resolution
3174                    if (sqlEnv != null) {
3175                        subNs.setSqlEnv(sqlEnv);
3176                    }
3177                    // Set the owning TTable for legacy sync support
3178                    subNs.setSourceTable(table);
3179                    namespace = subNs;
3180                    // Defer validation until after children are visited
3181                    pendingSubqueryValidation.put(table, subNs);
3182                }
3183
3184                currentFromScope.addChild(namespace, alias, false);
3185                tableToNamespaceMap.put(table, namespace);  // Store for legacy compatibility
3186
3187                if (DEBUG_SCOPE_BUILD) {
3188                    System.out.println("[DEBUG] Added TABLE function with subquery to FromScope: alias=" + alias);
3189                }
3190                return;
3191            }
3192        }
3193
3194        // Fallback: treat as a regular table-valued function (e.g., UDF)
3195        TableNamespace tableNs = new TableNamespace(table, nameMatcher, sqlEnv);
3196        tableNs.validate();
3197        currentFromScope.addChild(tableNs, alias, false);
3198        tableToNamespaceMap.put(table, tableNs);  // Store for legacy compatibility
3199    }
3200
3201    /**
3202     * Process a PIVOT table
3203     *
3204     * PIVOT tables are created from a source table and a PIVOT clause:
3205     * <pre>
3206     * FROM source_table
3207     * PIVOT (aggregate_function(value) FOR column IN (val1, val2, ...)) AS alias
3208     * </pre>
3209     *
3210     * The PIVOT produces new columns (val1, val2, ...) while maintaining
3211     * some pass-through columns from the source table.
3212     */
3213    private void processPivotTable(TTable table) {
3214        // Get the source table of the pivot (the table being pivoted)
3215        // Note: For SQL Server, getSourceTableOfPivot() may return null, so we also check
3216        // getPivotedTable().getTableSource() which is set during parsing.
3217        // However, for BigQuery, using this fallback can break resolution because BigQuery's
3218        // AST structure adds the source table to FROM scope separately during traversal.
3219        // For SQL Server, we need the fallback to properly resolve pass-through columns.
3220        TTable sourceTable = table.getSourceTableOfPivot();
3221
3222        // Get PIVOT clause from the table's pivot table reference
3223        TPivotClause pivotClause = null;
3224        TPivotedTable pivotedTable = table.getPivotedTable();
3225
3226        // SQL Server UNPIVOT issue: When UNPIVOT is used on a subquery, the table from
3227        // statement.tables may have tableType=pivoted_table but getPivotedTable()=null.
3228        // The actual pivot clause is on a different TTable object from statement.joins.
3229        // We need to search for it there.
3230        if (pivotedTable == null && (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql)) {
3231            // Try to find the actual pivot table from joins
3232            TTable pivotTableFromJoins = findPivotTableInJoins(table);
3233            if (pivotTableFromJoins != null && pivotTableFromJoins.getPivotedTable() != null) {
3234                pivotedTable = pivotTableFromJoins.getPivotedTable();
3235            }
3236        }
3237
3238        if (pivotedTable != null) {
3239            pivotClause = pivotedTable.getPivotClause();
3240            // Also try to get source table from the pivoted table if not found yet
3241            if (sourceTable == null) {
3242                TTable potentialSourceTable = pivotedTable.getTableSource();
3243                // Use the fallback to get the source table for vendors that need it.
3244                // This is required for pass-through column resolution in PIVOT tables.
3245                // Note: For BigQuery, the source table (especially subqueries) needs this
3246                // fallback to properly resolve pass-through columns.
3247                if (potentialSourceTable != null) {
3248                    sourceTable = potentialSourceTable;
3249                }
3250            }
3251        }
3252
3253        // Get the alias - for PIVOT tables, the alias is in the PIVOT clause
3254        String alias = getTableAlias(table);
3255
3256        // If table alias is null/empty, try to get from pivot clause's alias clause
3257        if ((alias == null || alias.isEmpty() || alias.startsWith("null")) && pivotClause != null) {
3258            if (pivotClause.getAliasClause() != null &&
3259                pivotClause.getAliasClause().getAliasName() != null) {
3260                alias = pivotClause.getAliasClause().getAliasName().toString();
3261            }
3262        }
3263
3264        // Fallback to appropriate default alias if still no alias
3265        if (alias == null || alias.isEmpty() || alias.startsWith("null")) {
3266            // Use different default for PIVOT vs UNPIVOT to match TPivotClause.doParse()
3267            if (pivotClause != null && pivotClause.getType() == TPivotClause.unpivot) {
3268                alias = "unpivot_alias";
3269            } else {
3270                alias = "pivot_alias";
3271            }
3272        }
3273
3274        // IMPORTANT: The visitor traverses through getRelations() which may contain
3275        // different TTable objects than statement.tables. The formatter matches
3276        // sourceTable against statement.tables using object identity. We need to
3277        // find the matching pivot table in statement.tables to ensure proper matching.
3278        TTable pivotTableForNamespace = findMatchingPivotTableInStatement(table, alias);
3279
3280        // Create PivotNamespace with the table from statement.tables (if found)
3281        PivotNamespace pivotNs = new PivotNamespace(pivotTableForNamespace, pivotClause, sourceTable, alias, nameMatcher);
3282        pivotNs.validate();
3283
3284        // Wire source namespace for pass-through column resolution (Delta 2)
3285        if (sourceTable != null) {
3286            INamespace sourceNamespace = resolveSourceNamespaceForPivot(sourceTable);
3287            if (sourceNamespace != null) {
3288                pivotNs.setSourceNamespace(sourceNamespace);
3289            }
3290        }
3291
3292        // Add to FROM scope with the pivot table alias
3293        currentFromScope.addChild(pivotNs, alias, false);
3294        tableToNamespaceMap.put(table, pivotNs);  // Store for legacy compatibility
3295        if (pivotTableForNamespace != null && pivotTableForNamespace != table) {
3296            tableToNamespaceMap.put(pivotTableForNamespace, pivotNs);  // Also map the matched table
3297        }
3298
3299        // Add all PIVOT/UNPIVOT columns to allColumnReferences
3300        // Per CLAUDE.md, this resolution logic MUST be in ScopeBuilder, not in the formatter
3301        if (pivotClause != null) {
3302            if (pivotClause.getType() == TPivotClause.unpivot) {
3303                // UNPIVOT case: add generated columns and IN clause source columns
3304                addUnpivotColumns(pivotClause, pivotTableForNamespace, sourceTable);
3305            } else {
3306                // PIVOT case: Check if there's an alias column list (e.g., AS p (col1, col2, col3))
3307                // If so, use the alias columns as they REPLACE the IN clause column names
3308                boolean hasAliasColumnList = pivotClause.getAliasClause() != null &&
3309                    pivotClause.getAliasClause().getColumns() != null &&
3310                    pivotClause.getAliasClause().getColumns().size() > 0;
3311
3312                if (hasAliasColumnList) {
3313                    // Use alias column list - these replace the default pivot column names
3314                    addPivotAliasColumns(pivotClause.getAliasClause().getColumns(), pivotTableForNamespace);
3315                } else {
3316                    // No alias column list - use IN clause columns as pivot column names
3317                    TPivotInClause inClause = pivotClause.getPivotInClause();
3318                    if (inClause != null) {
3319                        addPivotInClauseColumns(inClause, pivotTableForNamespace);
3320                    }
3321                }
3322            }
3323        }
3324
3325        if (DEBUG_SCOPE_BUILD) {
3326            System.out.println("[DEBUG] Added PIVOT table to FromScope: alias=" + alias +
3327                " sourceTable=" + (sourceTable != null ? sourceTable.getName() : "null") +
3328                " pivotColumns=" + pivotNs.getPivotColumns().size() +
3329                " pivotTable=" + (pivotTableForNamespace == table ? "same" : "from-stmt-tables"));
3330        }
3331
3332        // Mark that we're now processing PIVOT/UNPIVOT source relations.
3333        // This prevents the source subquery from being added to FromScope
3334        // when the visitor traverses the TPivotedTable's children.
3335        // Will be decremented in postVisit(TTable) for pivot tables.
3336        pivotSourceProcessingDepth++;
3337    }
3338
3339    /**
3340     * Add all columns from PIVOT IN clause to allColumnReferences.
3341     * This ensures all pivot columns appear in the output, not just the ones referenced in SELECT.
3342     *
3343     * @param inClause The PIVOT IN clause
3344     * @param pivotTable The pivot table to set as sourceTable
3345     */
3346    private void addPivotInClauseColumns(TPivotInClause inClause, TTable pivotTable) {
3347        if (inClause == null || pivotTable == null) {
3348            return;
3349        }
3350
3351        // Case 1: IN clause has items (e.g., IN ([1], [2]) or IN ("SINGAPORE","LONDON","HOUSTON"))
3352        TResultColumnList items = inClause.getItems();
3353        if (items != null) {
3354            for (int i = 0; i < items.size(); i++) {
3355                TResultColumn resultColumn = items.getResultColumn(i);
3356                if (resultColumn == null || resultColumn.getExpr() == null) {
3357                    continue;
3358                }
3359                TExpression expr = resultColumn.getExpr();
3360
3361                if (expr.getExpressionType() == EExpressionType.simple_object_name_t) {
3362                    // Column reference (e.g., [Sammich], [Apple], "HOUSTON" in BigQuery)
3363                    // These are pivot column DEFINITIONS, not references to source table columns.
3364                    TObjectName objName = expr.getObjectOperand();
3365                    if (objName != null) {
3366                        objName.setSourceTable(pivotTable);
3367                        // Mark as pivot IN clause column so preVisit(TObjectName) won't re-process it
3368                        // and NameResolver won't overwrite the sourceTable with the source table
3369                        pivotInClauseColumns.add(objName);
3370                        allColumnReferences.add(objName);
3371                        // IMPORTANT: Do NOT add to columnToScopeMap - these are column DEFINITIONS,
3372                        // not references that need resolution. Adding to the map would cause the
3373                        // NameResolver to resolve them as source table columns, overwriting the
3374                        // sourceTable we set above.
3375                    }
3376                } else if (expr.getExpressionType() == EExpressionType.simple_constant_t) {
3377                    // Constant value (e.g., "HOUSTON", 'value')
3378                    // These are pivot column DEFINITIONS, not references that need resolution.
3379                    TConstant constant = expr.getConstantOperand();
3380                    if (constant != null && constant.getValueToken() != null) {
3381                        // Strip quotes from constant value for clean column name
3382                        String rawValue = constant.getValueToken().toString();
3383                        String cleanValue = stripStringDelimiters(rawValue);
3384                        TObjectName newColRef = TObjectName.createObjectName(
3385                            inClause.dbvendor,
3386                            EDbObjectType.column,
3387                            new TSourceToken(cleanValue)
3388                        );
3389                        newColRef.setSourceTable(pivotTable);
3390                        allColumnReferences.add(newColRef);
3391                        // IMPORTANT: Do NOT add to columnToScopeMap - these are column DEFINITIONS,
3392                        // not references that need resolution. Adding to the map would cause the
3393                        // NameResolver to resolve them and overwrite the sourceTable we set above.
3394                    }
3395                }
3396            }
3397        }
3398
3399        // Case 2: IN clause has a subquery (e.g., IN (SELECT DISTINCT col FROM table))
3400        if (inClause.getSubQuery() != null) {
3401            TResultColumnList subqueryColumns = inClause.getSubQuery().getResultColumnList();
3402            if (subqueryColumns != null) {
3403                for (int i = 0; i < subqueryColumns.size(); i++) {
3404                    TResultColumn resultColumn = subqueryColumns.getResultColumn(i);
3405                    if (resultColumn != null) {
3406                        TObjectName pivotColumn = TObjectName.createObjectName(
3407                            inClause.dbvendor,
3408                            EDbObjectType.column,
3409                            new TSourceToken(resultColumn.getDisplayName())
3410                        );
3411                        pivotColumn.setSourceTable(pivotTable);
3412                        allColumnReferences.add(pivotColumn);
3413                        // IMPORTANT: Do NOT add to columnToScopeMap.
3414                        // These are synthetic PIVOT output column DEFINITIONS (derived from IN-subquery result),
3415                        // not references that should be name-resolved. Adding them to the map would allow
3416                        // NameResolver to overwrite pivotColumn.sourceTable to some source table.
3417                    }
3418                }
3419            }
3420        }
3421    }
3422
3423    /**
3424     * Add PIVOT alias columns to allColumnReferences.
3425     * When PIVOT has an alias clause with column list (e.g., AS p (empid_renamed, Q1, Q2, Q3, Q4)),
3426     * the alias columns REPLACE the IN clause column names in the output.
3427     *
3428     * @param aliasColumns The column list from the alias clause
3429     * @param pivotTable The pivot table to set as sourceTable
3430     */
3431    private void addPivotAliasColumns(TObjectNameList aliasColumns, TTable pivotTable) {
3432        if (aliasColumns == null || pivotTable == null) {
3433            return;
3434        }
3435
3436        for (int i = 0; i < aliasColumns.size(); i++) {
3437            TObjectName aliasCol = aliasColumns.getObjectName(i);
3438            if (aliasCol != null) {
3439                aliasCol.setSourceTable(pivotTable);
3440                allColumnReferences.add(aliasCol);
3441                // IMPORTANT: Do NOT add to columnToScopeMap.
3442                // These are PIVOT output column DEFINITIONS from the alias list,
3443                // not references that should be name-resolved.
3444            }
3445        }
3446    }
3447
3448    /**
3449     * Add UNPIVOT source columns (IN clause) to allColumnReferences and mark definition columns.
3450     *
3451     * UNPIVOT (yearly_total FOR order_mode IN (store AS 'direct', internet AS 'online'))
3452     * - yearly_total is the value column (DEFINITION - creates a new column, NOT a reference)
3453     * - order_mode is the FOR column (DEFINITION - creates a new column, NOT a reference)
3454     * - store, internet are source columns (REFERENCES - belong to source table)
3455     *
3456     * IMPORTANT: Value and FOR columns are DEFINITIONS that create new columns in the UNPIVOT
3457     * output. They should NOT be added to allColumnReferences (which tracks references).
3458     * The PivotNamespace already tracks these generated columns for resolution purposes.
3459     *
3460     * @param pivotClause The UNPIVOT clause
3461     * @param unpivotTable The UNPIVOT table for generated columns
3462     * @param sourceTable The source table for IN clause columns
3463     */
3464    private void addUnpivotColumns(TPivotClause pivotClause, TTable unpivotTable, TTable sourceTable) {
3465        if (pivotClause == null || unpivotTable == null) {
3466            return;
3467        }
3468
3469        // Mark value columns as UNPIVOT definitions (NOT column references)
3470        // These define new output columns like "yearly_total" in UNPIVOT (yearly_total FOR ...)
3471        // We still set sourceTable for resolution purposes, but mark them so they're not
3472        // collected as column references by isColumnReference().
3473        // Note: For single value column UNPIVOT (like Oracle), use getValueColumn() (deprecated singular)
3474        TObjectNameList valueColumns = pivotClause.getValueColumnList();
3475        if (valueColumns != null && valueColumns.size() > 0) {
3476            for (int i = 0; i < valueColumns.size(); i++) {
3477                TObjectName valueCol = valueColumns.getObjectName(i);
3478                if (valueCol != null) {
3479                    valueCol.setSourceTable(unpivotTable);
3480                    unpivotDefinitionColumns.add(valueCol);
3481                    // Add to output completeness list as a DEFINITION (no name resolution)
3482                    allColumnReferences.add(valueCol);
3483                }
3484            }
3485        } else {
3486            // Fallback to deprecated singular method for Oracle compatibility
3487            @SuppressWarnings("deprecation")
3488            TObjectName valueCol = pivotClause.getValueColumn();
3489            if (valueCol != null) {
3490                valueCol.setSourceTable(unpivotTable);
3491                unpivotDefinitionColumns.add(valueCol);
3492                // Add to output completeness list as a DEFINITION (no name resolution)
3493                allColumnReferences.add(valueCol);
3494            }
3495        }
3496
3497        // Mark FOR columns as UNPIVOT definitions (NOT column references)
3498        // These define new output columns like "order_mode" in UNPIVOT (... FOR order_mode IN ...)
3499        // We still set sourceTable for resolution purposes, but mark them so they're not
3500        // collected as column references by isColumnReference().
3501        // Note: For single FOR column UNPIVOT (like Oracle), use getPivotColumn() (deprecated singular)
3502        TObjectNameList pivotColumnList = pivotClause.getPivotColumnList();
3503        if (pivotColumnList != null && pivotColumnList.size() > 0) {
3504            for (int i = 0; i < pivotColumnList.size(); i++) {
3505                TObjectName forCol = pivotColumnList.getObjectName(i);
3506                if (forCol != null) {
3507                    forCol.setSourceTable(unpivotTable);
3508                    unpivotDefinitionColumns.add(forCol);
3509                    // Add to output completeness list as a DEFINITION (no name resolution)
3510                    allColumnReferences.add(forCol);
3511                }
3512            }
3513        } else {
3514            // Fallback to deprecated singular method for Oracle compatibility
3515            @SuppressWarnings("deprecation")
3516            TObjectName forCol = pivotClause.getPivotColumn();
3517            if (forCol != null) {
3518                forCol.setSourceTable(unpivotTable);
3519                unpivotDefinitionColumns.add(forCol);
3520                // Add to output completeness list as a DEFINITION (no name resolution)
3521                allColumnReferences.add(forCol);
3522            }
3523        }
3524
3525        // IN clause columns (e.g., store, internet in "IN (store AS 'direct', internet AS 'online')")
3526        // are references to columns in the source table. They should be added to allColumnReferences
3527        // so they appear in the output with source table attribution (e.g., pivot_table.store).
3528        //
3529        // We set their sourceTable to the source table and add them to allColumnReferences.
3530        // We also mark them as unpivotDefinitionColumns so they're not collected again by isColumnReference().
3531        Set<String> consumedColumns = new HashSet<>();  // Track consumed column names
3532        TUnpivotInClause unpivotInClause = pivotClause.getUnpivotInClause();
3533        if (unpivotInClause != null && unpivotInClause.getItems() != null && sourceTable != null) {
3534            for (int i = 0; i < unpivotInClause.getItems().size(); i++) {
3535                TUnpivotInClauseItem item = unpivotInClause.getItems().getElement(i);
3536                if (item != null) {
3537                    // Single column case
3538                    if (item.getColumn() != null) {
3539                        TObjectName col = item.getColumn();
3540                        col.setSourceTable(sourceTable);
3541                        // Mark as definition so it's not collected again in isColumnReference()
3542                        unpivotDefinitionColumns.add(col);
3543                        // Add to allColumnReferences - this IS a reference to a source table column
3544                        allColumnReferences.add(col);
3545                        // Track consumed column name (strip table prefix if present)
3546                        String colName = col.getColumnNameOnly();
3547                        if (colName != null) consumedColumns.add(colName.toLowerCase());
3548                    }
3549                    // Multi-column case
3550                    if (item.getColumnList() != null) {
3551                        for (int j = 0; j < item.getColumnList().size(); j++) {
3552                            TObjectName col = item.getColumnList().getObjectName(j);
3553                            if (col != null) {
3554                                col.setSourceTable(sourceTable);
3555                                // Mark as definition so it's not collected again in isColumnReference()
3556                                unpivotDefinitionColumns.add(col);
3557                                // Add to allColumnReferences - this IS a reference to a source table column
3558                                allColumnReferences.add(col);
3559                                // Track consumed column name
3560                                String colName = col.getColumnNameOnly();
3561                                if (colName != null) consumedColumns.add(colName.toLowerCase());
3562                            }
3563                        }
3564                    }
3565                }
3566            }
3567        }
3568
3569        // Collect value and FOR column names for exclusion
3570        // (reusing valueColumns and pivotColumnList already defined above)
3571        if (valueColumns != null) {
3572            for (int i = 0; i < valueColumns.size(); i++) {
3573                TObjectName vc = valueColumns.getObjectName(i);
3574                if (vc != null && vc.getColumnNameOnly() != null) {
3575                    consumedColumns.add(vc.getColumnNameOnly().toLowerCase());
3576                }
3577            }
3578        }
3579        if (pivotColumnList != null) {
3580            for (int i = 0; i < pivotColumnList.size(); i++) {
3581                TObjectName fc = pivotColumnList.getObjectName(i);
3582                if (fc != null && fc.getColumnNameOnly() != null) {
3583                    consumedColumns.add(fc.getColumnNameOnly().toLowerCase());
3584                }
3585            }
3586        }
3587
3588        // Add pass-through columns as part of the UNPIVOT virtual table's output schema.
3589        // Pass-through columns are source columns that are NOT consumed by UNPIVOT:
3590        // - NOT in the IN clause (consumed columns)
3591        // - NOT the value column (generated by UNPIVOT)
3592        // - NOT the FOR column (generated by UNPIVOT)
3593        //
3594        // This ensures pass-through columns appear in the output even when not explicitly
3595        // referenced in the outer SELECT, similar to how value and FOR columns are added.
3596        //
3597        // NOTE: The source subquery's own columns will ALSO be collected during normal
3598        // traversal (with pivotSourceProcessingDepth reset), so they'll appear with
3599        // source table attribution (e.g., #sample.col1). This gives us dual attribution:
3600        // - #sample.col1 (from source subquery)
3601        // - (pivot-table:unpvt).col1 (as part of UNPIVOT output schema)
3602        if (sourceTable != null && sourceTable.getSubquery() != null && unpivotTable != null) {
3603            TSelectSqlStatement sourceSubquery = sourceTable.getSubquery();
3604            TResultColumnList resultCols = sourceSubquery.getResultColumnList();
3605            if (resultCols != null) {
3606                for (int i = 0; i < resultCols.size(); i++) {
3607                    TResultColumn rc = resultCols.getResultColumn(i);
3608                    if (rc == null) continue;
3609
3610                    // Get the column name from the result column
3611                    // Handle both simple columns and aliased expressions
3612                    String columnName = null;
3613                    if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
3614                        columnName = rc.getAliasClause().getAliasName().toString();
3615                    } else if (rc.getExpr() != null) {
3616                        TExpression expr = rc.getExpr();
3617                        if (expr.getExpressionType() == EExpressionType.simple_object_name_t &&
3618                            expr.getObjectOperand() != null) {
3619                            columnName = expr.getObjectOperand().getColumnNameOnly();
3620                        }
3621                    }
3622
3623                    if (columnName == null || columnName.isEmpty()) continue;
3624
3625                    // Check if this is a pass-through column (not consumed)
3626                    // Normalize column name by removing brackets/quotes
3627                    String normalizedName = columnName.toLowerCase();
3628                    if (normalizedName.startsWith("[") && normalizedName.endsWith("]")) {
3629                        normalizedName = normalizedName.substring(1, normalizedName.length() - 1);
3630                    } else if (normalizedName.startsWith("\"") && normalizedName.endsWith("\"")) {
3631                        normalizedName = normalizedName.substring(1, normalizedName.length() - 1);
3632                    }
3633                    if (!consumedColumns.contains(normalizedName)) {
3634                        // This is a pass-through column - create a synthetic entry
3635                        // We use TObjectName.createObjectName() which properly initializes tokens
3636                        // so getColumnNameOnly() returns the correct value
3637                        TObjectName passthroughCol = TObjectName.createObjectName(
3638                            dbVendor,
3639                            EDbObjectType.column,
3640                            new TSourceToken(columnName)
3641                        );
3642                        passthroughCol.setSourceTable(unpivotTable);
3643
3644                        // Mark as definition column so it's not processed as a reference in isColumnReference()
3645                        unpivotDefinitionColumns.add(passthroughCol);
3646
3647                        // Add to output
3648                        allColumnReferences.add(passthroughCol);
3649
3650                        if (DEBUG_SCOPE_BUILD) {
3651                            System.out.println("[DEBUG] Added UNPIVOT pass-through column: " + columnName);
3652                        }
3653                    }
3654                }
3655            }
3656        }
3657    }
3658
3659    /**
3660     * Find the matching pivot table in the current statement's tables collection.
3661     *
3662     * The visitor traverses through getRelations() which may contain TTable objects
3663     * that are different from those in statement.tables. Since the formatter uses
3664     * object identity to match sourceTable against statement.tables, we need to
3665     * return the TTable from statement.tables.
3666     *
3667     * @param visitedTable The TTable from visitor traversal
3668     * @param alias The alias to match
3669     * @return The matching TTable from statement.tables, or visitedTable if not found
3670     */
3671    private TTable findMatchingPivotTableInStatement(TTable visitedTable, String alias) {
3672        // Get the current statement from the scope
3673        if (currentSelectScope == null || !(currentSelectScope.getNode() instanceof TSelectSqlStatement)) {
3674            return visitedTable;
3675        }
3676
3677        TSelectSqlStatement stmt = (TSelectSqlStatement) currentSelectScope.getNode();
3678        if (stmt.tables == null) {
3679            return visitedTable;
3680        }
3681
3682        // Search for a pivot table with matching alias in statement.tables
3683        for (int i = 0; i < stmt.tables.size(); i++) {
3684            TTable stmtTable = stmt.tables.getTable(i);
3685            if (stmtTable == null) continue;
3686
3687            // Check if this is a pivot table with matching alias
3688            if (stmtTable.getTableType() == ETableSource.pivoted_table) {
3689                String stmtAlias = getTableAlias(stmtTable);
3690                // Try to get alias from pivot clause if not found
3691                if ((stmtAlias == null || stmtAlias.isEmpty() || stmtAlias.startsWith("null"))
3692                    && stmtTable.getPivotedTable() != null
3693                    && stmtTable.getPivotedTable().getPivotClause() != null) {
3694                    TPivotClause pc = stmtTable.getPivotedTable().getPivotClause();
3695                    if (pc.getAliasClause() != null && pc.getAliasClause().getAliasName() != null) {
3696                        stmtAlias = pc.getAliasClause().getAliasName().toString();
3697                    }
3698                }
3699
3700                // Match by alias (case-insensitive)
3701                if (alias != null && stmtAlias != null && alias.equalsIgnoreCase(stmtAlias)) {
3702                    return stmtTable;
3703                }
3704            }
3705        }
3706
3707        // Not found in statement.tables, return the original visited table
3708        return visitedTable;
3709    }
3710
3711    /**
3712     * Find the actual pivot table with PivotedTable info from the statement's joins.
3713     *
3714     * SQL Server UNPIVOT issue: When UNPIVOT is used on a subquery, the table from
3715     * statement.tables may have tableType=pivoted_table but getPivotedTable()=null.
3716     * The actual pivot clause is on a different TTable object from statement.joins.
3717     *
3718     * @param table The pivot table from statement.tables (may have getPivotedTable()=null)
3719     * @return The matching TTable from joins with getPivotedTable() populated, or null if not found
3720     */
3721    private TTable findPivotTableInJoins(TTable table) {
3722        // Get the current statement from the scope
3723        if (currentSelectScope == null || !(currentSelectScope.getNode() instanceof TSelectSqlStatement)) {
3724            return null;
3725        }
3726
3727        TSelectSqlStatement stmt = (TSelectSqlStatement) currentSelectScope.getNode();
3728
3729        // Search in joins for a pivot table with getPivotedTable() != null
3730        TJoinList joins = stmt.joins;
3731        if (joins != null) {
3732            for (int i = 0; i < joins.size(); i++) {
3733                TJoin join = joins.getJoin(i);
3734                if (join == null) continue;
3735
3736                TTable joinTable = join.getTable();
3737                if (joinTable != null &&
3738                    joinTable.getTableType() == ETableSource.pivoted_table &&
3739                    joinTable.getPivotedTable() != null) {
3740                    // Found a pivot table with the actual PivotedTable info
3741                    // Match by alias if available
3742                    String tableAlias = getTableAlias(table);
3743                    String joinTableAlias = getTableAlias(joinTable);
3744
3745                    // If the table from statement.tables has an alias, try to match
3746                    if (tableAlias != null && !tableAlias.isEmpty()) {
3747                        // Try to get alias from pivot clause if joinTableAlias is null or has the
3748                        // placeholder pattern "null(piviot_table)" or similar
3749                        if ((joinTableAlias == null || joinTableAlias.isEmpty() ||
3750                             joinTableAlias.startsWith("null")) &&
3751                            joinTable.getPivotedTable().getPivotClause() != null) {
3752                            TPivotClause pc = joinTable.getPivotedTable().getPivotClause();
3753                            if (pc.getAliasClause() != null && pc.getAliasClause().getAliasName() != null) {
3754                                joinTableAlias = pc.getAliasClause().getAliasName().toString();
3755                            }
3756                        }
3757                        if (tableAlias.equalsIgnoreCase(joinTableAlias)) {
3758                            return joinTable;
3759                        }
3760                    } else {
3761                        // No alias to match, return the first pivot table found
3762                        return joinTable;
3763                    }
3764                }
3765            }
3766        }
3767
3768        return null;
3769    }
3770
3771    /**
3772     * Find a table by alias or name in the current SELECT statement's FROM clause.
3773     * Used for linking EXCEPT columns to the star column's source table.
3774     *
3775     * @param aliasOrName The table alias or name to find
3776     * @return The matching TTable, or null if not found
3777     */
3778    private TTable findTableByAliasInCurrentScope(String aliasOrName) {
3779        if (aliasOrName == null || aliasOrName.isEmpty()) {
3780            return null;
3781        }
3782
3783        // Get the current statement from the scope
3784        if (currentSelectScope == null || !(currentSelectScope.getNode() instanceof TSelectSqlStatement)) {
3785            return null;
3786        }
3787
3788        TSelectSqlStatement stmt = (TSelectSqlStatement) currentSelectScope.getNode();
3789        if (stmt.tables == null) {
3790            return null;
3791        }
3792
3793        // Search for a table with matching alias or name
3794        for (int i = 0; i < stmt.tables.size(); i++) {
3795            TTable table = stmt.tables.getTable(i);
3796            if (table == null) continue;
3797
3798            // Check alias first
3799            String tableAlias = getTableAlias(table);
3800            if (tableAlias != null && !tableAlias.isEmpty() &&
3801                aliasOrName.equalsIgnoreCase(tableAlias)) {
3802                return table;
3803            }
3804
3805            // Check table name
3806            String tableName = table.getTableName() != null ? table.getTableName().toString() : null;
3807            if (tableName != null && aliasOrName.equalsIgnoreCase(tableName)) {
3808                return table;
3809            }
3810
3811            // For subqueries without explicit alias, check the full name
3812            String fullName = table.getFullName();
3813            if (fullName != null && aliasOrName.equalsIgnoreCase(fullName)) {
3814                return table;
3815            }
3816        }
3817
3818        return null;
3819    }
3820
3821    /**
3822     * Resolve the source namespace for a PIVOT table (Delta 2 - pass-through column resolution).
3823     *
3824     * The source namespace is used to resolve pass-through columns that are not
3825     * part of the PIVOT IN clause (e.g., ADDRESS_ID in "SELECT p.ADDRESS_ID, p.[1] FROM CTE PIVOT(...) p").
3826     *
3827     * @param sourceTable The source table of the PIVOT
3828     * @return The namespace for the source table, or null if not found
3829     */
3830    private INamespace resolveSourceNamespaceForPivot(TTable sourceTable) {
3831        if (sourceTable == null) {
3832            return null;
3833        }
3834
3835        String sourceName = sourceTable.getName();
3836
3837        // Case 1: Source is a CTE reference
3838        CTENamespace cteNamespace = findCTEByName(sourceName);
3839        if (cteNamespace != null) {
3840            return cteNamespace;
3841        }
3842
3843        // Case 2: Source is a subquery
3844        if (sourceTable.getSubquery() != null) {
3845            // Create a SubqueryNamespace for the subquery
3846            SubqueryNamespace subqueryNs = new SubqueryNamespace(
3847                sourceTable.getSubquery(),
3848                getTableAlias(sourceTable),
3849                nameMatcher
3850            );
3851            subqueryNs.validate();
3852            return subqueryNs;
3853        }
3854
3855        // Case 3: Source is a physical table - create TableNamespace
3856        TableNamespace tableNs = new TableNamespace(sourceTable, nameMatcher, sqlEnv);
3857        tableNs.validate();
3858        return tableNs;
3859    }
3860
3861    /**
3862     * Process an UNNEST table expression.
3863     *
3864     * UNNEST flattens an array into rows, creating a virtual table:
3865     * <pre>
3866     * SELECT value FROM UNNEST(array_column)
3867     * SELECT element FROM UNNEST(['a', 'b', 'c']) AS element
3868     * SELECT * FROM UNNEST(array_column) WITH OFFSET
3869     * </pre>
3870     *
3871     * The UNNEST table provides:
3872     * 1. An implicit column for the unnested elements (named by alias or 'value')
3873     * 2. Optional WITH OFFSET column for array indices
3874     * 3. STRUCT field columns when unnesting ARRAY<STRUCT<...>>
3875     */
3876    private void processUnnestTable(TTable table) {
3877        String alias = getTableAlias(table);
3878
3879        // Create UnnestNamespace
3880        UnnestNamespace unnestNs = new UnnestNamespace(table, alias, nameMatcher);
3881        unnestNs.validate();
3882
3883        // Add to FROM scope
3884        currentFromScope.addChild(unnestNs, alias, false);
3885        tableToNamespaceMap.put(table, unnestNs);  // Store for legacy compatibility
3886
3887        // Process the array expression inside UNNEST to capture correlated references
3888        // e.g., UNNEST(nested_attribute) - nested_attribute should link to outer table
3889        TUnnestClause unnestClause = table.getUnnestClause();
3890        if (unnestClause != null && unnestClause.getArrayExpr() != null) {
3891            // Visit the array expression to collect column references
3892            // This will be handled by the expression visitor
3893            traverseExpressionForColumns(unnestClause.getArrayExpr());
3894        }
3895
3896        if (DEBUG_SCOPE_BUILD) {
3897            System.out.println("[DEBUG] Added UNNEST table to FromScope: alias=" + alias +
3898                " implicitColumn=" + unnestNs.getImplicitColumnName());
3899        }
3900    }
3901
3902    /**
3903     * Process a Snowflake stage table reference.
3904     * Stage tables reference files in storage (e.g., @stage/path/file.parquet).
3905     * They're treated as tables but without schema metadata - columns are accessed
3906     * via positional references ($1, $2) or JSON path access ($1:field).
3907     */
3908    private void processStageTable(TTable table) {
3909        String alias = getTableAlias(table);
3910        String tableName = table.getFullName();
3911
3912        // Create a TableNamespace for the stage table
3913        // Stage tables don't have metadata, so we use null for sqlEnv
3914        TableNamespace stageNs = new TableNamespace(table, nameMatcher, null);
3915        stageNs.validate();
3916
3917        // Add to FROM scope
3918        currentFromScope.addChild(stageNs, alias, false);
3919        lastProcessedFromTable = table;
3920        currentJoinChainTables.add(table);  // Track ALL tables in join chain for chained USING
3921        tableToNamespaceMap.put(table, stageNs);
3922
3923        if (DEBUG_SCOPE_BUILD) {
3924            System.out.println("[DEBUG] Added stage table to FromScope: alias=" + alias +
3925                " tableName=" + tableName);
3926        }
3927    }
3928
3929    /**
3930     * Process a Teradata TD_UNPIVOT table function.
3931     * TD_UNPIVOT transforms columns into rows. The columns are created during parsing
3932     * in TTDUnpivot.doParse() and linked to either the output table (VALUE_COLUMNS,
3933     * UNPIVOT_COLUMN) or the source table (COLUMN_LIST).
3934     *
3935     * This method collects those columns into allColumnReferences as definition columns
3936     * (they don't need name resolution - their sourceTable is already set).
3937     *
3938     * @see gudusoft.gsqlparser.nodes.teradata.TTDUnpivot
3939     */
3940    private void processTDUnpivotTable(TTable table) {
3941        TTDUnpivot tdUnpivot = table.getTdUnpivot();
3942        if (tdUnpivot == null) {
3943            return;
3944        }
3945
3946        // VALUE_COLUMNS: Output value columns of TD_UNPIVOT
3947        // These columns are created from string literals and linked to the output table
3948        TObjectNameList valueColumns = tdUnpivot.getValueColumns();
3949        if (valueColumns != null) {
3950            for (int i = 0; i < valueColumns.size(); i++) {
3951                TObjectName col = valueColumns.getObjectName(i);
3952                if (col != null) {
3953                    // Mark as definition column (already has sourceTable set during parsing)
3954                    unpivotDefinitionColumns.add(col);
3955                    // Add to output list
3956                    allColumnReferences.add(col);
3957                    if (DEBUG_SCOPE_BUILD) {
3958                        System.out.println("[DEBUG] TD_UNPIVOT valueColumn: " + col.getColumnNameOnly() +
3959                            " -> " + (col.getSourceTable() != null ? col.getSourceTable().getTableName() : "null"));
3960                    }
3961                }
3962            }
3963        }
3964
3965        // UNPIVOT_COLUMN: Output label column of TD_UNPIVOT (contains original column names)
3966        TObjectNameList unpivotColumns = tdUnpivot.getUnpivotColumns();
3967        if (unpivotColumns != null) {
3968            for (int i = 0; i < unpivotColumns.size(); i++) {
3969                TObjectName col = unpivotColumns.getObjectName(i);
3970                if (col != null) {
3971                    // Mark as definition column
3972                    unpivotDefinitionColumns.add(col);
3973                    // Add to output list
3974                    allColumnReferences.add(col);
3975                    if (DEBUG_SCOPE_BUILD) {
3976                        System.out.println("[DEBUG] TD_UNPIVOT unpivotColumn: " + col.getColumnNameOnly() +
3977                            " -> " + (col.getSourceTable() != null ? col.getSourceTable().getTableName() : "null"));
3978                    }
3979                }
3980            }
3981        }
3982
3983        // COLUMN_LIST: Source columns being unpivoted
3984        // These columns are linked to the source table (the table in the ON clause)
3985        TObjectNameList columnList = tdUnpivot.getColumnList();
3986        if (columnList != null) {
3987            for (int i = 0; i < columnList.size(); i++) {
3988                TObjectName col = columnList.getObjectName(i);
3989                if (col != null) {
3990                    // Mark as definition column
3991                    unpivotDefinitionColumns.add(col);
3992                    // Add to output list
3993                    allColumnReferences.add(col);
3994                    if (DEBUG_SCOPE_BUILD) {
3995                        System.out.println("[DEBUG] TD_UNPIVOT columnList: " + col.getColumnNameOnly() +
3996                            " -> " + (col.getSourceTable() != null ? col.getSourceTable().getTableName() : "null"));
3997                    }
3998                }
3999            }
4000        }
4001    }
4002
4003    /**
4004     * Process a VALUES table (inline data with column aliases).
4005     * Example: VALUES (1, 'a'), (2, 'b') AS t(id, name)
4006     * Used in Teradata MERGE: USING VALUES (:empno, :name, :salary) AS s(empno, name, salary)
4007     */
4008    private void processValuesTable(TTable table) {
4009        String alias = getTableAlias(table);
4010
4011        // Track column name definitions from the alias clause
4012        // These are column DEFINITIONS, not references - should NOT be collected as column refs
4013        // e.g., in "VALUES (1, 'a') AS t(id, name)", 'id' and 'name' are definitions
4014        TAliasClause aliasClause = table.getAliasClause();
4015        if (aliasClause != null) {
4016            TObjectNameList columns = aliasClause.getColumns();
4017            if (columns != null && columns.size() > 0) {
4018                for (int i = 0; i < columns.size(); i++) {
4019                    TObjectName colName = columns.getObjectName(i);
4020                    if (colName != null) {
4021                        valuesTableAliasColumns.add(colName);
4022                        if (DEBUG_SCOPE_BUILD) {
4023                            System.out.println("[DEBUG] Tracked VALUES alias column definition (will skip): " + colName.toString());
4024                        }
4025                    }
4026                }
4027            }
4028        }
4029
4030        // Create ValuesNamespace - columns are extracted from alias clause
4031        ValuesNamespace valuesNs = new ValuesNamespace(table, alias, nameMatcher);
4032        valuesNs.validate();
4033
4034        // Add to FROM scope with the table alias
4035        currentFromScope.addChild(valuesNs, alias, false);
4036        tableToNamespaceMap.put(table, valuesNs);  // Store for legacy compatibility
4037        lastProcessedFromTable = table;
4038        currentJoinChainTables.add(table);  // Track ALL tables in join chain for chained USING
4039
4040        if (DEBUG_SCOPE_BUILD) {
4041            System.out.println("[DEBUG] Added VALUES table to FromScope: alias=" + alias +
4042                ", columns=" + valuesNs.getAllColumnSources().keySet());
4043        }
4044    }
4045
4046    /**
4047     * Process a potential CTE reference
4048     */
4049    private void processCTEReference(TTable table) {
4050        String tableName = table.getName();
4051        CTENamespace cteNamespace = findCTEByName(tableName);
4052
4053        if (cteNamespace != null) {
4054            String alias = getTableAlias(table);
4055            currentFromScope.addChild(cteNamespace, alias, false);
4056        }
4057    }
4058
4059    /**
4060     * Find a CTE by name in all enclosing CTE scopes
4061     */
4062    private CTENamespace findCTEByName(String name) {
4063        if (name == null) {
4064            return null;
4065        }
4066
4067        // Normalize name by stripping SQL Server bracket delimiters [name] -> name
4068        String normalizedName = stripBrackets(name);
4069
4070        // Search through scope stack for CTE scopes
4071        for (int i = scopeStack.size() - 1; i >= 0; i--) {
4072            IScope scope = scopeStack.get(i);
4073            if (scope instanceof CTEScope) {
4074                CTEScope cteScope = (CTEScope) scope;
4075                CTENamespace cte = cteScope.getCTE(normalizedName);
4076                if (cte != null) {
4077                    return cte;
4078                }
4079            }
4080        }
4081
4082        return null;
4083    }
4084
4085    /**
4086     * Strip SQL Server bracket delimiters from a name.
4087     * Converts "[name]" to "name", leaves unbracketed names unchanged.
4088     */
4089    private String stripBrackets(String name) {
4090        if (name == null) {
4091            return null;
4092        }
4093        if (name.startsWith("[") && name.endsWith("]") && name.length() > 2) {
4094            return name.substring(1, name.length() - 1);
4095        }
4096        return name;
4097    }
4098
4099    /**
4100     * Strip string delimiters (double quotes and single quotes) from a constant value.
4101     * Used for PIVOT IN clause constant values like "HOUSTON" or 'value'.
4102     * SQL Server brackets are preserved as they're often needed for special names.
4103     */
4104    private String stripStringDelimiters(String value) {
4105        if (value == null || value.isEmpty()) {
4106            return value;
4107        }
4108        // Strip double quotes (BigQuery style identifier)
4109        if (value.startsWith("\"") && value.endsWith("\"") && value.length() > 2) {
4110            return value.substring(1, value.length() - 1);
4111        }
4112        // Strip single quotes (string literal)
4113        if (value.startsWith("'") && value.endsWith("'") && value.length() > 2) {
4114            return value.substring(1, value.length() - 1);
4115        }
4116        // SQL Server brackets [] are preserved as they're part of the column identity
4117        return value;
4118    }
4119
4120    /**
4121     * Get the alias for a table, or the table name if no alias
4122     */
4123    private String getTableAlias(TTable table) {
4124        if (table.getAliasName() != null && !table.getAliasName().isEmpty()) {
4125            return table.getAliasName();
4126        }
4127        return table.getName();
4128    }
4129
4130    // ========== JOIN ==========
4131
4132    @Override
4133    public void preVisit(TJoinExpr joinExpr) {
4134        // JOIN expressions are traversed automatically
4135        // Left and right tables will trigger preVisit(TTable)
4136        // ON condition columns will trigger preVisit(TObjectName)
4137        if (DEBUG_SCOPE_BUILD) {
4138            System.out.println("[DEBUG] preVisit(TJoinExpr): " + joinExpr +
4139                               ", usingColumns=" + (joinExpr.getUsingColumns() != null ? joinExpr.getUsingColumns().size() : "null") +
4140                               ", leftTable=" + joinExpr.getLeftTable() +
4141                               ", rightTable=" + joinExpr.getRightTable() +
4142                               ", joinChainTables=" + currentJoinChainTables.size());
4143        }
4144
4145        // Handle USING columns in TJoinExpr (used when USE_JOINEXPR_INSTEAD_OF_JOIN is true)
4146        if (joinExpr.getUsingColumns() != null && joinExpr.getUsingColumns().size() > 0) {
4147            TTable rightTable = joinExpr.getRightTable();
4148
4149            // For TJoinExpr, the left side may be another TJoinExpr (nested joins) or a single table.
4150            // We need to collect ALL tables on the left side recursively.
4151            List<TTable> leftSideTables = collectTablesFromJoinExpr(joinExpr.getLeftTable());
4152
4153            if (DEBUG_SCOPE_BUILD) {
4154                System.out.println("[DEBUG] preVisit(TJoinExpr) USING: leftSideTables=" + leftSideTables.size());
4155                for (TTable t : leftSideTables) {
4156                    System.out.println("[DEBUG]   - " + t.getFullName());
4157                }
4158            }
4159
4160            if (rightTable != null) {
4161                currentUsingJoinRightTable = rightTable;
4162                // USING clause semantic: For chained joins like "t1 JOIN t2 USING (c1) JOIN t3 USING (c2)",
4163                // the USING column c2 should be linked to ALL tables on the left side (t1 and t2), not just one.
4164                // This is because the left side of the second join is the result of (t1 JOIN t2).
4165                for (int i = 0; i < joinExpr.getUsingColumns().size(); i++) {
4166                    TObjectName usingCol = joinExpr.getUsingColumns().getObjectName(i);
4167                    if (usingCol != null) {
4168                        // Create synthetic columns for ALL tables on the left side
4169                        // The first table gets the original USING column, the rest get clones
4170                        boolean isFirstTable = true;
4171                        for (TTable leftTable : leftSideTables) {
4172                            if (isFirstTable) {
4173                                // Original USING column -> first left table
4174                                usingColumnToLeftTable.put(usingCol, leftTable);
4175                                isFirstTable = false;
4176                            } else {
4177                                // Create synthetic column for additional left tables
4178                                TObjectName chainTableCol = usingCol.clone();
4179                                chainTableCol.setSourceTable(leftTable);
4180
4181                                // Add the synthetic chain table column to references
4182                                if (currentSelectScope != null) {
4183                                    columnToScopeMap.put(chainTableCol, currentSelectScope);
4184                                }
4185                                allColumnReferences.add(chainTableCol);
4186
4187                                // Track in USING map for resolution
4188                                usingColumnToLeftTable.put(chainTableCol, leftTable);
4189
4190                                if (DEBUG_SCOPE_BUILD) {
4191                                    System.out.println("[DEBUG] Created synthetic USING column for left table: " +
4192                                                       usingCol.getColumnNameOnly() + " -> " + leftTable.getFullName());
4193                                }
4194                            }
4195                        }
4196
4197                        // Create a synthetic column reference for the right table
4198                        // Clone it and set the clone's sourceTable to right table
4199                        TObjectName rightTableCol = usingCol.clone();
4200                        rightTableCol.setSourceTable(rightTable);
4201
4202                        // Add the synthetic right table column to references
4203                        if (currentSelectScope != null) {
4204                            columnToScopeMap.put(rightTableCol, currentSelectScope);
4205                        }
4206                        allColumnReferences.add(rightTableCol);
4207
4208                        // ONLY track the synthetic column in USING map for right table resolution
4209                        usingColumnToRightTable.put(rightTableCol, rightTable);
4210                    }
4211                }
4212            }
4213        }
4214    }
4215
4216    /**
4217     * Collect all tables from a TTable that might be a join structure.
4218     * If the table is a join (type=join), recursively collect tables from the join tree.
4219     * If it's a simple table (objectname), return just that table.
4220     */
4221    private List<TTable> collectTablesFromJoinExpr(TTable table) {
4222        List<TTable> tables = new ArrayList<>();
4223        if (table == null) {
4224            return tables;
4225        }
4226
4227        if (table.getTableType() == ETableSource.join && table.getJoinExpr() != null) {
4228            // This is a join structure - recursively collect from both sides
4229            TJoinExpr joinExpr = table.getJoinExpr();
4230            tables.addAll(collectTablesFromJoinExpr(joinExpr.getLeftTable()));
4231            tables.addAll(collectTablesFromJoinExpr(joinExpr.getRightTable()));
4232        } else {
4233            // Simple table - add it
4234            tables.add(table);
4235        }
4236
4237        return tables;
4238    }
4239
4240    @Override
4241    public void preVisit(TJoinItem joinItem) {
4242        // Track the right-side table for JOIN...USING column resolution priority
4243        // In "a JOIN table2 USING (id)", joinItem.getTable() is table2 (the right side)
4244        if (DEBUG_SCOPE_BUILD) {
4245            System.out.println("[DEBUG] preVisit(TJoinItem): " + joinItem +
4246                               ", usingColumns=" + (joinItem.getUsingColumns() != null ? joinItem.getUsingColumns().size() : "null") +
4247                               ", lastProcessedFromTable=" + lastProcessedFromTable +
4248                               ", joinChainTables=" + currentJoinChainTables.size());
4249        }
4250        if (joinItem.getUsingColumns() != null && joinItem.getUsingColumns().size() > 0) {
4251            TTable rightTable = joinItem.getTable();
4252            TTable leftTable = lastProcessedFromTable;  // The table processed before this JOIN
4253            if (rightTable != null) {
4254                currentUsingJoinRightTable = rightTable;
4255                // USING clause semantic: For chained joins like "t1 JOIN t2 USING (c1) JOIN t3 USING (c2)",
4256                // the USING column c2 should be linked to ALL tables on the left side (t1 and t2), not just t2.
4257                // This is because the left side of the second join is the result of (t1 JOIN t2).
4258                for (int i = 0; i < joinItem.getUsingColumns().size(); i++) {
4259                    TObjectName usingCol = joinItem.getUsingColumns().getObjectName(i);
4260                    if (usingCol != null) {
4261                        // Track original USING column -> immediate left table (for reference only)
4262                        // This is the primary left table association
4263                        if (leftTable != null) {
4264                            usingColumnToLeftTable.put(usingCol, leftTable);
4265                        }
4266
4267                        // Create synthetic columns for ALL tables in the join chain (left side of this join)
4268                        // This ensures that in "t1 JOIN t2 USING (c1) JOIN t3 USING (c2)", column c2
4269                        // is linked to both t1 and t2, not just t2.
4270                        for (TTable chainTable : currentJoinChainTables) {
4271                            if (chainTable != leftTable) {  // Skip leftTable - handled by original usingCol
4272                                TObjectName chainTableCol = usingCol.clone();
4273                                chainTableCol.setSourceTable(chainTable);
4274
4275                                // Add the synthetic chain table column to references
4276                                if (currentSelectScope != null) {
4277                                    columnToScopeMap.put(chainTableCol, currentSelectScope);
4278                                }
4279                                allColumnReferences.add(chainTableCol);
4280
4281                                // Track in USING map for resolution
4282                                usingColumnToLeftTable.put(chainTableCol, chainTable);
4283
4284                                if (DEBUG_SCOPE_BUILD) {
4285                                    System.out.println("[DEBUG] Created synthetic USING column for chain table: " +
4286                                                       usingCol.getColumnNameOnly() + " -> " + chainTable.getFullName());
4287                                }
4288                            }
4289                        }
4290
4291                        // Create a synthetic column reference for the right table
4292                        // The original usingCol has sourceTable = left table (set by parser)
4293                        // Clone it and set the clone's sourceTable to right table
4294                        TObjectName rightTableCol = usingCol.clone();
4295                        rightTableCol.setSourceTable(rightTable);
4296
4297                        // Add the synthetic right table column to references
4298                        if (currentSelectScope != null) {
4299                            columnToScopeMap.put(rightTableCol, currentSelectScope);
4300                        }
4301                        allColumnReferences.add(rightTableCol);
4302
4303                        // ONLY track the synthetic column in USING map for right table resolution
4304                        // Do NOT add the original usingCol - it should keep its left table resolution
4305                        usingColumnToRightTable.put(rightTableCol, rightTable);
4306                    }
4307                }
4308            }
4309        }
4310    }
4311
4312    @Override
4313    public void postVisit(TJoinItem joinItem) {
4314        // Clear the current USING join right table after processing
4315        if (joinItem.getUsingColumns() != null && joinItem.getUsingColumns().size() > 0) {
4316            currentUsingJoinRightTable = null;
4317        }
4318    }
4319
4320    // ========== Expressions ==========
4321
4322    @Override
4323    public void preVisit(TExpression expression) {
4324        // Handle function expressions (type function_t) - the visitor may not automatically
4325        // traverse to getFunctionCall() and its arguments
4326        if (expression.getExpressionType() == EExpressionType.function_t &&
4327            expression.getFunctionCall() != null) {
4328            TFunctionCall func = expression.getFunctionCall();
4329
4330            // Handle STRUCT and similar functions that store field values in getFieldValues()
4331            if (func.getFieldValues() != null && func.getFieldValues().size() > 0) {
4332                for (int i = 0; i < func.getFieldValues().size(); i++) {
4333                    TResultColumn fieldValue = func.getFieldValues().getResultColumn(i);
4334                    if (fieldValue != null && fieldValue.getExpr() != null) {
4335                        traverseExpressionForColumns(fieldValue.getExpr());
4336                    }
4337                }
4338            }
4339
4340            // Handle regular functions with getArgs() (e.g., TO_JSON_STRING, ARRAY_LENGTH, etc.)
4341            if (func.getArgs() != null && func.getArgs().size() > 0) {
4342                for (int i = 0; i < func.getArgs().size(); i++) {
4343                    TExpression argExpr = func.getArgs().getExpression(i);
4344                    if (argExpr != null) {
4345                        traverseExpressionForColumns(argExpr);
4346                    }
4347                }
4348            }
4349
4350            // Handle special function expressions (CAST, CONVERT, EXTRACT, etc.)
4351            // These functions store their arguments in expr1/expr2/expr3 instead of args
4352            if (func.getExpr1() != null) {
4353                traverseExpressionForColumns(func.getExpr1());
4354            }
4355            if (func.getExpr2() != null) {
4356                traverseExpressionForColumns(func.getExpr2());
4357            }
4358            if (func.getExpr3() != null) {
4359                traverseExpressionForColumns(func.getExpr3());
4360            }
4361        }
4362
4363        // Handle array expressions - objectOperand contains the column reference
4364        if (expression.getExpressionType() == EExpressionType.array_t &&
4365            expression.getObjectOperand() != null) {
4366            preVisit(expression.getObjectOperand());
4367        }
4368
4369        // Handle array access expressions (e.g., str2['ptype'] in SparkSQL/Hive)
4370        // The column reference is in the LeftOperand
4371        if (expression.getExpressionType() == EExpressionType.array_access_expr_t) {
4372            if (expression.getLeftOperand() != null) {
4373                traverseExpressionForColumns(expression.getLeftOperand());
4374            }
4375        }
4376
4377        // Handle CASE expressions - traverse all sub-expressions to collect column references
4378        if (expression.getExpressionType() == EExpressionType.case_t &&
4379            expression.getCaseExpression() != null) {
4380            if (DEBUG_SCOPE_BUILD) {
4381                System.out.println("[DEBUG] preVisit(TExpression): Found CASE expression, currentSelectScope=" +
4382                    (currentSelectScope != null ? "set" : "null"));
4383            }
4384            traverseExpressionForColumns(expression);
4385        }
4386
4387        // Handle lambda expressions - mark parameters as NOT column references
4388        // Lambda parameters are local function parameters, not table column references
4389        // e.g., in "aggregate(array, 0, (acc, x) -> acc + x)", acc and x are lambda parameters
4390        if (expression.getExpressionType() == EExpressionType.lambda_t) {
4391            TExpression paramExpr = expression.getLeftOperand();
4392            TExpression bodyExpr = expression.getRightOperand();
4393            if (paramExpr != null) {
4394                // Collect parameter names first
4395                Set<String> paramNames = new HashSet<>();
4396                collectLambdaParameterNames(paramExpr, paramNames);
4397
4398                // Push parameter names onto the stack for use in preVisit(TObjectName)
4399                lambdaParameterStack.push(paramNames);
4400
4401                // Mark the parameter definition TObjectNames
4402                collectLambdaParameterObjects(paramExpr);
4403
4404                // Mark all usages in the body that match parameter names
4405                if (bodyExpr != null && !paramNames.isEmpty()) {
4406                    markLambdaParameterUsages(bodyExpr, paramNames);
4407                }
4408            }
4409        }
4410
4411        // Handle typecast expressions - the visitor may not automatically traverse the left operand
4412        // e.g., for "$1:apMac::string", we need to traverse "$1:apMac" to collect the column reference
4413        if (expression.getExpressionType() == EExpressionType.typecast_t) {
4414            if (expression.getLeftOperand() != null) {
4415                traverseExpressionForColumns(expression.getLeftOperand());
4416            }
4417        }
4418    }
4419
4420    @Override
4421    public void postVisit(TExpression expression) {
4422        // Pop lambda parameter context when exiting a lambda expression
4423        if (expression.getExpressionType() == EExpressionType.lambda_t) {
4424            TExpression paramExpr = expression.getLeftOperand();
4425            if (paramExpr != null && !lambdaParameterStack.isEmpty()) {
4426                lambdaParameterStack.pop();
4427            }
4428        }
4429    }
4430
4431    /**
4432     * Recursively collect lambda parameter NAMES as strings.
4433     */
4434    private void collectLambdaParameterNames(TExpression paramExpr, Set<String> paramNames) {
4435        if (paramExpr == null) return;
4436
4437        // Single parameter: expression has objectOperand
4438        if (paramExpr.getObjectOperand() != null) {
4439            String name = paramExpr.getObjectOperand().toString();
4440            if (name != null && !name.isEmpty()) {
4441                paramNames.add(name.toLowerCase());
4442            }
4443            return;
4444        }
4445
4446        // Multiple parameters: expression has exprList
4447        if (paramExpr.getExprList() != null) {
4448            for (int i = 0; i < paramExpr.getExprList().size(); i++) {
4449                TExpression e = paramExpr.getExprList().getExpression(i);
4450                collectLambdaParameterNames(e, paramNames);
4451            }
4452        }
4453    }
4454
4455    /**
4456     * Recursively collect all TObjectName nodes from lambda parameter definitions.
4457     */
4458    private void collectLambdaParameterObjects(TExpression paramExpr) {
4459        if (paramExpr == null) return;
4460
4461        if (paramExpr.getObjectOperand() != null) {
4462            lambdaParameters.add(paramExpr.getObjectOperand());
4463            return;
4464        }
4465
4466        if (paramExpr.getExprList() != null) {
4467            for (int i = 0; i < paramExpr.getExprList().size(); i++) {
4468                TExpression e = paramExpr.getExprList().getExpression(i);
4469                collectLambdaParameterObjects(e);
4470            }
4471        }
4472    }
4473
4474    /**
4475     * Recursively find and mark all TObjectName usages in a lambda body that match parameter names.
4476     */
4477    private void markLambdaParameterUsages(TExpression bodyExpr, Set<String> paramNames) {
4478        if (bodyExpr == null) return;
4479
4480        // Check if this expression is a simple column reference matching a parameter name
4481        if (bodyExpr.getObjectOperand() != null) {
4482            TObjectName objName = bodyExpr.getObjectOperand();
4483            String name = objName.toString();
4484            if (name != null && paramNames.contains(name.toLowerCase())) {
4485                lambdaParameters.add(objName);
4486            }
4487        }
4488
4489        // Recurse into sub-expressions
4490        if (bodyExpr.getLeftOperand() != null) {
4491            markLambdaParameterUsages(bodyExpr.getLeftOperand(), paramNames);
4492        }
4493        if (bodyExpr.getRightOperand() != null) {
4494            markLambdaParameterUsages(bodyExpr.getRightOperand(), paramNames);
4495        }
4496        if (bodyExpr.getExprList() != null) {
4497            for (int i = 0; i < bodyExpr.getExprList().size(); i++) {
4498                markLambdaParameterUsages(bodyExpr.getExprList().getExpression(i), paramNames);
4499            }
4500        }
4501
4502        // Handle CASE expressions
4503        if (bodyExpr.getCaseExpression() != null) {
4504            TCaseExpression caseExpr = bodyExpr.getCaseExpression();
4505            if (caseExpr.getInput_expr() != null) {
4506                markLambdaParameterUsages(caseExpr.getInput_expr(), paramNames);
4507            }
4508            if (caseExpr.getElse_expr() != null) {
4509                markLambdaParameterUsages(caseExpr.getElse_expr(), paramNames);
4510            }
4511            if (caseExpr.getWhenClauseItemList() != null) {
4512                for (int i = 0; i < caseExpr.getWhenClauseItemList().size(); i++) {
4513                    TWhenClauseItem item = caseExpr.getWhenClauseItemList().getWhenClauseItem(i);
4514                    if (item.getComparison_expr() != null) {
4515                        markLambdaParameterUsages(item.getComparison_expr(), paramNames);
4516                    }
4517                    if (item.getReturn_expr() != null) {
4518                        markLambdaParameterUsages(item.getReturn_expr(), paramNames);
4519                    }
4520                }
4521            }
4522        }
4523
4524        // Handle function calls
4525        if (bodyExpr.getFunctionCall() != null) {
4526            TFunctionCall func = bodyExpr.getFunctionCall();
4527            if (func.getArgs() != null) {
4528                for (int i = 0; i < func.getArgs().size(); i++) {
4529                    markLambdaParameterUsages(func.getArgs().getExpression(i), paramNames);
4530                }
4531            }
4532        }
4533    }
4534
4535    // ========== Function Calls ==========
4536
4537    @Override
4538    public void preVisit(TFunctionCall functionCall) {
4539        // Handle SQL Server UPDATE() function in trigger context
4540        // UPDATE(column_name) is used in triggers to check if a column was updated
4541        // The column argument should be resolved to the trigger target table
4542        if ((dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) &&
4543            currentTriggerTargetTable != null &&
4544            functionCall.getFunctionName() != null) {
4545            String funcName = functionCall.getFunctionName().toString();
4546            if ("update".equalsIgnoreCase(funcName) || "columns_updated".equalsIgnoreCase(funcName)) {
4547                // UPDATE(column) - link the column argument to trigger target table
4548                if (functionCall.getArgs() != null && functionCall.getArgs().size() == 1) {
4549                    TExpression argExpr = functionCall.getArgs().getExpression(0);
4550                    if (argExpr != null && argExpr.getObjectOperand() != null) {
4551                        TObjectName columnName = argExpr.getObjectOperand();
4552                        columnName.setSourceTable(currentTriggerTargetTable);
4553
4554                        // Add to allColumnReferences if not already there
4555                        if (!allColumnReferences.contains(columnName)) {
4556                            allColumnReferences.add(columnName);
4557                        }
4558
4559                        // Map the column to the current scope
4560                        IScope currentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
4561                        columnToScopeMap.put(columnName, currentScope);
4562
4563                        if (DEBUG_SCOPE_BUILD) {
4564                            System.out.println("[DEBUG] preVisit(TFunctionCall/update): " +
4565                                columnName + " -> " + currentTriggerTargetTable.getFullName());
4566                        }
4567                    }
4568                }
4569            }
4570        }
4571
4572        // Mark keyword arguments in built-in functions so they are not treated as column references.
4573        // For example, SECOND in TIMESTAMP_DIFF(ts1, ts2, SECOND) should not be collected as a column.
4574        // We do this in preVisit because TObjectName nodes are visited after TFunctionCall.
4575        markFunctionKeywordArguments(functionCall);
4576
4577        // Handle special functions like STRUCT that store field values in getFieldValues()
4578        // instead of getArgs(). The visitor pattern may not automatically traverse these.
4579        if (functionCall.getFieldValues() != null && functionCall.getFieldValues().size() > 0) {
4580            // STRUCT and similar functions - traverse the field values to collect column references
4581            for (int i = 0; i < functionCall.getFieldValues().size(); i++) {
4582                TResultColumn fieldValue = functionCall.getFieldValues().getResultColumn(i);
4583                if (fieldValue != null && fieldValue.getExpr() != null) {
4584                    // Manually traverse the field value expression
4585                    traverseExpressionForColumns(fieldValue.getExpr());
4586                }
4587            }
4588        }
4589
4590        // Handle special function expressions (CAST, CONVERT, EXTRACT, etc.)
4591        // These functions store their arguments in expr1/expr2/expr3 instead of args
4592        // The visitor pattern may not automatically traverse these expressions.
4593        if (functionCall.getExpr1() != null) {
4594            traverseExpressionForColumns(functionCall.getExpr1());
4595        }
4596        if (functionCall.getExpr2() != null) {
4597            traverseExpressionForColumns(functionCall.getExpr2());
4598        }
4599        if (functionCall.getExpr3() != null) {
4600            traverseExpressionForColumns(functionCall.getExpr3());
4601        }
4602
4603        // Handle XML functions that store their arguments in special properties
4604        // These functions don't use getArgs() but have dedicated value expression lists
4605        // XMLELEMENT: getXMLElementValueExprList() contains the value expressions
4606        // XMLFOREST: getXMLForestValueList() contains the value expressions
4607        // XMLQUERY/XMLEXISTS: getXmlPassingClause().getPassingList() contains column refs
4608        // XMLAttributes: getXMLAttributesClause().getValueExprList() contains attributes
4609        if (functionCall.getXMLElementValueExprList() != null) {
4610            TResultColumnList xmlValueList = functionCall.getXMLElementValueExprList();
4611            for (int i = 0; i < xmlValueList.size(); i++) {
4612                TResultColumn rc = xmlValueList.getResultColumn(i);
4613                if (rc != null && rc.getExpr() != null) {
4614                    traverseExpressionForColumns(rc.getExpr());
4615                }
4616            }
4617        }
4618        if (functionCall.getXMLForestValueList() != null) {
4619            TResultColumnList xmlForestList = functionCall.getXMLForestValueList();
4620            for (int i = 0; i < xmlForestList.size(); i++) {
4621                TResultColumn rc = xmlForestList.getResultColumn(i);
4622                if (rc != null && rc.getExpr() != null) {
4623                    traverseExpressionForColumns(rc.getExpr());
4624                }
4625            }
4626        }
4627        if (functionCall.getXmlPassingClause() != null &&
4628            functionCall.getXmlPassingClause().getPassingList() != null) {
4629            TResultColumnList passingList = functionCall.getXmlPassingClause().getPassingList();
4630            for (int i = 0; i < passingList.size(); i++) {
4631                TResultColumn rc = passingList.getResultColumn(i);
4632                if (rc != null && rc.getExpr() != null) {
4633                    traverseExpressionForColumns(rc.getExpr());
4634                }
4635            }
4636        }
4637        if (functionCall.getXMLAttributesClause() != null &&
4638            functionCall.getXMLAttributesClause().getValueExprList() != null) {
4639            TResultColumnList attrList = functionCall.getXMLAttributesClause().getValueExprList();
4640            for (int i = 0; i < attrList.size(); i++) {
4641                TResultColumn rc = attrList.getResultColumn(i);
4642                if (rc != null && rc.getExpr() != null) {
4643                    traverseExpressionForColumns(rc.getExpr());
4644                }
4645            }
4646        }
4647        // Handle XMLCAST/XMLQUERY typeExpression - stores the inner expression to cast
4648        // For XMLCAST(expr AS type), the expr is stored in typeExpression
4649        if (functionCall.getTypeExpression() != null) {
4650            traverseExpressionForColumns(functionCall.getTypeExpression());
4651        }
4652
4653        // Skip if this is a table-valued function (from FROM clause)
4654        // Table functions like [exce].[sampleTable]() should not be treated as column.method()
4655        if (tableValuedFunctionCalls.contains(functionCall)) {
4656            return;
4657        }
4658
4659        // Handle OGC/spatial/CLR method calls on columns (SQL Server specific)
4660        // These can be in two forms:
4661        // 1. table.column.method() - 3-part name where:
4662        //    - databaseToken = table alias (e.g., "ad")
4663        //    - schemaToken = column name (e.g., "SpatialLocation")
4664        //    - objectToken = method name (e.g., "STDistance")
4665        //
4666        // 2. column.method() - 2-part name where:
4667        //    - schemaToken = column name (e.g., "SpatialLocation")
4668        //    - objectToken = method name (e.g., "STDistance")
4669        //    - databaseToken = null
4670        //    In this case, if there's only one table in FROM, infer the column belongs to it
4671        //
4672        // NOTE: This is SQL Server specific because Oracle uses schema.function() syntax
4673        // for package calls (e.g., DBMS_OUTPUT.PUT_LINE, ERRLOG.LOAD_ERR_DTL).
4674        //
4675        // NOTE: We must skip static type methods like "geography::STGeomFromText()"
4676        // These use the :: syntax and the "schemaToken" is actually a type name, not a column.
4677        //
4678        // We extract the column reference and link it to the source table
4679        // Only apply column method handling for SQL Server (not Oracle, etc.)
4680        if (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) {
4681            TObjectName funcName = functionCall.getFunctionName();
4682
4683            if (DEBUG_SCOPE_BUILD) {
4684                System.out.println("[DEBUG] preVisit(TFunctionCall): " + functionCall);
4685                System.out.println("[DEBUG]   funcName: " + funcName);
4686                if (funcName != null) {
4687                    System.out.println("[DEBUG]   schemaToken: " + funcName.getSchemaToken());
4688                    System.out.println("[DEBUG]   databaseToken: " + funcName.getDatabaseToken());
4689                    System.out.println("[DEBUG]   objectToken: " + funcName.getObjectToken());
4690                    System.out.println("[DEBUG]   currentFromScope: " + (currentFromScope != null ? "exists" : "null"));
4691                }
4692            }
4693
4694            if (funcName != null) {
4695                String funcNameStr = funcName.toString();
4696
4697                // Skip static type methods like "geography::STGeomFromText()"
4698                // These are identified by the "::" in the function name string
4699                if (funcNameStr != null && funcNameStr.contains("::")) {
4700                    if (DEBUG_SCOPE_BUILD) {
4701                        System.out.println("[DEBUG]   Skipping static type method: " + funcNameStr);
4702                    }
4703                    return; // Don't process static type methods as column references
4704                }
4705
4706                if (funcName.getSchemaToken() != null) {
4707                    // schemaToken might be a column name (column.method()) or a schema name (schema.function())
4708                    // We distinguish by:
4709                    // 1. First checking SQLEnv if the full function name exists as a registered function
4710                    // 2. Then checking if the first part is a known SQL Server system schema name
4711                    String possibleColumnOrSchema = funcName.getSchemaToken().toString();
4712
4713                    // Check SQLEnv first - if the function is registered, this is schema.function()
4714                    // This handles user-defined functions with custom schema names (e.g., dbo1.ufnGetInventoryStock)
4715                    if (sqlEnv != null && sqlEnv.searchFunction(funcNameStr) != null) {
4716                        if (DEBUG_SCOPE_BUILD) {
4717                            System.out.println("[DEBUG]   Skipping schema.function() call (found in SQLEnv): " + funcNameStr);
4718                        }
4719                        return; // Don't treat as column.method()
4720                    }
4721
4722                    // Also check for known SQL Server system schema names as a fallback
4723                    // (for functions not explicitly created in this batch but are system/built-in)
4724                    if (isSqlServerSchemaName(possibleColumnOrSchema)) {
4725                        if (DEBUG_SCOPE_BUILD) {
4726                            System.out.println("[DEBUG]   Skipping schema.function() call (system schema): " + funcNameStr);
4727                        }
4728                        return; // Don't treat as column.method()
4729                    }
4730
4731                    if (funcName.getDatabaseToken() != null) {
4732                        // Case 1: 3-part name (table.column.method)
4733                        String tableAlias = funcName.getDatabaseToken().toString();
4734                        if (DEBUG_SCOPE_BUILD) {
4735                            System.out.println("[DEBUG]   Detected 3-part name: " + tableAlias + "." + possibleColumnOrSchema + ".method");
4736                        }
4737                        handleMethodCallOnColumn(tableAlias, possibleColumnOrSchema);
4738                    } else if (currentFromScope != null) {
4739                        // Case 2: 2-part name (column.method) with schemaToken set
4740                        if (DEBUG_SCOPE_BUILD) {
4741                            System.out.println("[DEBUG]   Detected 2-part name (schemaToken): " + possibleColumnOrSchema + ".method");
4742                        }
4743                        handleUnqualifiedMethodCall(possibleColumnOrSchema);
4744                    }
4745                } else if (funcNameStr != null && funcNameStr.contains(".") && currentFromScope != null) {
4746                    // Alternative case: function name contains dots but schemaToken is null
4747                    // This happens in UPDATE SET clause: Location.SetXY(...)
4748                    // Or in SELECT with 3-part names: p.Demographics.value(...)
4749                    //
4750                    // We need to distinguish between:
4751                    // - 2-part: column.method() - first part is a column name
4752                    // - 3-part: table.column.method() - first part is a table alias
4753                    //
4754                    // We check if the first part matches a table alias in the current FROM scope
4755                    int firstDotPos = funcNameStr.indexOf('.');
4756                    if (firstDotPos > 0) {
4757                        String firstPart = funcNameStr.substring(0, firstDotPos);
4758                        String remainder = funcNameStr.substring(firstDotPos + 1);
4759
4760                        // Check if first part is a table alias
4761                        boolean firstPartIsTable = false;
4762                        for (ScopeChild child : currentFromScope.getChildren()) {
4763                            if (nameMatcher.matches(child.getAlias(), firstPart)) {
4764                                firstPartIsTable = true;
4765                                break;
4766                            }
4767                        }
4768
4769                        if (firstPartIsTable) {
4770                            // 3-part name: table.column.method()
4771                            // Extract column name from remainder (before the next dot, if any)
4772                            int secondDotPos = remainder.indexOf('.');
4773                            if (secondDotPos > 0) {
4774                                String columnName = remainder.substring(0, secondDotPos);
4775                                if (DEBUG_SCOPE_BUILD) {
4776                                    System.out.println("[DEBUG]   Detected 3-part name (parsed): " + firstPart + "." + columnName + ".method");
4777                                }
4778                                handleMethodCallOnColumn(firstPart, columnName);
4779                            }
4780                            // If no second dot, the structure is ambiguous (table.method?) - skip it
4781                        } else {
4782                            // 2-part name: column.method()
4783                            if (DEBUG_SCOPE_BUILD) {
4784                                System.out.println("[DEBUG]   Detected 2-part name (parsed): " + firstPart + ".method");
4785                            }
4786                            handleUnqualifiedMethodCall(firstPart);
4787                        }
4788                    }
4789                }
4790            }
4791        }
4792    }
4793
4794    /**
4795     * Mark keyword arguments in a function call so they are not treated as column references.
4796     * Uses TBuiltFunctionUtil to check which argument positions contain keywords.
4797     */
4798    private void markFunctionKeywordArguments(TFunctionCall functionCall) {
4799        if (functionCall.getArgs() == null || functionCall.getArgs().size() == 0) {
4800            return;
4801        }
4802        if (functionCall.getFunctionName() == null) {
4803            return;
4804        }
4805
4806        String functionName = functionCall.getFunctionName().toString();
4807        Set<Integer> keywordPositions = TBuiltFunctionUtil.argumentsIncludeKeyword(dbVendor, functionName);
4808
4809        if (keywordPositions == null || keywordPositions.isEmpty()) {
4810            return;
4811        }
4812
4813        TExpressionList args = functionCall.getArgs();
4814        for (Integer pos : keywordPositions) {
4815            int index = pos - 1; // TBuiltFunctionUtil uses 1-based positions
4816            if (index >= 0 && index < args.size()) {
4817                TExpression argExpr = args.getExpression(index);
4818                // Mark the objectOperand if it's a simple object name or constant
4819                if (argExpr != null) {
4820                    TObjectName objectName = argExpr.getObjectOperand();
4821                    if (objectName != null) {
4822                        functionKeywordArguments.add(objectName);
4823                        if (DEBUG_SCOPE_BUILD) {
4824                            System.out.println("[DEBUG] Marked function keyword argument: " +
4825                                objectName.toString() + " at position " + pos +
4826                                " in function " + functionName);
4827                        }
4828                    }
4829                }
4830            }
4831        }
4832    }
4833
4834    /**
4835     * Handle a qualified method call on a column: table.column.method()
4836     * Creates a synthetic column reference and links it to the source table.
4837     *
4838     * @param tableAlias the table alias or name
4839     * @param columnName the column name
4840     */
4841    private void handleMethodCallOnColumn(String tableAlias, String columnName) {
4842        if (currentFromScope == null) {
4843            return;
4844        }
4845
4846        for (ScopeChild child : currentFromScope.getChildren()) {
4847            if (nameMatcher.matches(child.getAlias(), tableAlias)) {
4848                // Found matching table - create column reference
4849                createAndRegisterColumnReference(tableAlias, columnName, child);
4850                break;
4851            }
4852        }
4853    }
4854
4855    /**
4856     * Handle an unqualified method call on a column: column.method()
4857     * Attempts to infer the table when there's only one table in FROM,
4858     * or when the column name uniquely identifies the source table.
4859     *
4860     * @param columnName the column name
4861     */
4862    private void handleUnqualifiedMethodCall(String columnName) {
4863        if (currentFromScope == null) {
4864            return;
4865        }
4866
4867        List<ScopeChild> children = currentFromScope.getChildren();
4868
4869        // Case 1: Only one table in FROM - column must belong to it
4870        if (children.size() == 1) {
4871            ScopeChild child = children.get(0);
4872            createAndRegisterColumnReference(child.getAlias(), columnName, child);
4873            return;
4874        }
4875
4876        // Case 2: Multiple tables - try to find which table has this column
4877        // This requires metadata from TableNamespace or SQLEnv
4878        ScopeChild matchedChild = null;
4879        int matchCount = 0;
4880
4881        for (ScopeChild child : children) {
4882            INamespace ns = child.getNamespace();
4883            if (ns != null) {
4884                // Check if this namespace has the column
4885                ColumnLevel level = ns.hasColumn(columnName);
4886                if (level == ColumnLevel.EXISTS) {
4887                    matchedChild = child;
4888                    matchCount++;
4889                } else if (level == ColumnLevel.MAYBE && matchedChild == null) {
4890                    // MAYBE means the table has no metadata, so column might exist
4891                    // Only use as fallback if no definite match found
4892                    matchedChild = child;
4893                }
4894            }
4895        }
4896
4897        // If exactly one table definitely has the column, use it
4898        // If no definite match but one MAYBE, use that as fallback
4899        if (matchedChild != null && (matchCount <= 1)) {
4900            createAndRegisterColumnReference(matchedChild.getAlias(), columnName, matchedChild);
4901        }
4902    }
4903
4904    /**
4905     * Create and register a synthetic column reference for a method call on a column.
4906     *
4907     * @param tableAlias the table alias or name
4908     * @param columnName the column name
4909     * @param scopeChild the ScopeChild containing the namespace
4910     */
4911    private void createAndRegisterColumnReference(String tableAlias, String columnName, ScopeChild scopeChild) {
4912        // Create TObjectName with proper tokens for table.column
4913        // Use TObjectName.createObjectName() which properly initializes objectToken and partToken
4914        TSourceToken tableToken = new TSourceToken(tableAlias);
4915        TSourceToken columnToken = new TSourceToken(columnName);
4916        TObjectName columnRef = TObjectName.createObjectName(
4917            dbVendor,
4918            EDbObjectType.column,
4919            tableToken,
4920            columnToken
4921        );
4922
4923        // Get the TTable from the namespace
4924        INamespace ns = scopeChild.getNamespace();
4925        TTable sourceTable = null;
4926        if (ns instanceof TableNamespace) {
4927            sourceTable = ((TableNamespace) ns).getTable();
4928        } else if (ns instanceof CTENamespace) {
4929            sourceTable = ((CTENamespace) ns).getReferencingTable();
4930        } else if (ns != null) {
4931            // For other namespace types (SubqueryNamespace, etc.), try getFinalTable()
4932            sourceTable = ns.getFinalTable();
4933        }
4934
4935        if (sourceTable != null) {
4936            columnRef.setSourceTable(sourceTable);
4937        }
4938
4939        // Determine the scope for this column
4940        IScope columnScope = determineColumnScope(columnRef);
4941        if (columnScope != null) {
4942            columnToScopeMap.put(columnRef, columnScope);
4943        }
4944        allColumnReferences.add(columnRef);
4945
4946        if (DEBUG_SCOPE_BUILD) {
4947            System.out.println("[DEBUG] Created synthetic column reference for method call: " +
4948                    tableAlias + "." + columnName + " -> " +
4949                    (sourceTable != null ? sourceTable.getFullName() : "unknown"));
4950        }
4951    }
4952
4953    /**
4954     * Recursively traverse an expression to collect column references.
4955     * This is needed for expression types that may not be fully traversed by acceptChildren().
4956     */
4957    private void traverseExpressionForColumns(TExpression expr) {
4958        if (expr == null) return;
4959
4960        // Handle lambda expressions - push parameter names before traversing body
4961        boolean isLambda = (expr.getExpressionType() == EExpressionType.lambda_t);
4962        if (isLambda && expr.getLeftOperand() != null) {
4963            Set<String> paramNames = new HashSet<>();
4964            collectLambdaParameterNames(expr.getLeftOperand(), paramNames);
4965            lambdaParameterStack.push(paramNames);
4966            // Also collect the parameter objects
4967            collectLambdaParameterObjects(expr.getLeftOperand());
4968        }
4969
4970        // Check for column reference in objectOperand (common in array access, simple names)
4971        // Skip constants (e.g., DAY in DATEDIFF(DAY, ...) is parsed as simple_constant_t)
4972        if (expr.getObjectOperand() != null &&
4973            expr.getExpressionType() != EExpressionType.simple_constant_t) {
4974            if (DEBUG_SCOPE_BUILD) {
4975                System.out.println("[DEBUG] traverseExpressionForColumns: Found objectOperand=" +
4976                    expr.getObjectOperand().toString() + " in expr type=" + expr.getExpressionType());
4977            }
4978            // This will trigger preVisit(TObjectName) if not a table reference
4979            preVisit(expr.getObjectOperand());
4980        }
4981
4982        // Recursively traverse sub-expressions
4983        if (expr.getLeftOperand() != null) {
4984            traverseExpressionForColumns(expr.getLeftOperand());
4985        }
4986        if (expr.getRightOperand() != null) {
4987            traverseExpressionForColumns(expr.getRightOperand());
4988        }
4989
4990        // Pop lambda parameter context after traversing
4991        if (isLambda && expr.getLeftOperand() != null && !lambdaParameterStack.isEmpty()) {
4992            lambdaParameterStack.pop();
4993        }
4994
4995        // Traverse function call arguments
4996        if (expr.getFunctionCall() != null) {
4997            TFunctionCall func = expr.getFunctionCall();
4998            if (func.getArgs() != null) {
4999                for (int i = 0; i < func.getArgs().size(); i++) {
5000                    traverseExpressionForColumns(func.getArgs().getExpression(i));
5001                }
5002            }
5003            // Handle STRUCT field values recursively
5004            if (func.getFieldValues() != null) {
5005                for (int i = 0; i < func.getFieldValues().size(); i++) {
5006                    TResultColumn rc = func.getFieldValues().getResultColumn(i);
5007                    if (rc != null && rc.getExpr() != null) {
5008                        traverseExpressionForColumns(rc.getExpr());
5009                    }
5010                }
5011            }
5012            // Handle special function expressions (CAST, CONVERT, EXTRACT, etc.)
5013            // These functions store their arguments in expr1/expr2/expr3 instead of args
5014            if (func.getExpr1() != null) {
5015                traverseExpressionForColumns(func.getExpr1());
5016            }
5017            if (func.getExpr2() != null) {
5018                traverseExpressionForColumns(func.getExpr2());
5019            }
5020            if (func.getExpr3() != null) {
5021                traverseExpressionForColumns(func.getExpr3());
5022            }
5023            // Handle XML functions that store their arguments in special properties
5024            if (func.getXMLElementValueExprList() != null) {
5025                TResultColumnList xmlValueList = func.getXMLElementValueExprList();
5026                for (int j = 0; j < xmlValueList.size(); j++) {
5027                    TResultColumn rc = xmlValueList.getResultColumn(j);
5028                    if (rc != null && rc.getExpr() != null) {
5029                        traverseExpressionForColumns(rc.getExpr());
5030                    }
5031                }
5032            }
5033            if (func.getXMLForestValueList() != null) {
5034                TResultColumnList xmlForestList = func.getXMLForestValueList();
5035                for (int j = 0; j < xmlForestList.size(); j++) {
5036                    TResultColumn rc = xmlForestList.getResultColumn(j);
5037                    if (rc != null && rc.getExpr() != null) {
5038                        traverseExpressionForColumns(rc.getExpr());
5039                    }
5040                }
5041            }
5042            if (func.getXmlPassingClause() != null &&
5043                func.getXmlPassingClause().getPassingList() != null) {
5044                TResultColumnList passingList = func.getXmlPassingClause().getPassingList();
5045                for (int j = 0; j < passingList.size(); j++) {
5046                    TResultColumn rc = passingList.getResultColumn(j);
5047                    if (rc != null && rc.getExpr() != null) {
5048                        traverseExpressionForColumns(rc.getExpr());
5049                    }
5050                }
5051            }
5052            if (func.getXMLAttributesClause() != null &&
5053                func.getXMLAttributesClause().getValueExprList() != null) {
5054                TResultColumnList attrList = func.getXMLAttributesClause().getValueExprList();
5055                for (int j = 0; j < attrList.size(); j++) {
5056                    TResultColumn rc = attrList.getResultColumn(j);
5057                    if (rc != null && rc.getExpr() != null) {
5058                        traverseExpressionForColumns(rc.getExpr());
5059                    }
5060                }
5061            }
5062            // Handle XMLCAST typeExpression
5063            if (func.getTypeExpression() != null) {
5064                traverseExpressionForColumns(func.getTypeExpression());
5065            }
5066        }
5067
5068        // Traverse expression list (e.g., IN clause)
5069        if (expr.getExprList() != null) {
5070            for (int i = 0; i < expr.getExprList().size(); i++) {
5071                traverseExpressionForColumns(expr.getExprList().getExpression(i));
5072            }
5073        }
5074
5075        // Traverse subquery if present
5076        if (expr.getSubQuery() != null) {
5077            expr.getSubQuery().acceptChildren(this);
5078        }
5079
5080        // Traverse CASE expression
5081        if (expr.getExpressionType() == EExpressionType.case_t && expr.getCaseExpression() != null) {
5082            TCaseExpression caseExpr = expr.getCaseExpression();
5083
5084            // Traverse input expression (for simple CASE: CASE input_expr WHEN ...)
5085            if (caseExpr.getInput_expr() != null) {
5086                traverseExpressionForColumns(caseExpr.getInput_expr());
5087            }
5088
5089            // Traverse each WHEN...THEN clause
5090            if (caseExpr.getWhenClauseItemList() != null) {
5091                for (int i = 0; i < caseExpr.getWhenClauseItemList().size(); i++) {
5092                    TWhenClauseItem whenItem = caseExpr.getWhenClauseItemList().getWhenClauseItem(i);
5093                    if (whenItem != null) {
5094                        // Traverse WHEN condition
5095                        if (whenItem.getComparison_expr() != null) {
5096                            traverseExpressionForColumns(whenItem.getComparison_expr());
5097                        }
5098                        // Traverse PostgreSQL condition list
5099                        if (whenItem.getConditionList() != null) {
5100                            for (int j = 0; j < whenItem.getConditionList().size(); j++) {
5101                                traverseExpressionForColumns(whenItem.getConditionList().getExpression(j));
5102                            }
5103                        }
5104                        // Traverse THEN result
5105                        if (whenItem.getReturn_expr() != null) {
5106                            traverseExpressionForColumns(whenItem.getReturn_expr());
5107                        }
5108                    }
5109                }
5110            }
5111
5112            // Traverse ELSE expression
5113            if (caseExpr.getElse_expr() != null) {
5114                traverseExpressionForColumns(caseExpr.getElse_expr());
5115            }
5116        }
5117    }
5118
5119    // ========== Result Columns ==========
5120
5121    @Override
5122    public void preVisit(TResultColumn resultColumn) {
5123        // Mark that we're inside a result column context.
5124        // Lateral alias matching should ONLY apply inside result columns.
5125        inResultColumnContext = true;
5126
5127        // Track the current result column's alias to exclude from lateral alias matching.
5128        // A column reference inside the expression that DEFINES an alias cannot be
5129        // a reference TO that alias - it must be a reference to the source table column.
5130        if (resultColumn.getAliasClause() != null && resultColumn.getAliasClause().getAliasName() != null) {
5131            currentResultColumnAlias = normalizeAliasName(
5132                resultColumn.getAliasClause().getAliasName().toString()).toLowerCase();
5133        } else {
5134            currentResultColumnAlias = null;
5135        }
5136
5137        // Collect SQL Server proprietary column aliases (column_alias = expression)
5138        // These should not be treated as column references
5139        if (resultColumn.getExpr() != null &&
5140            resultColumn.getExpr().getExpressionType() == EExpressionType.sqlserver_proprietary_column_alias_t) {
5141            TExpression leftOperand = resultColumn.getExpr().getLeftOperand();
5142            if (leftOperand != null &&
5143                leftOperand.getExpressionType() == EExpressionType.simple_object_name_t &&
5144                leftOperand.getObjectOperand() != null) {
5145                sqlServerProprietaryAliases.add(leftOperand.getObjectOperand());
5146                if (DEBUG_SCOPE_BUILD) {
5147                    System.out.println("[DEBUG] Collected SQL Server proprietary alias: " +
5148                            leftOperand.getObjectOperand().toString());
5149                }
5150            }
5151        }
5152
5153        // Handle BigQuery EXCEPT columns: SELECT * EXCEPT (column1, column2)
5154        // EXCEPT columns are added to allColumnReferences for DDL verification and lineage tracking,
5155        // but WITHOUT scope mapping to avoid triggering auto-inference in inner namespaces.
5156        // Star column expansion in TSQLResolver2 handles EXCEPT filtering directly.
5157        TObjectNameList exceptColumns = resultColumn.getExceptColumnList();
5158        if (exceptColumns != null && exceptColumns.size() > 0) {
5159            // Get the star column's source table from the expression
5160            TTable starSourceTable = null;
5161            if (resultColumn.getExpr() != null &&
5162                resultColumn.getExpr().getExpressionType() == EExpressionType.simple_object_name_t &&
5163                resultColumn.getExpr().getObjectOperand() != null) {
5164                TObjectName starColumn = resultColumn.getExpr().getObjectOperand();
5165                String starStr = starColumn.toString();
5166                if (starStr != null && starStr.endsWith("*")) {
5167                    // Get the table qualifier (e.g., "COMMON" from "COMMON.*")
5168                    String tableQualifier = starColumn.getTableString();
5169                    if (tableQualifier != null && !tableQualifier.isEmpty()) {
5170                        // Find the table by alias in the current scope
5171                        starSourceTable = findTableByAliasInCurrentScope(tableQualifier);
5172                    }
5173                }
5174            }
5175
5176            for (int i = 0; i < exceptColumns.size(); i++) {
5177                TObjectName exceptCol = exceptColumns.getObjectName(i);
5178                if (exceptCol != null) {
5179                    if (starSourceTable != null) {
5180                        // Qualified star (e.g., COMMON.*): Link EXCEPT column directly to source table
5181                        exceptCol.setSourceTable(starSourceTable);
5182                    }
5183                    // IMPORTANT: Add to allColumnReferences for tracking, but do NOT add to
5184                    // columnToScopeMap - this prevents triggering auto-inference in inner namespaces
5185                    allColumnReferences.add(exceptCol);
5186                    if (DEBUG_SCOPE_BUILD) {
5187                        System.out.println("[DEBUG] EXCEPT column added for tracking (no scope mapping): " +
5188                            exceptCol.toString() + (starSourceTable != null ? " -> " + starSourceTable.getFullName() : ""));
5189                    }
5190                }
5191            }
5192        }
5193
5194        // Handle BigQuery REPLACE columns: SELECT * REPLACE (expr AS identifier)
5195        // REPLACE columns create new columns that replace existing ones in star expansion.
5196        // Link them directly to the star's source table.
5197        java.util.ArrayList<gudusoft.gsqlparser.nodes.TReplaceExprAsIdentifier> replaceColumns =
5198            resultColumn.getReplaceExprAsIdentifiers();
5199        if (replaceColumns != null && replaceColumns.size() > 0) {
5200            // Get the star column's source table (similar to EXCEPT handling)
5201            TTable starSourceTable = null;
5202            if (resultColumn.getExpr() != null &&
5203                resultColumn.getExpr().getExpressionType() == EExpressionType.simple_object_name_t &&
5204                resultColumn.getExpr().getObjectOperand() != null) {
5205                TObjectName starColumn = resultColumn.getExpr().getObjectOperand();
5206                String starStr = starColumn.toString();
5207                if (starStr != null && starStr.endsWith("*")) {
5208                    String tableQualifier = starColumn.getTableString();
5209                    if (tableQualifier != null && !tableQualifier.isEmpty()) {
5210                        starSourceTable = findTableByAliasInCurrentScope(tableQualifier);
5211                    }
5212                }
5213            }
5214
5215            for (int i = 0; i < replaceColumns.size(); i++) {
5216                gudusoft.gsqlparser.nodes.TReplaceExprAsIdentifier replaceCol = replaceColumns.get(i);
5217                if (replaceCol != null && replaceCol.getIdentifier() != null) {
5218                    TObjectName replaceId = replaceCol.getIdentifier();
5219                    if (starSourceTable != null) {
5220                        // Qualified star: Link REPLACE identifier to the star's source table
5221                        replaceId.setSourceTable(starSourceTable);
5222                        allColumnReferences.add(replaceId);
5223                        if (DEBUG_SCOPE_BUILD) {
5224                            System.out.println("[DEBUG] REPLACE column linked to star source table: " +
5225                                replaceId.toString() + " -> " + starSourceTable.getFullName());
5226                        }
5227                    } else {
5228                        // Unqualified star: Add with scope mapping for normal resolution
5229                        if (currentSelectScope != null) {
5230                            columnToScopeMap.put(replaceId, currentSelectScope);
5231                        }
5232                        allColumnReferences.add(replaceId);
5233                        if (DEBUG_SCOPE_BUILD) {
5234                            System.out.println("[DEBUG] REPLACE column added for resolution (unqualified star): " +
5235                                replaceId.toString());
5236                        }
5237                    }
5238                }
5239            }
5240        }
5241
5242        // Handle CTAS target columns (CREATE TABLE AS SELECT)
5243        // In CTAS context, result columns become target table column definitions.
5244        // These are registered as "definition columns" (not reference columns):
5245        // - Added to allColumnReferences (for output)
5246        // - Added to ctasTargetColumns/tupleAliasColumns (definition set - prevents re-resolution)
5247        // - NOT added to columnToScopeMap (prevents NameResolver from overwriting sourceTable)
5248        //
5249        // IMPORTANT: Only handle CTAS target columns for the main SELECT of CTAS,
5250        // not for result columns inside CTEs or subqueries within the CTAS.
5251        // When inside a CTE definition (cteDefinitionDepth > 0), skip CTAS handling because
5252        // those result columns define CTE output, not CTAS target table columns.
5253        // Also check currentSelectScope == ctasMainSelectScope to exclude subqueries.
5254        if (currentCTASTargetTable != null && cteDefinitionDepth == 0 && currentSelectScope == ctasMainSelectScope) {
5255            boolean ctasColumnHandled = false;
5256
5257            if (resultColumn.getAliasClause() != null) {
5258                TObjectNameList tupleColumns = resultColumn.getAliasClause().getColumns();
5259
5260                // Case 1: Tuple aliases (e.g., AS (b1, b2, b3) in SparkSQL/Hive)
5261                if (tupleColumns != null && tupleColumns.size() > 0) {
5262                    for (int i = 0; i < tupleColumns.size(); i++) {
5263                        TObjectName tupleCol = tupleColumns.getObjectName(i);
5264                        if (tupleCol != null) {
5265                            // Mark as tuple alias column to skip in preVisit(TObjectName)
5266                            tupleAliasColumns.add(tupleCol);
5267                            // Set the source table to the CTAS target table
5268                            tupleCol.setSourceTable(currentCTASTargetTable);
5269                            // Add to all column references so it appears in output
5270                            allColumnReferences.add(tupleCol);
5271                            if (DEBUG_SCOPE_BUILD) {
5272                                System.out.println("[DEBUG] Registered CTAS tuple alias column: " +
5273                                        tupleCol.toString() + " -> " + currentCTASTargetTable.getFullName());
5274                            }
5275                        }
5276                    }
5277                    ctasColumnHandled = true;
5278                }
5279                // Case 2: Standard alias (AS alias / Teradata NAMED alias)
5280                else {
5281                    TObjectName aliasName = resultColumn.getAliasClause().getAliasName();
5282                    if (aliasName != null) {
5283                        // For alias names, partToken may be null but startToken has the actual text
5284                        // We need to set partToken so getColumnNameOnly() works correctly in the formatter
5285                        // This is done on a clone to avoid modifying the original AST node
5286                        if (aliasName.getPartToken() == null && aliasName.getStartToken() != null) {
5287                            // Clone the aliasName to avoid modifying the original AST
5288                            TObjectName ctasCol = aliasName.clone();
5289                            ctasCol.setPartToken(ctasCol.getStartToken());
5290                            ctasCol.setSourceTable(currentCTASTargetTable);
5291                            ctasTargetColumns.add(ctasCol);
5292                            allColumnReferences.add(ctasCol);
5293                            if (DEBUG_SCOPE_BUILD) {
5294                                System.out.println("[DEBUG] Registered CTAS target column (standard alias clone): " +
5295                                        ctasCol.getColumnNameOnly() + " -> " + currentCTASTargetTable.getFullName());
5296                            }
5297                            ctasColumnHandled = true;
5298                        } else {
5299                            String colName = aliasName.getColumnNameOnly();
5300                            if (colName != null && !colName.isEmpty()) {
5301                                // Register aliasName as CTAS target column DEFINITION
5302                                aliasName.setSourceTable(currentCTASTargetTable);
5303                                ctasTargetColumns.add(aliasName);
5304                                allColumnReferences.add(aliasName);
5305                                if (DEBUG_SCOPE_BUILD) {
5306                                    System.out.println("[DEBUG] Registered CTAS target column (standard alias): " +
5307                                            colName + " -> " + currentCTASTargetTable.getFullName());
5308                                }
5309                                ctasColumnHandled = true;
5310                            }
5311                        }
5312                    }
5313                }
5314            }
5315
5316            // Case 3: No alias, simple column reference or star (e.g., SELECT a FROM s, SELECT * FROM s)
5317            // The target column inherits the source column name.
5318            // Use clone pattern (like JOIN...USING) to avoid polluting source column's sourceTable.
5319            if (!ctasColumnHandled && resultColumn.getExpr() != null &&
5320                resultColumn.getExpr().getExpressionType() == EExpressionType.simple_object_name_t &&
5321                resultColumn.getExpr().getObjectOperand() != null) {
5322                TObjectName sourceCol = resultColumn.getExpr().getObjectOperand();
5323                // Get column name - use getColumnNameOnly() first, fall back to toString()
5324                String colName = sourceCol.getColumnNameOnly();
5325                if (colName == null || colName.isEmpty()) {
5326                    colName = sourceCol.toString();
5327                }
5328                if (colName != null && !colName.isEmpty()) {
5329                    // Clone the source column to create a synthetic CTAS target column
5330                    // The clone refers to original start/end tokens for proper name extraction
5331                    // This includes star columns (SELECT * FROM s) which should create t.*
5332                    TObjectName ctasCol = sourceCol.clone();
5333                    ctasCol.setSourceTable(currentCTASTargetTable);
5334                    ctasTargetColumns.add(ctasCol);
5335                    allColumnReferences.add(ctasCol);
5336                    if (DEBUG_SCOPE_BUILD) {
5337                        System.out.println("[DEBUG] Registered CTAS target column (simple ref clone): " +
5338                                colName + " -> " + currentCTASTargetTable.getFullName());
5339                    }
5340                }
5341            }
5342        } else {
5343            // NOT in CTAS context - track result column alias names to skip them
5344            // These are NOT column references - they're alias names given to expressions.
5345            // This includes standard "AS alias" and Teradata "NAMED alias" syntax.
5346            if (resultColumn.getAliasClause() != null) {
5347                TObjectName aliasName = resultColumn.getAliasClause().getAliasName();
5348                if (aliasName != null) {
5349                    resultColumnAliasNames.add(aliasName);
5350                    if (DEBUG_SCOPE_BUILD) {
5351                        System.out.println("[DEBUG] Tracked result column alias name (will skip): " + aliasName.toString());
5352                    }
5353                }
5354            }
5355        }
5356
5357        // Handle UPDATE SET clause columns
5358        // When inside an UPDATE statement, the left-hand side of SET assignments
5359        // (e.g., SET col = expr) should be linked to the target table
5360        if (currentUpdateTargetTable != null && resultColumn.getExpr() != null) {
5361            TExpression expr = resultColumn.getExpr();
5362            // SET clause uses assignment_t for "column = value" assignments in Teradata
5363            // or simple_comparison_t in some other databases
5364            if ((expr.getExpressionType() == EExpressionType.assignment_t ||
5365                 expr.getExpressionType() == EExpressionType.simple_comparison_t) &&
5366                expr.getLeftOperand() != null &&
5367                expr.getLeftOperand().getExpressionType() == EExpressionType.simple_object_name_t &&
5368                expr.getLeftOperand().getObjectOperand() != null) {
5369                TObjectName leftColumn = expr.getLeftOperand().getObjectOperand();
5370                // Link the SET clause column to the UPDATE target table
5371                leftColumn.setSourceTable(currentUpdateTargetTable);
5372                allColumnReferences.add(leftColumn);
5373                // Mark as SET clause target - should NOT be re-resolved through star column
5374                setClauseTargetColumns.add(leftColumn);
5375                // Also add to columnToScopeMap with the current UpdateScope
5376                if (currentUpdateScope != null) {
5377                    columnToScopeMap.put(leftColumn, currentUpdateScope);
5378                }
5379                if (DEBUG_SCOPE_BUILD) {
5380                    System.out.println("[DEBUG] Linked UPDATE SET column: " +
5381                            leftColumn.toString() + " -> " + currentUpdateTargetTable.getFullName());
5382                }
5383            }
5384        }
5385
5386        // Explicitly traverse typecast expressions in result columns
5387        // The visitor pattern may not automatically traverse nested expressions in typecast
5388        // This is important for Snowflake stage file columns like "$1:apMac::string"
5389        if (resultColumn.getExpr() != null &&
5390            resultColumn.getExpr().getExpressionType() == EExpressionType.typecast_t) {
5391            TExpression typecastExpr = resultColumn.getExpr();
5392            if (typecastExpr.getLeftOperand() != null) {
5393                traverseExpressionForColumns(typecastExpr.getLeftOperand());
5394            }
5395        }
5396    }
5397
5398    @Override
5399    public void postVisit(TResultColumn resultColumn) {
5400        // Clear the current result column alias when we leave the result column
5401        currentResultColumnAlias = null;
5402        // Mark that we're leaving the result column context
5403        inResultColumnContext = false;
5404    }
5405
5406    // ========== Column References ==========
5407
5408    @Override
5409    public void preVisit(TObjectName objectName) {
5410        // Skip if this is a table name reference
5411        if (tableNameReferences.contains(objectName)) {
5412            return;
5413        }
5414
5415        // Skip if this is a tuple alias column (already handled in preVisit(TResultColumn))
5416        if (tupleAliasColumns.contains(objectName)) {
5417            return;
5418        }
5419
5420        // Skip if this is a CTAS target column (already handled in preVisit(TResultColumn))
5421        // These are definition columns for the CTAS target table, not column references
5422        if (ctasTargetColumns.contains(objectName)) {
5423            return;
5424        }
5425
5426        // Skip if this is a VALUES table alias column definition (already handled in processValuesTable)
5427        // These are column NAME definitions like 'id', 'name' in "VALUES (...) AS t(id, name)"
5428        if (valuesTableAliasColumns.contains(objectName)) {
5429            return;
5430        }
5431
5432        // Skip if this is a PIVOT IN clause column (already handled in addPivotInClauseColumns)
5433        // These are pivot column DEFINITIONS, not references to source table columns.
5434        if (pivotInClauseColumns.contains(objectName)) {
5435            return;
5436        }
5437
5438        // Skip if this is a result column alias name (tracked in preVisit(TResultColumn))
5439        // These are NOT column references - they're alias names for expressions
5440        // e.g., "COUNT(1) AS cnt" or Teradata "COUNT(1)(NAMED cnt)"
5441        if (resultColumnAliasNames.contains(objectName)) {
5442            return;
5443        }
5444
5445        // Skip if this is a lambda parameter
5446        // Lambda parameters are local function parameters, not table column references
5447        // e.g., in "transform(array, x -> x + 1)", x is a lambda parameter
5448        if (lambdaParameters.contains(objectName) || isLambdaParameter(objectName)) {
5449            return;
5450        }
5451
5452        // Skip if this is a DDL target object name (file format, stage, pipe, etc.)
5453        // These are object names in DDL statements, not column references
5454        if (ddlTargetNames.contains(objectName)) {
5455            if (DEBUG_SCOPE_BUILD) {
5456                System.out.println("[DEBUG] Skipping DDL target name: " + objectName);
5457            }
5458            return;
5459        }
5460        if (DEBUG_SCOPE_BUILD && !ddlTargetNames.isEmpty()) {
5461            System.out.println("[DEBUG] DDL target check for " + objectName +
5462                " (hashCode=" + System.identityHashCode(objectName) +
5463                "): ddlTargetNames has " + ddlTargetNames.size() + " entries");
5464            for (TObjectName t : ddlTargetNames) {
5465                System.out.println("[DEBUG]   DDL target: " + t + " (hashCode=" + System.identityHashCode(t) + ")");
5466            }
5467        }
5468
5469        // Skip if this is clearly not a column reference
5470        if (!isColumnReference(objectName)) {
5471            return;
5472        }
5473
5474        // Special handling: Link Snowflake stage positional columns to the stage table
5475        // These columns ($1, $2, $1:path) need to be linked to the stage table in the FROM clause
5476        if (dbVendor == EDbVendor.dbvsnowflake &&
5477            objectName.getSourceTable() == null &&
5478            isSnowflakeStagePositionalColumn(objectName)) {
5479            TTable stageTable = findSnowflakeStageTableInScope();
5480            if (DEBUG_SCOPE_BUILD) {
5481                System.out.println("[DEBUG] findSnowflakeStageTableInScope returned: " +
5482                    (stageTable != null ? stageTable.getTableName() : "null") +
5483                    " for column " + objectName);
5484            }
5485            if (stageTable != null) {
5486                objectName.setSourceTable(stageTable);
5487                if (DEBUG_SCOPE_BUILD) {
5488                    System.out.println("[DEBUG] Linked Snowflake stage column " + objectName +
5489                        " to stage table " + stageTable.getTableName());
5490                }
5491            }
5492        }
5493
5494        // Determine the scope for this column
5495        IScope scope = determineColumnScope(objectName);
5496
5497        if (DEBUG_SCOPE_BUILD) {
5498            System.out.println("[DEBUG] preVisit(TObjectName): " + objectName.toString() +
5499                " scope=" + (scope != null ? scope.getScopeType() : "null") +
5500                " currentSelectScope=" + (currentSelectScope != null ? "exists" : "null") +
5501                " currentUpdateScope=" + (currentUpdateScope != null ? "exists" : "null"));
5502        }
5503
5504        // Record the mapping
5505        if (scope != null) {
5506            columnToScopeMap.put(objectName, scope);
5507            allColumnReferences.add(objectName);
5508        }
5509    }
5510
5511    /**
5512     * Check if a TObjectName is a column reference (not a table/schema/etc.)
5513     */
5514    private boolean isColumnReference(TObjectName objectName) {
5515        if (objectName == null) {
5516            return false;
5517        }
5518
5519        // reuse result from TCustomsqlstatement.isValidColumnName(EDbVendor pDBVendor)
5520        if (objectName.getObjectType() == TObjectName.ttobjNotAObject) return false;
5521        if (objectName.getDbObjectType() == EDbObjectType.hint) return false;
5522
5523        // Skip if this is marked as a variable (e.g., DECLARE statement element name)
5524        if (objectName.getObjectType() == TObjectName.ttobjVariable) {
5525            return false;
5526        }
5527
5528        // Skip if it's already marked as a table reference
5529        if (tableNameReferences.contains(objectName)) {
5530            return false;
5531        }
5532
5533        // Skip if this is a variable declaration name (from DECLARE statement)
5534        if (variableDeclarationNames.contains(objectName)) {
5535            return false;
5536        }
5537
5538        // Skip if this is an UNPIVOT definition column (value or FOR column)
5539        // These are column DEFINITIONS that create new output columns, not references
5540        if (unpivotDefinitionColumns.contains(objectName)) {
5541            return false;
5542        }
5543
5544        // Skip if we're inside EXECUTE IMMEDIATE dynamic string expression
5545        // The variable name used for dynamic SQL is not a column reference
5546        if (insideExecuteImmediateDynamicExpr) {
5547            if (DEBUG_SCOPE_BUILD) {
5548                System.out.println("[DEBUG] Skipping identifier inside EXECUTE IMMEDIATE: " + objectName.toString());
5549            }
5550            return false;
5551        }
5552
5553        // Skip if column name is empty or null - this indicates a table alias or similar
5554        String columnName = objectName.getColumnNameOnly();
5555        if (columnName == null || columnName.isEmpty()) {
5556            return false;
5557        }
5558
5559        // Check object type
5560        // Column references typically have objectType that indicates column usage
5561        // or appear in contexts where columns are expected
5562
5563        // Skip if this is part of a CREATE TABLE column definition
5564        // Note: getParentObjectName() returns TObjectName (for qualified names), not the AST parent
5565        TParseTreeNode parent = objectName.getParentObjectName();
5566        if (parent instanceof TColumnDefinition) {
5567            return false;
5568        }
5569
5570        // Skip if this looks like a table name in FROM clause
5571        if (parent instanceof TTable) {
5572            return false;
5573        }
5574
5575        // Skip if this is a table alias (parent is TAliasClause)
5576        if (parent instanceof gudusoft.gsqlparser.nodes.TAliasClause) {
5577            return false;
5578        }
5579
5580        // Skip if this is a cursor name in cursor-related statements (DECLARE CURSOR, OPEN, FETCH, CLOSE, DEALLOCATE)
5581        if (isCursorName(objectName)) {
5582            return false;
5583        }
5584
5585        // Skip if this is a datepart keyword in a date function (e.g., DAY in DATEDIFF)
5586        // These have null parent but are known date/time keywords used in function arguments
5587        // We need to verify it's actually in a date function context by checking surrounding tokens
5588        if (parent == null && objectName.getTableToken() == null &&
5589            isSqlServerDatepartKeyword(columnName) &&
5590            isInDateFunctionContext(objectName)) {
5591            return false;
5592        }
5593
5594        // Skip if this was pre-marked as a function keyword argument in preVisit(TFunctionCall)
5595        // This handles keywords like SECOND in TIMESTAMP_DIFF(ts1, ts2, SECOND) for BigQuery/Snowflake
5596        if (functionKeywordArguments.contains(objectName)) {
5597            if (DEBUG_SCOPE_BUILD) {
5598                System.out.println("[DEBUG] Skipping pre-marked function keyword: " + objectName.toString());
5599            }
5600            return false;
5601        }
5602
5603        // Skip if this is a keyword argument in a built-in function (e.g., DAY in DATEDIFF)
5604        // This is a fallback using parent traversal (may not work if parent is null)
5605        if (isFunctionKeywordArgument(objectName)) {
5606            return false;
5607        }
5608
5609        // Skip if this is a variable or function parameter (e.g., p_date in CREATE FUNCTION)
5610        // EXCEPTION: In PL/SQL blocks, we need to collect variable references so TSQLResolver2
5611        // can distinguish between table columns and block variables during name resolution
5612        if (objectName.getLinkedVariable() != null) {
5613            if (currentPlsqlBlockScope == null) {  // Not in PL/SQL block
5614                return false;
5615            }
5616        }
5617        EDbObjectType dbObjType = objectName.getDbObjectType();
5618        if (dbObjType == EDbObjectType.variable || dbObjType == EDbObjectType.parameter) {
5619            // Special case: Snowflake stage file positional columns ($1, $2, $1:path, etc.)
5620            // These are parsed as "parameter" but are actually column references to stage files
5621            boolean isStagePositional = (dbVendor == EDbVendor.dbvsnowflake && isSnowflakeStagePositionalColumn(objectName));
5622            if (DEBUG_SCOPE_BUILD) {
5623                System.out.println("[DEBUG] dbObjType check: dbObjType=" + dbObjType +
5624                    " colNameOnly=" + objectName.getColumnNameOnly() +
5625                    " objStr=" + objectName.getObjectString() +
5626                    " isSnowflakeStagePositional=" + isStagePositional);
5627            }
5628            if (isStagePositional) {
5629                // Allow collection - will be linked to stage table during name resolution
5630                // Fall through to return true
5631            } else if (currentPlsqlBlockScope == null) {  // Not in PL/SQL block
5632                return false;
5633            } else {
5634                return false;
5635                // In PL/SQL block - allow collection so name resolution can distinguish
5636                // between table columns and block variables
5637            }
5638        }
5639
5640        // Skip if this name matches a registered parameter in the current PL/SQL block scope
5641        // BUT ONLY if we're NOT in a DML statement context (SELECT/INSERT/UPDATE/DELETE/MERGE)
5642        // This handles cases where the parser doesn't link the reference to the parameter declaration
5643        // (e.g., EXECUTE IMMEDIATE ddl_in where ddl_in is a procedure parameter)
5644        //
5645        // When inside a DML statement, we MUST allow collection so name resolution can distinguish
5646        // between table columns and block variables (e.g., "DELETE FROM emp WHERE ename = main.ename")
5647        if (currentPlsqlBlockScope != null) {
5648            // Check if we're inside any DML statement context
5649            // Note: currentInsertTargetTable is set when inside an INSERT statement
5650            boolean inDmlContext = (currentSelectScope != null || currentUpdateScope != null ||
5651                                    currentDeleteScope != null || currentMergeScope != null ||
5652                                    currentInsertTargetTable != null);
5653
5654            if (!inDmlContext) {
5655                // Not in DML context - this is likely a standalone expression like EXECUTE IMMEDIATE param
5656                // or a WHILE condition, etc.
5657                // Check if the name is a variable in the current scope OR any parent PL/SQL block scope.
5658                String nameOnly = objectName.getColumnNameOnly();
5659                if (nameOnly != null && isVariableInPlsqlScopeChain(nameOnly)) {
5660                    if (DEBUG_SCOPE_BUILD) {
5661                        System.out.println("[DEBUG] Skipping registered PL/SQL parameter/variable (not in DML): " + nameOnly);
5662                    }
5663                    return false;
5664                }
5665
5666                // Also skip qualified references (like TEMP1.M1) when not in DML context
5667                // BUT only if the prefix is NOT a trigger correlation variable (:new, :old, new, old)
5668                // These are likely PL/SQL object/record field accesses, not table columns
5669                if (objectName.getTableToken() != null) {
5670                    String prefix = objectName.getTableToken().toString().toLowerCase();
5671                    // Skip filtering for trigger correlation variables
5672                    if (!":new".equals(prefix) && !":old".equals(prefix) &&
5673                        !"new".equals(prefix) && !"old".equals(prefix)) {
5674                        if (DEBUG_SCOPE_BUILD) {
5675                            System.out.println("[DEBUG] Skipping qualified reference in non-DML PL/SQL context: " + objectName.toString());
5676                        }
5677                        return false;
5678                    }
5679                }
5680            }
5681        }
5682
5683        // Skip if this is a bind variable (e.g., :project_id in Oracle, @param in SQL Server)
5684        // BUT skip this check for Snowflake stage positional columns with JSON paths like $1:apMac
5685        // which are tokenized as bind variables due to the colon syntax
5686        if (objectName.getPartToken() != null && objectName.getPartToken().tokentype == ETokenType.ttbindvar) {
5687            // Check if this is a Snowflake stage JSON path column - these are NOT bind variables
5688            if (!(dbVendor == EDbVendor.dbvsnowflake && isSnowflakeStagePositionalColumn(objectName))) {
5689                if (DEBUG_SCOPE_BUILD) {
5690                    System.out.println("[DEBUG] partToken bindvar check: " + objectName.toString() +
5691                        " tokentype=" + objectName.getPartToken().tokentype);
5692                }
5693                return false;
5694            }
5695        }
5696        // Also check by column name pattern for bind variables
5697        // BUT skip this check for Snowflake stage positional columns with JSON paths like $1:apMac
5698        // where getColumnNameOnly() returns ":apMac" - this is NOT a bind variable
5699        String colName = objectName.getColumnNameOnly();
5700        if (colName != null && (colName.startsWith(":") || colName.startsWith("@"))) {
5701            // Check if this is a Snowflake stage JSON path column (e.g., $1:apMac)
5702            // In this case, colName is ":apMac" but it's a JSON path, not a bind variable
5703            boolean isStageCol = (dbVendor == EDbVendor.dbvsnowflake && isSnowflakeStagePositionalColumn(objectName));
5704            if (DEBUG_SCOPE_BUILD) {
5705                System.out.println("[DEBUG] bind variable check: colName=" + colName +
5706                    " objStr=" + objectName.getObjectString() +
5707                    " isSnowflakeStageCol=" + isStageCol);
5708            }
5709            if (!isStageCol) {
5710                return false;
5711            }
5712        }
5713
5714        // Skip built-in functions without parentheses (niladic functions)
5715        // These look like column references but are actually function calls
5716        // Examples: CURRENT_USER (SQL Server), CURRENT_DATETIME (BigQuery), etc.
5717        // Only check unqualified names - qualified names like "table.column" are real column references
5718        if (objectName.getTableToken() == null && isBuiltInFunctionName(colName)) {
5719            return false;
5720        }
5721
5722        // Skip Snowflake procedure system variables (e.g., sqlrowcount, sqlerrm, sqlstate)
5723        // These are special variables available in Snowflake stored procedures
5724        if (dbVendor == EDbVendor.dbvsnowflake && objectName.getTableToken() == null &&
5725            isSnowflakeProcedureSystemVariable(colName)) {
5726            if (DEBUG_SCOPE_BUILD) {
5727                System.out.println("[DEBUG] Skipping Snowflake procedure system variable: " + colName);
5728            }
5729            return false;
5730        }
5731
5732        // Skip if this is a double-quoted string literal in MySQL
5733        // In MySQL, "Z" is a string literal by default (unless ANSI_QUOTES mode)
5734        if (dbVendor == EDbVendor.dbvmysql && objectName.getPartToken() != null) {
5735            if (objectName.getPartToken().tokentype == ETokenType.ttdqstring) {
5736                return false;
5737            }
5738        }
5739
5740        // Skip if this is the alias part of SQL Server's proprietary column alias syntax
5741        // e.g., in "column_alias = expression", column_alias is an alias, not a column reference
5742        if (isSqlServerProprietaryColumnAlias(objectName, parent)) {
5743            return false;
5744        }
5745
5746        // Skip if this is a lateral column alias reference (Snowflake, BigQuery, etc.)
5747        // e.g., in "SELECT col as x, x + 1 as y", x is a lateral alias reference, not a source column
5748        // Only check unqualified names (no table prefix) - qualified names like "t.x" are not lateral aliases
5749        if (objectName.getTableToken() == null && isLateralColumnAlias(columnName)) {
5750            return false;
5751        }
5752
5753        // Handle PL/SQL package constants (e.g., sch.pk_constv2.c_cdsl in VALUES clause)
5754        // These are not resolvable as table columns, but we still collect them so they can
5755        // be output as "missed." columns when linkOrphanColumnToFirstTable=false, or linked
5756        // to the first physical table when linkOrphanColumnToFirstTable=true.
5757        // Note: The qualified prefix (schema.table) is preserved in the TObjectName tokens
5758        // (schemaToken, tableToken) for DataFlowAnalyzer to use when creating relationships.
5759        if (isPlsqlPackageConstant(objectName, parent)) {
5760            // Clear sourceTable since this is not a real table column
5761            // Don't set dbObjectType to variable so that populateOrphanColumns() in TSQLResolver2
5762            // will process it and set ownStmt, enabling linkOrphanColumnToFirstTable to work
5763            objectName.setSourceTable(null);
5764            if (DEBUG_SCOPE_BUILD) {
5765                System.out.println("[DEBUG] Collected PL/SQL package constant as orphan: " + objectName.toString());
5766            }
5767            // Return true to collect as orphan column
5768            return true;
5769        }
5770
5771        // Oracle PL/SQL special identifiers (pseudo-columns, implicit vars, exception handlers, cursor attrs)
5772        // Best practice: filter in ScopeBuilder (early), with strict gating:
5773        // - Oracle vendor only
5774        // - Quoted identifiers override keywords (e.g., "ROWNUM" is a user identifier)
5775        if (dbVendor == EDbVendor.dbvoracle && objectName.getQuoteType() == EQuoteType.notQuoted) {
5776            String nameOnly = objectName.getColumnNameOnly();
5777
5778            // 1) ROWNUM pseudo-column: completely exclude as requested
5779            if (nameOnly != null && "ROWNUM".equalsIgnoreCase(nameOnly)) {
5780                markNotAColumn(objectName, EDbObjectType.constant);
5781                return false;
5782            }
5783
5784            // 2) PL/SQL inquiry directives: $$PLSQL_UNIT, $$PLSQL_LINE, $$PLSQL_UNIT_OWNER, etc.
5785            // These are compile-time constants, not column references
5786            if (nameOnly != null && nameOnly.startsWith("$$")) {
5787                markNotAColumn(objectName, EDbObjectType.constant);
5788                return false;
5789            }
5790
5791            // 3) Inside PL/SQL blocks: filter implicit identifiers & cursor attributes
5792            if (currentPlsqlBlockScope != null) {
5793                if (nameOnly != null) {
5794                    String lower = nameOnly.toLowerCase(Locale.ROOT);
5795                    // SQLCODE (implicit variable) / SQLERRM (built-in; often written like a variable)
5796                    if ("sqlcode".equals(lower) || "sqlerrm".equals(lower)) {
5797                        markNotAColumn(objectName, EDbObjectType.variable);
5798                        return false;
5799                    }
5800                }
5801
5802                // Cursor attributes: SQL%NOTFOUND, cursor%ROWCOUNT, etc.
5803                if (isOraclePlsqlCursorAttribute(objectName)) {
5804                    markNotAColumn(objectName, EDbObjectType.variable);
5805                    return false;
5806                }
5807
5808                // Boolean literals: TRUE, FALSE (case-insensitive)
5809                // These are PL/SQL boolean constants, not column references
5810                if (nameOnly != null) {
5811                    String lower = nameOnly.toLowerCase(Locale.ROOT);
5812                    if ("true".equals(lower) || "false".equals(lower)) {
5813                        markNotAColumn(objectName, EDbObjectType.constant);
5814                        return false;
5815                    }
5816                }
5817
5818                // Oracle predefined exceptions (unqualified names in RAISE/WHEN clauses)
5819                // Examples: NO_DATA_FOUND, TOO_MANY_ROWS, CONFIGURATION_MISMATCH, etc.
5820                if (nameOnly != null && isOraclePredefinedException(nameOnly)) {
5821                    markNotAColumn(objectName, EDbObjectType.variable);
5822                    return false;
5823                }
5824
5825                // Collection methods: .COUNT, .LAST, .FIRST, .DELETE, .EXISTS, .PRIOR, .NEXT, .TRIM, .EXTEND
5826                // These appear as qualified names like "collection.COUNT" where COUNT is the method
5827                if (isOraclePlsqlCollectionMethod(objectName)) {
5828                    markNotAColumn(objectName, EDbObjectType.variable);
5829                    return false;
5830                }
5831
5832                // Cursor variable references: check if this is a known cursor variable
5833                // Example: emp in "OPEN emp FOR SELECT * FROM employees"
5834                // This is based on actual TCursorDeclStmt/TOpenforStmt declarations tracked in scope
5835                if (nameOnly != null && cursorVariableNames.contains(nameOnly.toLowerCase(Locale.ROOT))) {
5836                    markNotAColumn(objectName, EDbObjectType.variable);
5837                    if (DEBUG_SCOPE_BUILD) {
5838                        System.out.println("[DEBUG] Skipping cursor variable: " + nameOnly);
5839                    }
5840                    return false;
5841                }
5842
5843                // Record variable fields: when "table.column" refers to a record variable field
5844                // Example: rec.field_name where rec is declared as "rec record_type%ROWTYPE"
5845                // Check if the "table" part is a known variable in the current scope
5846                if (objectName.getTableToken() != null) {
5847                    String tablePrefix = objectName.getTableToken().toString();
5848                    if (tablePrefix != null && isVariableInPlsqlScopeChain(tablePrefix)) {
5849                        // The "table" part is actually a record variable, so this is a record field access
5850                        markNotAColumn(objectName, EDbObjectType.variable);
5851                        return false;
5852                    }
5853                    // Also check cursor FOR loop record names (e.g., "rec" in "for rec in (SELECT ...)")
5854                    if (tablePrefix != null && cursorForLoopRecordNames.contains(tablePrefix.toLowerCase(Locale.ROOT))) {
5855                        // The "table" part is a cursor FOR loop record, so this is a record field access
5856                        markNotAColumn(objectName, EDbObjectType.variable);
5857                        if (DEBUG_SCOPE_BUILD) {
5858                            System.out.println("[DEBUG] Skipping cursor FOR loop record field: " + objectName.toString());
5859                        }
5860                        return false;
5861                    }
5862
5863                    // Package member references (pkg.member or schema.pkg.member)
5864                    // Check if the table prefix matches a known package
5865                    if (tablePrefix != null && packageRegistry != null && packageRegistry.isPackage(tablePrefix)) {
5866                        OraclePackageNamespace pkgNs = packageRegistry.getPackage(tablePrefix);
5867                        String memberName = objectName.getColumnNameOnly();
5868                        if (pkgNs != null && memberName != null && pkgNs.hasMember(memberName)) {
5869                            markNotAColumn(objectName, EDbObjectType.variable);
5870                            if (DEBUG_SCOPE_BUILD) {
5871                                System.out.println("[DEBUG] Skipping package member reference: " +
5872                                    tablePrefix + "." + memberName);
5873                            }
5874                            return false;
5875                        }
5876                    }
5877                }
5878
5879                // Package-level variable (unqualified) when inside package body
5880                // Check if this is a known package member without qualification
5881                if (currentPackageScope != null && objectName.getTableToken() == null) {
5882                    OraclePackageNamespace pkgNs = currentPackageScope.getPackageNamespace();
5883                    if (pkgNs != null && nameOnly != null && pkgNs.hasMember(nameOnly)) {
5884                        markNotAColumn(objectName, EDbObjectType.variable);
5885                        if (DEBUG_SCOPE_BUILD) {
5886                            System.out.println("[DEBUG] Skipping unqualified package member: " + nameOnly);
5887                        }
5888                        return false;
5889                    }
5890                }
5891            }
5892        }
5893
5894        // Accept if it appears in an expression context
5895        if (parent instanceof TExpression) {
5896            return true;
5897        }
5898
5899        // Accept if it appears in a result column
5900        if (parent instanceof TResultColumn) {
5901            return true;
5902        }
5903
5904        // Default: accept as column reference
5905        return true;
5906    }
5907
5908    /**
5909     * Mark a TObjectName as "not a column" so downstream collectors/resolvers can skip it.
5910     */
5911    private void markNotAColumn(TObjectName objectName, EDbObjectType objType) {
5912        if (objectName == null) return;
5913        objectName.setValidate_column_status(TBaseType.MARKED_NOT_A_COLUMN_IN_COLUMN_RESOLVER);
5914        // IMPORTANT: setSourceTable(null) may set dbObjectType to column; override after
5915        objectName.setSourceTable(null);
5916        if (objType != null) {
5917            objectName.setDbObjectTypeDirectly(objType);
5918        }
5919    }
5920
5921    /**
5922     * Oracle PL/SQL cursor attributes are syntactically identified by '%' (e.g., SQL%NOTFOUND, cur%ROWCOUNT).
5923     * These are never table columns.
5924     */
5925    private boolean isOraclePlsqlCursorAttribute(TObjectName objectName) {
5926        if (dbVendor != EDbVendor.dbvoracle) return false;
5927        if (currentPlsqlBlockScope == null) return false;
5928        if (objectName == null) return false;
5929
5930        String text = objectName.toString();
5931        if (text == null) return false;
5932        int idx = text.lastIndexOf('%');
5933        if (idx < 0 || idx == text.length() - 1) return false;
5934
5935        String attr = text.substring(idx + 1).trim().toUpperCase(Locale.ROOT);
5936        // Common PL/SQL cursor attributes
5937        switch (attr) {
5938            case "FOUND":
5939            case "NOTFOUND":
5940            case "ROWCOUNT":
5941            case "ISOPEN":
5942            case "BULK_ROWCOUNT":
5943            case "BULK_EXCEPTIONS":
5944                return true;
5945            default:
5946                return false;
5947        }
5948    }
5949
5950    /**
5951     * Oracle predefined exceptions that should not be treated as column references.
5952     * These include standard PL/SQL exceptions and DBMS_* package exceptions.
5953     */
5954    private static final java.util.Set<String> ORACLE_PREDEFINED_EXCEPTIONS = new java.util.HashSet<>(java.util.Arrays.asList(
5955        // Standard PL/SQL exceptions
5956        "ACCESS_INTO_NULL", "CASE_NOT_FOUND", "COLLECTION_IS_NULL", "CURSOR_ALREADY_OPEN",
5957        "DUP_VAL_ON_INDEX", "INVALID_CURSOR", "INVALID_NUMBER", "LOGIN_DENIED",
5958        "NO_DATA_FOUND", "NO_DATA_NEEDED", "NOT_LOGGED_ON", "PROGRAM_ERROR",
5959        "ROWTYPE_MISMATCH", "SELF_IS_NULL", "STORAGE_ERROR", "SUBSCRIPT_BEYOND_COUNT",
5960        "SUBSCRIPT_OUTSIDE_LIMIT", "SYS_INVALID_ROWID", "TIMEOUT_ON_RESOURCE",
5961        "TOO_MANY_ROWS", "VALUE_ERROR", "ZERO_DIVIDE",
5962        // DBMS_STANDARD exceptions
5963        "CONFIGURATION_MISMATCH", "OTHERS"
5964    ));
5965
5966    private boolean isOraclePredefinedException(String name) {
5967        if (name == null) return false;
5968        return ORACLE_PREDEFINED_EXCEPTIONS.contains(name.toUpperCase(Locale.ROOT));
5969    }
5970
5971    /**
5972     * Oracle PL/SQL collection methods that should not be treated as column references.
5973     * Examples: my_collection.COUNT, my_array.LAST, my_table.DELETE
5974     */
5975    private boolean isOraclePlsqlCollectionMethod(TObjectName objectName) {
5976        if (dbVendor != EDbVendor.dbvoracle) return false;
5977        if (currentPlsqlBlockScope == null) return false;
5978        if (objectName == null) return false;
5979
5980        // Check if this is a qualified name (has a table/object prefix)
5981        // Collection methods appear as "collection_name.METHOD"
5982        if (objectName.getTableToken() == null) return false;
5983
5984        String methodName = objectName.getColumnNameOnly();
5985        if (methodName == null) return false;
5986
5987        String upper = methodName.toUpperCase(Locale.ROOT);
5988        switch (upper) {
5989            case "COUNT":
5990            case "FIRST":
5991            case "LAST":
5992            case "NEXT":
5993            case "PRIOR":
5994            case "EXISTS":
5995            case "DELETE":
5996            case "TRIM":
5997            case "EXTEND":
5998                return true;
5999            default:
6000                return false;
6001        }
6002    }
6003
6004    /**
6005     * Detect whether this TObjectName belongs to an Oracle exception handler condition.
6006     * Example: EXCEPTION WHEN no_data_found THEN ... / WHEN OTHERS THEN ...
6007     *
6008     * We use parent-chain context rather than a keyword list to avoid false positives.
6009     */
6010    private boolean isInsideOracleExceptionHandler(TObjectName objectName) {
6011        if (dbVendor != EDbVendor.dbvoracle) return false;
6012        if (currentPlsqlBlockScope == null) return false;
6013        if (objectName == null) return false;
6014
6015        TParseTreeNode node = objectName.getParentObjectName();
6016        while (node != null) {
6017            if (node instanceof TExceptionHandler) {
6018                return true;
6019            }
6020            node = node.getParentObjectName();
6021        }
6022        return false;
6023    }
6024
6025    /**
6026     * Check if a TObjectName is a keyword argument in a built-in function.
6027     * For example, DAY in DATEDIFF(DAY, start_date, end_date) is a keyword, not a column.
6028     * Uses TBuiltFunctionUtil to check against the configured keyword argument positions.
6029     */
6030    private boolean isFunctionKeywordArgument(TObjectName objectName) {
6031        // Traverse up to find the containing expression
6032        TParseTreeNode parent = objectName.getParentObjectName();
6033
6034        // Debug output for function keyword detection
6035        if (DEBUG_SCOPE_BUILD) {
6036            System.out.println("[DEBUG] isFunctionKeywordArgument: objectName=" + objectName.toString() +
6037                " parent=" + (parent != null ? parent.getClass().getSimpleName() : "null"));
6038        }
6039
6040        if (!(parent instanceof TExpression)) {
6041            return false;
6042        }
6043
6044        TExpression expr = (TExpression) parent;
6045
6046        // Check if the expression type indicates this could be a keyword argument
6047        // Keywords like DAY, MONTH, YEAR in DATEDIFF/DATEADD are parsed as simple_constant_t
6048        // but still have objectOperand set, so we need to check both types
6049        EExpressionType exprType = expr.getExpressionType();
6050        if (exprType != EExpressionType.simple_object_name_t &&
6051            exprType != EExpressionType.simple_constant_t) {
6052            return false;
6053        }
6054
6055        // Traverse up to find the containing function call
6056        TParseTreeNode exprParent = expr.getParentObjectName();
6057
6058        // The expression might be inside a TExpressionList (function args)
6059        if (exprParent instanceof TExpressionList) {
6060            TExpressionList argList = (TExpressionList) exprParent;
6061            TParseTreeNode argListParent = argList.getParentObjectName();
6062
6063            if (argListParent instanceof TFunctionCall) {
6064                TFunctionCall functionCall = (TFunctionCall) argListParent;
6065                return checkFunctionKeywordPosition(functionCall, argList, expr, objectName);
6066            }
6067        }
6068
6069        // Direct parent might be TFunctionCall in some cases
6070        if (exprParent instanceof TFunctionCall) {
6071            TFunctionCall functionCall = (TFunctionCall) exprParent;
6072            TExpressionList args = functionCall.getArgs();
6073            if (args != null) {
6074                return checkFunctionKeywordPosition(functionCall, args, expr, objectName);
6075            }
6076        }
6077
6078        return false;
6079    }
6080
6081    /**
6082     * Check if the expression is at a keyword argument position in the function call.
6083     */
6084    private boolean checkFunctionKeywordPosition(TFunctionCall functionCall,
6085                                                  TExpressionList argList,
6086                                                  TExpression expr,
6087                                                  TObjectName objectName) {
6088        // Find the position of this expression in the argument list
6089        int position = -1;
6090        for (int i = 0; i < argList.size(); i++) {
6091            if (argList.getExpression(i) == expr) {
6092                position = i + 1; // TBuiltFunctionUtil uses 1-based positions
6093                break;
6094            }
6095        }
6096
6097        if (position < 0) {
6098            return false;
6099        }
6100
6101        // Get function name
6102        String functionName = functionCall.getFunctionName() != null
6103                ? functionCall.getFunctionName().toString()
6104                : null;
6105        if (functionName == null || functionName.isEmpty()) {
6106            return false;
6107        }
6108
6109        // Check against the configured keyword argument positions
6110        // Use the ScopeBuilder's dbVendor since TFunctionCall.dbvendor may not be set
6111        Set<Integer> keywordPositions = TBuiltFunctionUtil.argumentsIncludeKeyword(
6112                dbVendor, functionName);
6113
6114        if (keywordPositions != null && keywordPositions.contains(position)) {
6115            if (DEBUG_SCOPE_BUILD) {
6116                System.out.println("[DEBUG] Skipping function keyword argument: " +
6117                        objectName.toString() + " at position " + position +
6118                        " in function " + functionName);
6119            }
6120            return true;
6121        }
6122
6123        return false;
6124    }
6125
6126    /**
6127     * Check if a TObjectName is a PL/SQL package constant (not a table column).
6128     *
6129     * This filter applies ONLY when all of the following are true:
6130     * - Vendor is Oracle
6131     * - We are inside a PL/SQL block (currentPlsqlBlockScope != null)
6132     * - The TObjectName is multi-part: schema.object.part (e.g., sch.pk_constv2.c_cdsl)
6133     * - The TObjectName is used in an expression/value context (not DDL/definition context)
6134     *
6135     * Decision rule (no naming heuristics):
6136     * - Try to resolve the prefix (schema.object) as a table/alias/CTE in the current scope
6137     * - If neither resolves -> it's a package constant, NOT a column reference
6138     * - If either resolves -> it's a real column reference
6139     *
6140     * @param objectName The TObjectName to check
6141     * @param parent The parent node (may be null for some TObjectName nodes)
6142     * @return true if this is a PL/SQL package constant (should NOT be collected as column)
6143     */
6144    private boolean isPlsqlPackageConstant(TObjectName objectName, TParseTreeNode parent) {
6145        // Gating condition 1: Oracle vendor only
6146        if (dbVendor != EDbVendor.dbvoracle) {
6147            return false;
6148        }
6149
6150        // Gating condition 2: Must be inside a PL/SQL block
6151        if (currentPlsqlBlockScope == null) {
6152            return false;
6153        }
6154
6155        // Gating condition 3: Must be multi-part name (schema.object.part)
6156        // e.g., sch.pk_constv2.c_cdsl where:
6157        //   - schemaToken = "sch"
6158        //   - tableToken = "pk_constv2"
6159        //   - partToken = "c_cdsl"
6160        if (objectName.getSchemaToken() == null ||
6161            objectName.getTableToken() == null ||
6162            objectName.getPartToken() == null) {
6163            return false;
6164        }
6165
6166        // Gating condition 4: Should not be in DDL definition context
6167        // Skip if parent indicates a DDL context (these are already filtered earlier,
6168        // but check here for safety). For VALUES clause expressions, parent is often null.
6169        if (parent instanceof TColumnDefinition || parent instanceof TTable) {
6170            return false;
6171        }
6172
6173        // Now check if the prefix is resolvable as a table/alias in the current scope
6174        IScope scope = determineColumnScope(objectName);
6175        if (scope == null) {
6176            // Conservative: if we can't determine scope, don't filter
6177            return false;
6178        }
6179
6180        String schemaName = objectName.getSchemaString();   // "sch"
6181        String qualifier = objectName.getTableString();      // "pk_constv2"
6182
6183        // Try to resolve as table alias or table name
6184        INamespace ns1 = scope.resolveTable(qualifier);
6185
6186        // Try to resolve as schema-qualified table name
6187        INamespace ns2 = null;
6188        if (schemaName != null && !schemaName.isEmpty()) {
6189            ns2 = scope.resolveTable(schemaName + "." + qualifier);
6190        }
6191
6192        if (ns1 == null && ns2 == null) {
6193            // Not resolvable as a table/alias in this scope
6194            // -> Treat as package constant, NOT a column reference
6195            if (DEBUG_SCOPE_BUILD) {
6196                System.out.println("[DEBUG] Filtered PL/SQL package constant: " +
6197                    objectName.toString() + " (prefix '" + schemaName + "." + qualifier +
6198                    "' not resolvable in scope)");
6199            }
6200            return true;
6201        }
6202
6203        // Resolvable as a table/alias -> treat as real column reference
6204        return false;
6205    }
6206
6207
6208    /**
6209     * SQL Server datepart keywords - used in DATEDIFF, DATEADD, DATEPART, DATENAME functions.
6210     * These are parsed as TObjectName but are actually date/time keywords, not columns.
6211     */
6212    private static final Set<String> SQL_SERVER_DATEPART_KEYWORDS = new HashSet<>(Arrays.asList(
6213        // Standard datepart values
6214        "year", "yy", "yyyy",
6215        "quarter", "qq", "q",
6216        "month", "mm", "m",
6217        "dayofyear", "dy", "y",
6218        "day", "dd", "d",
6219        "week", "wk", "ww",
6220        "weekday", "dw", "w",
6221        "hour", "hh",
6222        "minute", "mi", "n",
6223        "second", "ss", "s",
6224        "millisecond", "ms",
6225        "microsecond", "mcs",
6226        "nanosecond", "ns",
6227        // ISO week
6228        "iso_week", "isowk", "isoww",
6229        // Timezone offset
6230        "tzoffset", "tz"
6231    ));
6232
6233    /**
6234     * Check if a name is a niladic function (built-in function without parentheses)
6235     * for the current database vendor.
6236     *
6237     * Uses TNiladicFunctionUtil with builtinFunctions/niladicFunctions.properties file.
6238     *
6239     * Niladic functions are functions that can be called without parentheses and look like
6240     * column references but are actually function calls returning values.
6241     * Examples: CURRENT_USER (SQL Server), CURRENT_DATETIME (BigQuery), SYSDATE (Oracle)
6242     *
6243     * Note: Regular built-in functions that require parentheses (like DAY(date), COUNT(*))
6244     * are NOT filtered here because they would have parentheses in the SQL and thus be
6245     * parsed as TFunctionCall nodes, not TObjectName.
6246     *
6247     * @param name The identifier name to check
6248     * @return true if it's a known niladic function for the current vendor
6249     */
6250    private boolean isBuiltInFunctionName(String name) {
6251        if (name == null || name.isEmpty()) {
6252            return false;
6253        }
6254
6255        boolean isNiladic = TNiladicFunctionUtil.isNiladicFunction(dbVendor, name);
6256
6257        if (DEBUG_SCOPE_BUILD && isNiladic) {
6258            System.out.println("[DEBUG] Identified niladic function: " + name + " for vendor " + dbVendor);
6259        }
6260
6261        return isNiladic;
6262    }
6263
6264    /**
6265     * Check if the given name is a Snowflake procedure system variable.
6266     * These are special variables available in Snowflake stored procedures:
6267     * - SQLROWCOUNT: Number of rows affected by the last SQL statement
6268     * - SQLERRM: Error message of the last SQL statement
6269     * - SQLSTATE: SQL state code of the last SQL statement
6270     * - SQLCODE: Deprecated, replaced by SQLSTATE
6271     */
6272    private boolean isSnowflakeProcedureSystemVariable(String name) {
6273        if (name == null || name.isEmpty()) {
6274            return false;
6275        }
6276        String upperName = name.toUpperCase();
6277        return "SQLROWCOUNT".equals(upperName) ||
6278               "SQLERRM".equals(upperName) ||
6279               "SQLSTATE".equals(upperName) ||
6280               "SQLCODE".equals(upperName);
6281    }
6282
6283    /**
6284     * Find a Snowflake stage table in the current scope.
6285     * Stage tables are identified by their table name starting with '@' or being a quoted
6286     * string starting with '@' (e.g., '@stage/path' or '@schema.stage_name').
6287     *
6288     * @return The stage table if found, null otherwise
6289     */
6290    private TTable findSnowflakeStageTableInScope() {
6291        // Check if we have a current select scope with a FROM scope
6292        if (currentSelectScope == null) {
6293            if (DEBUG_SCOPE_BUILD) {
6294                System.out.println("[DEBUG] findSnowflakeStageTableInScope: currentSelectScope is null");
6295            }
6296            return null;
6297        }
6298
6299        FromScope fromScope = currentSelectScope.getFromScope();
6300        if (fromScope == null) {
6301            if (DEBUG_SCOPE_BUILD) {
6302                System.out.println("[DEBUG] findSnowflakeStageTableInScope: fromScope is null");
6303            }
6304            return null;
6305        }
6306
6307        if (DEBUG_SCOPE_BUILD) {
6308            System.out.println("[DEBUG] findSnowflakeStageTableInScope: fromScope has " +
6309                fromScope.getChildren().size() + " children");
6310        }
6311
6312        // Search for stage tables through the FromScope's children (namespaces)
6313        for (ScopeChild child : fromScope.getChildren()) {
6314            INamespace namespace = child.getNamespace();
6315            if (namespace != null) {
6316                TTable table = namespace.getFinalTable();
6317                if (DEBUG_SCOPE_BUILD) {
6318                    System.out.println("[DEBUG]   child: alias=" + child.getAlias() +
6319                        " table=" + (table != null ? table.getTableName() : "null") +
6320                        " isStage=" + (table != null ? isSnowflakeStageTable(table) : "N/A"));
6321                }
6322                if (table != null && isSnowflakeStageTable(table)) {
6323                    return table;
6324                }
6325            }
6326        }
6327
6328        return null;
6329    }
6330
6331    /**
6332     * Check if a TTable is a Snowflake stage table.
6333     * Stage tables can be identified by:
6334     * - Table type is stageReference
6335     * - Table name starting with '@' (internal stage)
6336     * - Quoted string starting with '@' (external stage with path)
6337     *
6338     * @param table The table to check
6339     * @return true if this is a stage table
6340     */
6341    private boolean isSnowflakeStageTable(TTable table) {
6342        if (table == null) {
6343            return false;
6344        }
6345
6346        // Check for stageReference table type (e.g., @schema.stage_name/path)
6347        if (table.getTableType() == ETableSource.stageReference) {
6348            return true;
6349        }
6350
6351        if (table.getTableName() == null) {
6352            return false;
6353        }
6354
6355        String tableName = table.getTableName().toString();
6356        if (tableName == null || tableName.isEmpty()) {
6357            return false;
6358        }
6359
6360        // Check for stage table patterns:
6361        // - @stage_name
6362        // - '@stage/path/'
6363        // - @schema.stage_name
6364        return tableName.startsWith("@") ||
6365               tableName.startsWith("'@") ||
6366               tableName.startsWith("\"@");
6367    }
6368
6369    /**
6370     * Check if a TObjectName represents a Snowflake stage file positional column.
6371     * Snowflake stage files allow positional column references like $1, $2, etc.,
6372     * and JSON path access like $1:field.
6373     *
6374     * Patterns recognized:
6375     * - Simple positional: $1, $2, $10, etc. (columnNameOnly = "$1")
6376     * - JSON path: $1:fieldName (objectString = "$1", columnNameOnly = ":fieldName")
6377     *
6378     * @param objectName The object name to check
6379     * @return true if this is a Snowflake stage file positional column
6380     */
6381    private boolean isSnowflakeStagePositionalColumn(TObjectName objectName) {
6382        if (objectName == null) {
6383            return false;
6384        }
6385
6386        String colName = objectName.getColumnNameOnly();
6387        String objStr = objectName.getObjectString();
6388
6389        // Pattern 1: Simple positional column ($1, $2, etc.)
6390        // columnNameOnly = "$1", objectString = ""
6391        if (colName != null && colName.length() >= 2 && colName.startsWith("$")) {
6392            char secondChar = colName.charAt(1);
6393            if (Character.isDigit(secondChar)) {
6394                // Verify all remaining chars are digits
6395                int i = 2;
6396                while (i < colName.length() && Character.isDigit(colName.charAt(i))) {
6397                    i++;
6398                }
6399                if (i == colName.length()) {
6400                    return true;
6401                }
6402            }
6403        }
6404
6405        // Pattern 2: JSON path access ($1:fieldName)
6406        // objectString = "$1", columnNameOnly = ":fieldName"
6407        if (objStr != null && objStr.length() >= 2 && objStr.startsWith("$")) {
6408            char secondChar = objStr.charAt(1);
6409            if (Character.isDigit(secondChar)) {
6410                // Verify remaining chars are digits
6411                int i = 2;
6412                while (i < objStr.length() && Character.isDigit(objStr.charAt(i))) {
6413                    i++;
6414                }
6415                // If objectString is pure positional ($1, $12, etc.) and columnNameOnly starts with ':'
6416                if (i == objStr.length() && colName != null && colName.startsWith(":")) {
6417                    return true;
6418                }
6419            }
6420        }
6421
6422        return false;
6423    }
6424
6425    /**
6426     * Check if a variable name exists in the current PL/SQL block scope chain.
6427     * This walks up the scope chain from the current scope to all parent PL/SQL scopes.
6428     * Used to filter out variable references that should not be collected as column references.
6429     *
6430     * @param variableName The variable name to check (case-insensitive)
6431     * @return true if the variable exists in any scope in the chain
6432     */
6433    private boolean isVariableInPlsqlScopeChain(String variableName) {
6434        // Check the current scope first
6435        if (currentPlsqlBlockScope != null &&
6436            currentPlsqlBlockScope.getVariableNamespace().hasColumn(variableName) == ColumnLevel.EXISTS) {
6437            return true;
6438        }
6439
6440        // Check all parent scopes in the stack
6441        // The stack contains saved parent scopes when we enter nested blocks
6442        for (PlsqlBlockScope parentScope : plsqlBlockScopeStack) {
6443            if (parentScope.getVariableNamespace().hasColumn(variableName) == ColumnLevel.EXISTS) {
6444                return true;
6445            }
6446        }
6447
6448        return false;
6449    }
6450
6451    /**
6452     * Check if a TObjectName is a lambda expression parameter by examining the parent chain.
6453     * Lambda parameters (e.g., x in "x -> x + 1" or acc, x in "(acc, x) -> acc + x")
6454     * should not be treated as column references.
6455     *
6456     * This method is called when lambdaParameters set hasn't been populated yet
6457     * (because TObjectName is visited before TExpression for the lambda).
6458     *
6459     * @param objectName The object name to check
6460     * @return true if it's a lambda parameter
6461     */
6462    private boolean isLambdaParameter(TObjectName objectName) {
6463        if (objectName == null) return false;
6464        if (lambdaParameterStack.isEmpty()) return false;
6465
6466        // Check if this objectName's name matches any parameter in the current lambda context
6467        String name = objectName.toString();
6468        if (name == null) return false;
6469        String nameLower = name.toLowerCase();
6470
6471        // Check all lambda contexts on the stack (for nested lambdas)
6472        for (Set<String> paramNames : lambdaParameterStack) {
6473            if (paramNames.contains(nameLower)) {
6474                // Also add to lambdaParameters for future reference
6475                lambdaParameters.add(objectName);
6476                return true;
6477            }
6478        }
6479
6480        return false;
6481    }
6482
6483    /**
6484     * Check if a name is a known SQL Server schema name.
6485     * Schema names in SQL Server include system schemas and common user schemas.
6486     *
6487     * When a 2-part function name like "dbo.ufnGetInventoryStock" is encountered,
6488     * we need to distinguish between:
6489     * - schema.function() call (e.g., dbo.ufnGetInventoryStock) - NOT a column reference
6490     * - column.method() call (e.g., Demographics.value) - IS a column reference
6491     *
6492     * We use schema name detection to identify the former case.
6493     *
6494     * @param name The potential schema name to check
6495     * @return true if it's a known SQL Server schema name
6496     */
6497    private boolean isSqlServerSchemaName(String name) {
6498        if (name == null || name.isEmpty()) {
6499            return false;
6500        }
6501
6502        String upperName = name.toUpperCase();
6503
6504        // System schemas
6505        if (upperName.equals("DBO") ||
6506            upperName.equals("SYS") ||
6507            upperName.equals("INFORMATION_SCHEMA") ||
6508            upperName.equals("GUEST") ||
6509            upperName.equals("DB_OWNER") ||
6510            upperName.equals("DB_ACCESSADMIN") ||
6511            upperName.equals("DB_SECURITYADMIN") ||
6512            upperName.equals("DB_DDLADMIN") ||
6513            upperName.equals("DB_BACKUPOPERATOR") ||
6514            upperName.equals("DB_DATAREADER") ||
6515            upperName.equals("DB_DATAWRITER") ||
6516            upperName.equals("DB_DENYDATAREADER") ||
6517            upperName.equals("DB_DENYDATAWRITER")) {
6518            return true;
6519        }
6520
6521        return false;
6522    }
6523
6524    /**
6525     * Check if a TObjectName is a cursor name in cursor-related statements.
6526     * Cursor names appear in DECLARE CURSOR, OPEN, FETCH, CLOSE, DEALLOCATE statements
6527     * and should not be treated as column references.
6528     *
6529     * @param objectName The object name to check
6530     * @return true if it's a cursor name in a cursor-related statement
6531     */
6532    private boolean isCursorName(TObjectName objectName) {
6533        // Traverse up the parent chain to find cursor-related statements
6534        if (objectName.getDbObjectType() == EDbObjectType.cursor) return true;
6535
6536        TParseTreeNode node = objectName.getParentObjectName();
6537        while (node != null) {
6538            // SQL Server cursor statements
6539            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare) {
6540                gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare declare =
6541                    (gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare) node;
6542                if (declare.getCursorName() == objectName) {
6543                    return true;
6544                }
6545            }
6546            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlOpen) {
6547                gudusoft.gsqlparser.stmt.mssql.TMssqlOpen open =
6548                    (gudusoft.gsqlparser.stmt.mssql.TMssqlOpen) node;
6549                if (open.getCursorName() == objectName) {
6550                    return true;
6551                }
6552            }
6553            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlFetch) {
6554                gudusoft.gsqlparser.stmt.mssql.TMssqlFetch fetch =
6555                    (gudusoft.gsqlparser.stmt.mssql.TMssqlFetch) node;
6556                if (fetch.getCursorName() == objectName) {
6557                    return true;
6558                }
6559            }
6560            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlClose) {
6561                gudusoft.gsqlparser.stmt.mssql.TMssqlClose close =
6562                    (gudusoft.gsqlparser.stmt.mssql.TMssqlClose) node;
6563                if (close.getCursorName() == objectName) {
6564                    return true;
6565                }
6566            }
6567            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlDeallocate) {
6568                gudusoft.gsqlparser.stmt.mssql.TMssqlDeallocate deallocate =
6569                    (gudusoft.gsqlparser.stmt.mssql.TMssqlDeallocate) node;
6570                if (deallocate.getCursorName() == objectName) {
6571                    return true;
6572                }
6573            }
6574            // Generic cursor statements (used by other databases)
6575            if (node instanceof gudusoft.gsqlparser.stmt.TDeclareCursorStmt) {
6576                gudusoft.gsqlparser.stmt.TDeclareCursorStmt declare =
6577                    (gudusoft.gsqlparser.stmt.TDeclareCursorStmt) node;
6578                if (declare.getCursorName() == objectName) {
6579                    return true;
6580                }
6581            }
6582            if (node instanceof gudusoft.gsqlparser.stmt.TOpenStmt) {
6583                gudusoft.gsqlparser.stmt.TOpenStmt open =
6584                    (gudusoft.gsqlparser.stmt.TOpenStmt) node;
6585                if (open.getCursorName() == objectName) {
6586                    return true;
6587                }
6588            }
6589            if (node instanceof gudusoft.gsqlparser.stmt.TFetchStmt) {
6590                gudusoft.gsqlparser.stmt.TFetchStmt fetch =
6591                    (gudusoft.gsqlparser.stmt.TFetchStmt) node;
6592                if (fetch.getCursorName() == objectName) {
6593                    return true;
6594                }
6595            }
6596            if (node instanceof gudusoft.gsqlparser.stmt.TCloseStmt) {
6597                gudusoft.gsqlparser.stmt.TCloseStmt close =
6598                    (gudusoft.gsqlparser.stmt.TCloseStmt) node;
6599                if (close.getCursorName() == objectName) {
6600                    return true;
6601                }
6602            }
6603            node = node.getParentObjectName();
6604        }
6605        return false;
6606    }
6607
6608    /**
6609     * Check if a name is a SQL Server datepart keyword.
6610     * These keywords are used in DATEDIFF, DATEADD, DATEPART, DATENAME functions
6611     * and should not be treated as column references.
6612     *
6613     * @param name The identifier name to check
6614     * @return true if it's a known SQL Server datepart keyword
6615     */
6616    private boolean isSqlServerDatepartKeyword(String name) {
6617        if (name == null) {
6618            return false;
6619        }
6620        // Only apply this check for SQL Server and Azure SQL Database
6621        if (dbVendor != EDbVendor.dbvmssql && dbVendor != EDbVendor.dbvazuresql) {
6622            return false;
6623        }
6624        return SQL_SERVER_DATEPART_KEYWORDS.contains(name.toLowerCase());
6625    }
6626
6627    /**
6628     * SQL Server date functions that take a datepart keyword as first argument.
6629     */
6630    private static final Set<String> SQL_SERVER_DATE_FUNCTIONS = new HashSet<>(Arrays.asList(
6631        "datediff", "dateadd", "datepart", "datename", "datetrunc",
6632        "datediff_big"  // SQL Server 2016+
6633    ));
6634
6635    /**
6636     * Check if a TObjectName is in a date function context.
6637     * This verifies that the token is preceded by "FUNCTION_NAME(" pattern.
6638     *
6639     * @param objectName The object name to check
6640     * @return true if it appears to be a datepart argument in a date function
6641     */
6642    private boolean isInDateFunctionContext(TObjectName objectName) {
6643        TSourceToken startToken = objectName.getStartToken();
6644        if (startToken == null) {
6645            return false;
6646        }
6647
6648        // Use the token's container to access the token list
6649        TCustomSqlStatement stmt = objectName.getGsqlparser() != null ?
6650            (objectName.getGsqlparser().sqlstatements != null &&
6651             objectName.getGsqlparser().sqlstatements.size() > 0 ?
6652             objectName.getGsqlparser().sqlstatements.get(0) : null) : null;
6653        if (stmt == null || stmt.sourcetokenlist == null) {
6654            return false;
6655        }
6656        TSourceTokenList tokenList = stmt.sourcetokenlist;
6657
6658        // Find the position of our token
6659        int pos = startToken.posinlist;
6660        if (pos < 0) {
6661            return false;
6662        }
6663
6664        // Look for opening paren before this token (skipping whitespace)
6665        int parenPos = pos - 1;
6666        while (parenPos >= 0 && tokenList.get(parenPos).tokentype == ETokenType.ttwhitespace) {
6667            parenPos--;
6668        }
6669        if (parenPos < 0) {
6670            return false;
6671        }
6672
6673        TSourceToken parenToken = tokenList.get(parenPos);
6674        if (parenToken.tokentype != ETokenType.ttleftparenthesis) {
6675            return false;
6676        }
6677
6678        // Look for function name before the opening paren (skipping whitespace)
6679        int funcPos = parenPos - 1;
6680        while (funcPos >= 0 && tokenList.get(funcPos).tokentype == ETokenType.ttwhitespace) {
6681            funcPos--;
6682        }
6683        if (funcPos < 0) {
6684            return false;
6685        }
6686
6687        TSourceToken funcToken = tokenList.get(funcPos);
6688        String funcName = funcToken.toString().toLowerCase();
6689        return SQL_SERVER_DATE_FUNCTIONS.contains(funcName);
6690    }
6691
6692    /**
6693     * Check if a TObjectName is the alias part of SQL Server's proprietary column alias syntax.
6694     * In SQL Server, "column_alias = expression" is a valid way to alias a column.
6695     * The left side (column_alias) should not be treated as a column reference.
6696     *
6697     * Example: SELECT day_diff = DATEDIFF(DAY, start_date, end_date)
6698     * Here "day_diff" is an alias, not a column from any table.
6699     *
6700     * This method checks against the set populated by preVisit(TResultColumn).
6701     */
6702    private boolean isSqlServerProprietaryColumnAlias(TObjectName objectName, TParseTreeNode parent) {
6703        if (sqlServerProprietaryAliases.contains(objectName)) {
6704            if (DEBUG_SCOPE_BUILD) {
6705                System.out.println("[DEBUG] Skipping SQL Server proprietary column alias: " +
6706                        objectName.toString());
6707            }
6708            return true;
6709        }
6710        return false;
6711    }
6712
6713    /**
6714     * Determine which scope a column reference belongs to
6715     */
6716    private IScope determineColumnScope(TObjectName objectName) {
6717        // Special handling for ORDER BY columns in combined queries (UNION/INTERSECT/EXCEPT)
6718        // The parser moves ORDER BY from a branch to the combined query, but the column
6719        // should be resolved in the branch's scope where it originally appeared.
6720        if (currentSelectScope != null) {
6721            TSelectSqlStatement selectStmt = getStatementForScope(currentSelectScope);
6722            if (selectStmt != null && selectStmt.isCombinedQuery()) {
6723                // Check if column is in an ORDER BY clause
6724                if (isInOrderByClause(objectName, selectStmt)) {
6725                    // Find the branch that contains this column's position
6726                    SelectScope branchScope = findBranchScopeByPosition(selectStmt, objectName);
6727                    if (branchScope != null) {
6728                        return branchScope;
6729                    }
6730                }
6731            }
6732            return currentSelectScope;
6733        }
6734
6735        // Or use current UpdateScope if processing UPDATE statement
6736        if (currentUpdateScope != null) {
6737            return currentUpdateScope;
6738        }
6739
6740        // Or use current MergeScope if processing MERGE statement
6741        if (currentMergeScope != null) {
6742            return currentMergeScope;
6743        }
6744
6745        // Or use current DeleteScope if processing DELETE statement
6746        if (currentDeleteScope != null) {
6747            return currentDeleteScope;
6748        }
6749
6750        // Fallback to top of stack
6751        return scopeStack.isEmpty() ? globalScope : scopeStack.peek();
6752    }
6753
6754    /**
6755     * Get the TSelectSqlStatement for a SelectScope
6756     */
6757    private TSelectSqlStatement getStatementForScope(SelectScope scope) {
6758        for (Map.Entry<TSelectSqlStatement, SelectScope> entry : statementScopeMap.entrySet()) {
6759            if (entry.getValue() == scope) {
6760                return entry.getKey();
6761            }
6762        }
6763        return null;
6764    }
6765
6766    /**
6767     * Check if an object name is inside an ORDER BY clause
6768     */
6769    private boolean isInOrderByClause(TObjectName objectName, TSelectSqlStatement stmt) {
6770        TOrderBy orderBy = stmt.getOrderbyClause();
6771        if (orderBy == null) {
6772            return false;
6773        }
6774        // Check if objectName's position is within ORDER BY's position range
6775        long objOffset = objectName.getStartToken().posinlist;
6776        long orderByStart = orderBy.getStartToken().posinlist;
6777        long orderByEnd = orderBy.getEndToken().posinlist;
6778        return objOffset >= orderByStart && objOffset <= orderByEnd;
6779    }
6780
6781    /**
6782     * Find the branch SelectScope that contains the given column's position.
6783     * Returns null if no matching branch is found.
6784     */
6785    private SelectScope findBranchScopeByPosition(TSelectSqlStatement combinedStmt, TObjectName objectName) {
6786        if (!combinedStmt.isCombinedQuery()) {
6787            return null;
6788        }
6789
6790        long columnLine = objectName.getStartToken().lineNo;
6791
6792        // Search through branches recursively
6793        return findBranchScopeByLineRecursive(combinedStmt, columnLine);
6794    }
6795
6796    /**
6797     * Recursively search for the branch that contains the given line number.
6798     */
6799    private SelectScope findBranchScopeByLineRecursive(TSelectSqlStatement stmt, long targetLine) {
6800        if (!stmt.isCombinedQuery()) {
6801            // This is a leaf branch - check if it contains the target line
6802            // A branch "contains" the line if its FROM clause table is on that line
6803            if (stmt.tables != null && stmt.tables.size() > 0) {
6804                for (int i = 0; i < stmt.tables.size(); i++) {
6805                    TTable table = stmt.tables.getTable(i);
6806                    if (table.getStartToken().lineNo == targetLine) {
6807                        return statementScopeMap.get(stmt);
6808                    }
6809                }
6810            }
6811            // Alternative: check if statement's range includes the target line
6812            long stmtStartLine = stmt.getStartToken().lineNo;
6813            long stmtEndLine = stmt.getEndToken().lineNo;
6814            if (targetLine >= stmtStartLine && targetLine <= stmtEndLine) {
6815                return statementScopeMap.get(stmt);
6816            }
6817            return null;
6818        }
6819
6820        // Combined query - search children
6821        SelectScope leftResult = findBranchScopeByLineRecursive(stmt.getLeftStmt(), targetLine);
6822        if (leftResult != null) {
6823            return leftResult;
6824        }
6825        return findBranchScopeByLineRecursive(stmt.getRightStmt(), targetLine);
6826    }
6827
6828    // ========== Accessors ==========
6829
6830    public GlobalScope getGlobalScope() {
6831        return globalScope;
6832    }
6833
6834    public INameMatcher getNameMatcher() {
6835        return nameMatcher;
6836    }
6837
6838    public Map<TUpdateSqlStatement, UpdateScope> getUpdateScopeMap() {
6839        return Collections.unmodifiableMap(updateScopeMap);
6840    }
6841
6842    public Map<TDeleteSqlStatement, DeleteScope> getDeleteScopeMap() {
6843        return Collections.unmodifiableMap(deleteScopeMap);
6844    }
6845
6846    /**
6847     * Get the mapping of USING columns to their right-side tables.
6848     * In JOIN...USING syntax, USING columns should preferentially resolve
6849     * to the right-side (physical) table for TGetTableColumn compatibility.
6850     *
6851     * @return Map of USING column TObjectName -> right-side TTable
6852     */
6853    public Map<TObjectName, TTable> getUsingColumnToRightTable() {
6854        return Collections.unmodifiableMap(usingColumnToRightTable);
6855    }
6856
6857    /**
6858     * Get the set of virtual trigger tables (deleted/inserted in SQL Server triggers).
6859     * These tables should be excluded from table output since their columns are
6860     * resolved to the trigger's target table.
6861     *
6862     * @return Set of TTable objects that are virtual trigger tables
6863     */
6864    public Set<TTable> getVirtualTriggerTables() {
6865        return Collections.unmodifiableSet(virtualTriggerTables);
6866    }
6867
6868    /**
6869     * Get the set of SET clause target columns (UPDATE SET left-side columns).
6870     * These columns already have sourceTable correctly set to the UPDATE target table
6871     * and should NOT be re-resolved through star column push-down.
6872     *
6873     * @return Set of TObjectName nodes that are SET clause target columns
6874     */
6875    public Set<TObjectName> getSetClauseTargetColumns() {
6876        return Collections.unmodifiableSet(setClauseTargetColumns);
6877    }
6878
6879    /**
6880     * Get the set of INSERT ALL target columns (from TInsertIntoValue columnList).
6881     * These columns already have sourceTable correctly set to the INSERT target table
6882     * and should NOT be re-resolved against the subquery scope.
6883     *
6884     * @return Set of TObjectName nodes that are INSERT ALL target columns
6885     */
6886    public Set<TObjectName> getInsertAllTargetColumns() {
6887        return Collections.unmodifiableSet(insertAllTargetColumns);
6888    }
6889}