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