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