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