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            cteNamespace.setReferencingTable(table);  // Set the referencing TTable for getFinalTable() fallback
3137            currentFromScope.addChild(cteNamespace, alias, false);
3138            lastProcessedFromTable = table;  // Track for JOIN...USING left table detection
3139            currentJoinChainTables.add(table);  // Track ALL tables in join chain for chained USING
3140            if (DEBUG_SCOPE_BUILD) {
3141                System.out.println("[DEBUG] Added CTE to FromScope: alias=" + alias);
3142            }
3143        } else {
3144            // Check if this is a SQL Server virtual table (deleted/inserted)
3145            // These can appear in:
3146            // 1. CREATE TRIGGER bodies - should reference the trigger's target table
3147            // 2. OUTPUT clauses of INSERT/UPDATE/DELETE - handled by preVisit(TOutputClause)
3148            //
3149            // IMPORTANT: Only substitute deleted/inserted when inside a TRIGGER context.
3150            // In DML (INSERT/UPDATE/DELETE) context, deleted/inserted in FROM clause
3151            // are regular table references (could be trigger pseudo-tables from an outer trigger).
3152            // The OUTPUT clause pseudo-columns are handled separately in preVisit(TOutputClause).
3153            TTable effectiveTable = table;
3154            if (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) {
3155                String upperName = tableName != null ? tableName.toUpperCase() : "";
3156                if ("DELETED".equals(upperName) || "INSERTED".equals(upperName)) {
3157                    // Only substitute when inside a TRIGGER (not just any DML statement)
3158                    if (currentTriggerTargetTable != null) {
3159                        effectiveTable = currentTriggerTargetTable;
3160                        // Track this table as a virtual trigger table (to be skipped in table output)
3161                        virtualTriggerTables.add(table);
3162                        if (DEBUG_SCOPE_BUILD) {
3163                            System.out.println("[DEBUG] Substituting virtual table '" + tableName +
3164                                "' with trigger target table '" + currentTriggerTargetTable.getName() + "'");
3165                        }
3166                    }
3167                }
3168            }
3169
3170            // Regular physical table - pass TSQLEnv for metadata lookup
3171            TableNamespace tableNs = new TableNamespace(effectiveTable, nameMatcher, sqlEnv);
3172            tableNs.validate();
3173            String alias = getTableAlias(table);
3174
3175            // Add to FROM scope ONLY if not inside a PIVOT/UNPIVOT source processing context.
3176            // When a table is the source of a PIVOT/UNPIVOT, only the PIVOT/UNPIVOT table
3177            // should be visible in the outer query, not the source table itself.
3178            if (pivotSourceProcessingDepth == 0) {
3179                currentFromScope.addChild(tableNs, alias, false);
3180                if (DEBUG_SCOPE_BUILD) {
3181                    System.out.println("[DEBUG] Added table to FromScope: alias=" + alias + " tableName=" + tableName +
3182                        " hasMetadata=" + (tableNs.getResolvedTable() != null));
3183                }
3184            } else {
3185                if (DEBUG_SCOPE_BUILD) {
3186                    System.out.println("[DEBUG] Skipping table from FromScope (inside PIVOT source): alias=" + alias);
3187                }
3188            }
3189
3190            lastProcessedFromTable = table;  // Track for JOIN...USING left table detection
3191            currentJoinChainTables.add(table);  // Track ALL tables in join chain for chained USING
3192            tableToNamespaceMap.put(table, tableNs);  // Store for legacy compatibility
3193        }
3194    }
3195
3196    /**
3197     * Process a subquery in FROM clause
3198     */
3199    private void processSubqueryTable(TTable table) {
3200        TSelectSqlStatement subquery = table.getSubquery();
3201        if (subquery == null) {
3202            return;
3203        }
3204
3205        // Track column name definitions from the alias clause
3206        // These are column DEFINITIONS, not references - should NOT be collected as column refs
3207        // e.g., in "FROM (SELECT ...) AS t(id, name)", 'id' and 'name' are definitions
3208        TAliasClause aliasClause = table.getAliasClause();
3209        if (aliasClause != null) {
3210            TObjectNameList columns = aliasClause.getColumns();
3211            if (columns != null && columns.size() > 0) {
3212                for (int i = 0; i < columns.size(); i++) {
3213                    TObjectName colName = columns.getObjectName(i);
3214                    if (colName != null) {
3215                        valuesTableAliasColumns.add(colName);
3216                        if (DEBUG_SCOPE_BUILD) {
3217                            System.out.println("[DEBUG] Tracked subquery alias column definition (will skip): " + colName.toString());
3218                        }
3219                    }
3220                }
3221            }
3222        }
3223
3224        String alias = table.getAliasName();
3225        INamespace namespace;
3226
3227        // Check if this is a TABLE function (Oracle TABLE(SELECT ...) syntax)
3228        // When isTableKeyword() is true, the subquery is wrapped in a TABLE() function
3229        boolean isTableFunction = table.isTableKeyword();
3230
3231        // Check if this is a UNION/INTERSECT/EXCEPT query
3232        if (subquery.isCombinedQuery()) {
3233            // Create UnionNamespace for set operations
3234            UnionNamespace unionNs = new UnionNamespace(subquery, alias, nameMatcher);
3235            namespace = unionNs;
3236
3237            if (DEBUG_SCOPE_BUILD) {
3238                System.out.println("[DEBUG]   Detected UNION query with " +
3239                    unionNs.getBranchCount() + " branches");
3240            }
3241        } else {
3242            // Regular subquery - create SubqueryNamespace
3243            // Pass isTableFunction to mark TABLE function subqueries for expression alias filtering
3244            SubqueryNamespace subNs = new SubqueryNamespace(subquery, alias, nameMatcher, isTableFunction);
3245            // Pass guessColumnStrategy for config-based isolation (prevents test side effects)
3246            if (guessColumnStrategy >= 0) {
3247                subNs.setGuessColumnStrategy(guessColumnStrategy);
3248            }
3249            // Pass sqlEnv for metadata lookup during star column resolution
3250            if (sqlEnv != null) {
3251                subNs.setSqlEnv(sqlEnv);
3252            }
3253            // Set the owning TTable for legacy sync support
3254            subNs.setSourceTable(table);
3255            namespace = subNs;
3256
3257            // Register for deferred validation
3258            // SubqueryNamespace needs to be validated AFTER its subquery's SELECT list is processed
3259            // because the column names come from the SELECT list
3260            pendingSubqueryValidation.put(table, subNs);
3261        }
3262
3263        // Add to FROM scope ONLY if not inside a PIVOT/UNPIVOT source processing context.
3264        // When a subquery is the source of a PIVOT/UNPIVOT, only the PIVOT/UNPIVOT table
3265        // should be visible in the outer query, not the source subquery itself.
3266        // Example: FROM (SELECT ...) p UNPIVOT (...) AS unpvt
3267        //   - Only 'unpvt' should be visible, not 'p'
3268        //   - Column references in outer SELECT should resolve to 'unpvt', not ambiguously to both
3269        if (pivotSourceProcessingDepth == 0) {
3270            currentFromScope.addChild(namespace, alias != null ? alias : "<subquery>", false);
3271            if (DEBUG_SCOPE_BUILD) {
3272                System.out.println("[DEBUG] Added subquery to FromScope: alias=" + alias);
3273            }
3274        } else {
3275            if (DEBUG_SCOPE_BUILD) {
3276                System.out.println("[DEBUG] Skipping subquery from FromScope (inside PIVOT source): alias=" + alias);
3277            }
3278        }
3279        tableToNamespaceMap.put(table, namespace);  // Store for legacy compatibility
3280
3281        // Note: The subquery SELECT will be processed when acceptChildren traverses into it
3282        // For UnionNamespace, validation happens during construction
3283    }
3284
3285    /**
3286     * Process a table-valued function
3287     *
3288     * TABLE functions can contain subqueries as arguments, e.g.:
3289     * TABLE (SELECT AVG(E.SALARY) AS AVGSAL, COUNT(*) AS EMPCOUNT FROM EMP E) AS EMPINFO
3290     *
3291     * In this case, we create a SubqueryNamespace to expose the subquery's SELECT list
3292     * columns (AVGSAL, EMPCOUNT) to the outer query.
3293     */
3294    private void processTableFunction(TTable table) {
3295        // Track this function call as a table-valued function (not a column method call)
3296        if (table.getFuncCall() != null) {
3297            tableValuedFunctionCalls.add(table.getFuncCall());
3298        }
3299
3300        String alias = getTableAlias(table);
3301
3302        // Check if the TABLE function has a subquery argument
3303        TFunctionCall funcCall = table.getFuncCall();
3304        if (funcCall != null && funcCall.getArgs() != null && funcCall.getArgs().size() > 0) {
3305            TExpression firstArg = funcCall.getArgs().getExpression(0);
3306            if (firstArg != null && firstArg.getSubQuery() != null) {
3307                // TABLE function contains a subquery - create SubqueryNamespace
3308                TSelectSqlStatement subquery = firstArg.getSubQuery();
3309
3310                // Check if this is a UNION/INTERSECT/EXCEPT query
3311                INamespace namespace;
3312                if (subquery.isCombinedQuery()) {
3313                    // Create UnionNamespace for set operations
3314                    UnionNamespace unionNs = new UnionNamespace(subquery, alias, nameMatcher);
3315                    namespace = unionNs;
3316                } else {
3317                    // Create SubqueryNamespace for regular subquery, marked as from TABLE function
3318                    SubqueryNamespace subNs = new SubqueryNamespace(subquery, alias, nameMatcher, true);
3319                    // Pass guessColumnStrategy for config-based isolation (prevents test side effects)
3320                    if (guessColumnStrategy >= 0) {
3321                        subNs.setGuessColumnStrategy(guessColumnStrategy);
3322                    }
3323                    // Pass sqlEnv for metadata lookup during star column resolution
3324                    if (sqlEnv != null) {
3325                        subNs.setSqlEnv(sqlEnv);
3326                    }
3327                    // Set the owning TTable for legacy sync support
3328                    subNs.setSourceTable(table);
3329                    namespace = subNs;
3330                    // Defer validation until after children are visited
3331                    pendingSubqueryValidation.put(table, subNs);
3332                }
3333
3334                currentFromScope.addChild(namespace, alias, false);
3335                tableToNamespaceMap.put(table, namespace);  // Store for legacy compatibility
3336
3337                if (DEBUG_SCOPE_BUILD) {
3338                    System.out.println("[DEBUG] Added TABLE function with subquery to FromScope: alias=" + alias);
3339                }
3340                return;
3341            }
3342        }
3343
3344        // Fallback: treat as a regular table-valued function (e.g., UDF)
3345        TableNamespace tableNs = new TableNamespace(table, nameMatcher, sqlEnv);
3346        tableNs.validate();
3347        currentFromScope.addChild(tableNs, alias, false);
3348        tableToNamespaceMap.put(table, tableNs);  // Store for legacy compatibility
3349    }
3350
3351    /**
3352     * Process a PIVOT table
3353     *
3354     * PIVOT tables are created from a source table and a PIVOT clause:
3355     * <pre>
3356     * FROM source_table
3357     * PIVOT (aggregate_function(value) FOR column IN (val1, val2, ...)) AS alias
3358     * </pre>
3359     *
3360     * The PIVOT produces new columns (val1, val2, ...) while maintaining
3361     * some pass-through columns from the source table.
3362     */
3363    private void processPivotTable(TTable table) {
3364        // Get the source table of the pivot (the table being pivoted)
3365        // Note: For SQL Server, getSourceTableOfPivot() may return null, so we also check
3366        // getPivotedTable().getTableSource() which is set during parsing.
3367        // However, for BigQuery, using this fallback can break resolution because BigQuery's
3368        // AST structure adds the source table to FROM scope separately during traversal.
3369        // For SQL Server, we need the fallback to properly resolve pass-through columns.
3370        TTable sourceTable = table.getSourceTableOfPivot();
3371
3372        // Get PIVOT clause from the table's pivot table reference
3373        TPivotClause pivotClause = null;
3374        TPivotedTable pivotedTable = table.getPivotedTable();
3375
3376        // SQL Server UNPIVOT issue: When UNPIVOT is used on a subquery, the table from
3377        // statement.tables may have tableType=pivoted_table but getPivotedTable()=null.
3378        // The actual pivot clause is on a different TTable object from statement.joins.
3379        // We need to search for it there.
3380        if (pivotedTable == null && (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql)) {
3381            // Try to find the actual pivot table from joins
3382            TTable pivotTableFromJoins = findPivotTableInJoins(table);
3383            if (pivotTableFromJoins != null && pivotTableFromJoins.getPivotedTable() != null) {
3384                pivotedTable = pivotTableFromJoins.getPivotedTable();
3385            }
3386        }
3387
3388        if (pivotedTable != null) {
3389            pivotClause = pivotedTable.getPivotClause();
3390            // Also try to get source table from the pivoted table if not found yet
3391            if (sourceTable == null) {
3392                TTable potentialSourceTable = pivotedTable.getTableSource();
3393                // Use the fallback to get the source table for vendors that need it.
3394                // This is required for pass-through column resolution in PIVOT tables.
3395                // Note: For BigQuery, the source table (especially subqueries) needs this
3396                // fallback to properly resolve pass-through columns.
3397                if (potentialSourceTable != null) {
3398                    sourceTable = potentialSourceTable;
3399                }
3400            }
3401        }
3402
3403        // Get the alias - for PIVOT tables, the alias is in the PIVOT clause
3404        String alias = getTableAlias(table);
3405
3406        // If table alias is null/empty, try to get from pivot clause's alias clause
3407        if ((alias == null || alias.isEmpty() || alias.startsWith("null")) && pivotClause != null) {
3408            if (pivotClause.getAliasClause() != null &&
3409                pivotClause.getAliasClause().getAliasName() != null) {
3410                alias = pivotClause.getAliasClause().getAliasName().toString();
3411            }
3412        }
3413
3414        // Fallback to appropriate default alias if still no alias
3415        if (alias == null || alias.isEmpty() || alias.startsWith("null")) {
3416            // Use different default for PIVOT vs UNPIVOT to match TPivotClause.doParse()
3417            if (pivotClause != null && pivotClause.getType() == TPivotClause.unpivot) {
3418                alias = "unpivot_alias";
3419            } else {
3420                alias = "pivot_alias";
3421            }
3422        }
3423
3424        // IMPORTANT: The visitor traverses through getRelations() which may contain
3425        // different TTable objects than statement.tables. The formatter matches
3426        // sourceTable against statement.tables using object identity. We need to
3427        // find the matching pivot table in statement.tables to ensure proper matching.
3428        TTable pivotTableForNamespace = findMatchingPivotTableInStatement(table, alias);
3429
3430        // Create PivotNamespace with the table from statement.tables (if found)
3431        PivotNamespace pivotNs = new PivotNamespace(pivotTableForNamespace, pivotClause, sourceTable, alias, nameMatcher);
3432        pivotNs.validate();
3433
3434        // Wire source namespace for pass-through column resolution (Delta 2)
3435        if (sourceTable != null) {
3436            INamespace sourceNamespace = resolveSourceNamespaceForPivot(sourceTable);
3437            if (sourceNamespace != null) {
3438                pivotNs.setSourceNamespace(sourceNamespace);
3439            }
3440        }
3441
3442        // Add to FROM scope with the pivot table alias
3443        currentFromScope.addChild(pivotNs, alias, false);
3444        tableToNamespaceMap.put(table, pivotNs);  // Store for legacy compatibility
3445        if (pivotTableForNamespace != null && pivotTableForNamespace != table) {
3446            tableToNamespaceMap.put(pivotTableForNamespace, pivotNs);  // Also map the matched table
3447        }
3448
3449        // Add all PIVOT/UNPIVOT columns to allColumnReferences
3450        // Per CLAUDE.md, this resolution logic MUST be in ScopeBuilder, not in the formatter
3451        if (pivotClause != null) {
3452            if (pivotClause.getType() == TPivotClause.unpivot) {
3453                // UNPIVOT case: add generated columns and IN clause source columns
3454                addUnpivotColumns(pivotClause, pivotTableForNamespace, sourceTable);
3455            } else {
3456                // PIVOT case: Check if there's an alias column list (e.g., AS p (col1, col2, col3))
3457                // If so, use the alias columns as they REPLACE the IN clause column names
3458                boolean hasAliasColumnList = pivotClause.getAliasClause() != null &&
3459                    pivotClause.getAliasClause().getColumns() != null &&
3460                    pivotClause.getAliasClause().getColumns().size() > 0;
3461
3462                if (hasAliasColumnList) {
3463                    // Use alias column list - these replace the default pivot column names
3464                    addPivotAliasColumns(pivotClause.getAliasClause().getColumns(), pivotTableForNamespace);
3465                } else {
3466                    // No alias column list - use IN clause columns as pivot column names
3467                    TPivotInClause inClause = pivotClause.getPivotInClause();
3468                    if (inClause != null) {
3469                        addPivotInClauseColumns(inClause, pivotTableForNamespace);
3470                    }
3471                }
3472            }
3473        }
3474
3475        if (DEBUG_SCOPE_BUILD) {
3476            System.out.println("[DEBUG] Added PIVOT table to FromScope: alias=" + alias +
3477                " sourceTable=" + (sourceTable != null ? sourceTable.getName() : "null") +
3478                " pivotColumns=" + pivotNs.getPivotColumns().size() +
3479                " pivotTable=" + (pivotTableForNamespace == table ? "same" : "from-stmt-tables"));
3480        }
3481
3482        // Mark that we're now processing PIVOT/UNPIVOT source relations.
3483        // This prevents the source subquery from being added to FromScope
3484        // when the visitor traverses the TPivotedTable's children.
3485        // Will be decremented in postVisit(TTable) for pivot tables.
3486        pivotSourceProcessingDepth++;
3487    }
3488
3489    /**
3490     * Add all columns from PIVOT IN clause to allColumnReferences.
3491     * This ensures all pivot columns appear in the output, not just the ones referenced in SELECT.
3492     *
3493     * @param inClause The PIVOT IN clause
3494     * @param pivotTable The pivot table to set as sourceTable
3495     */
3496    private void addPivotInClauseColumns(TPivotInClause inClause, TTable pivotTable) {
3497        if (inClause == null || pivotTable == null) {
3498            return;
3499        }
3500
3501        // Case 1: IN clause has items (e.g., IN ([1], [2]) or IN ("SINGAPORE","LONDON","HOUSTON"))
3502        TResultColumnList items = inClause.getItems();
3503        if (items != null) {
3504            for (int i = 0; i < items.size(); i++) {
3505                TResultColumn resultColumn = items.getResultColumn(i);
3506                if (resultColumn == null || resultColumn.getExpr() == null) {
3507                    continue;
3508                }
3509                TExpression expr = resultColumn.getExpr();
3510
3511                if (expr.getExpressionType() == EExpressionType.simple_object_name_t) {
3512                    // Column reference (e.g., [Sammich], [Apple], "HOUSTON" in BigQuery)
3513                    // These are pivot column DEFINITIONS, not references to source table columns.
3514                    TObjectName objName = expr.getObjectOperand();
3515                    if (objName != null) {
3516                        objName.setSourceTable(pivotTable);
3517                        // Mark as pivot IN clause column so preVisit(TObjectName) won't re-process it
3518                        // and NameResolver won't overwrite the sourceTable with the source table
3519                        pivotInClauseColumns.add(objName);
3520                        allColumnReferences.add(objName);
3521                        // IMPORTANT: Do NOT add to columnToScopeMap - these are column DEFINITIONS,
3522                        // not references that need resolution. Adding to the map would cause the
3523                        // NameResolver to resolve them as source table columns, overwriting the
3524                        // sourceTable we set above.
3525                    }
3526                } else if (expr.getExpressionType() == EExpressionType.simple_constant_t) {
3527                    // Constant value (e.g., "HOUSTON", 'value')
3528                    // These are pivot column DEFINITIONS, not references that need resolution.
3529                    TConstant constant = expr.getConstantOperand();
3530                    if (constant != null && constant.getValueToken() != null) {
3531                        // Strip quotes from constant value for clean column name
3532                        String rawValue = constant.getValueToken().toString();
3533                        String cleanValue = stripStringDelimiters(rawValue);
3534                        TObjectName newColRef = TObjectName.createObjectName(
3535                            inClause.dbvendor,
3536                            EDbObjectType.column,
3537                            new TSourceToken(cleanValue)
3538                        );
3539                        newColRef.setSourceTable(pivotTable);
3540                        allColumnReferences.add(newColRef);
3541                        // IMPORTANT: Do NOT add to columnToScopeMap - these are column DEFINITIONS,
3542                        // not references that need resolution. Adding to the map would cause the
3543                        // NameResolver to resolve them and overwrite the sourceTable we set above.
3544                    }
3545                }
3546            }
3547        }
3548
3549        // Case 2: IN clause has a subquery (e.g., IN (SELECT DISTINCT col FROM table))
3550        if (inClause.getSubQuery() != null) {
3551            TResultColumnList subqueryColumns = inClause.getSubQuery().getResultColumnList();
3552            if (subqueryColumns != null) {
3553                for (int i = 0; i < subqueryColumns.size(); i++) {
3554                    TResultColumn resultColumn = subqueryColumns.getResultColumn(i);
3555                    if (resultColumn != null) {
3556                        TObjectName pivotColumn = TObjectName.createObjectName(
3557                            inClause.dbvendor,
3558                            EDbObjectType.column,
3559                            new TSourceToken(resultColumn.getDisplayName())
3560                        );
3561                        pivotColumn.setSourceTable(pivotTable);
3562                        allColumnReferences.add(pivotColumn);
3563                        // IMPORTANT: Do NOT add to columnToScopeMap.
3564                        // These are synthetic PIVOT output column DEFINITIONS (derived from IN-subquery result),
3565                        // not references that should be name-resolved. Adding them to the map would allow
3566                        // NameResolver to overwrite pivotColumn.sourceTable to some source table.
3567                    }
3568                }
3569            }
3570        }
3571    }
3572
3573    /**
3574     * Add PIVOT alias columns to allColumnReferences.
3575     * When PIVOT has an alias clause with column list (e.g., AS p (empid_renamed, Q1, Q2, Q3, Q4)),
3576     * the alias columns REPLACE the IN clause column names in the output.
3577     *
3578     * @param aliasColumns The column list from the alias clause
3579     * @param pivotTable The pivot table to set as sourceTable
3580     */
3581    private void addPivotAliasColumns(TObjectNameList aliasColumns, TTable pivotTable) {
3582        if (aliasColumns == null || pivotTable == null) {
3583            return;
3584        }
3585
3586        for (int i = 0; i < aliasColumns.size(); i++) {
3587            TObjectName aliasCol = aliasColumns.getObjectName(i);
3588            if (aliasCol != null) {
3589                aliasCol.setSourceTable(pivotTable);
3590                allColumnReferences.add(aliasCol);
3591                // IMPORTANT: Do NOT add to columnToScopeMap.
3592                // These are PIVOT output column DEFINITIONS from the alias list,
3593                // not references that should be name-resolved.
3594            }
3595        }
3596    }
3597
3598    /**
3599     * Add UNPIVOT source columns (IN clause) to allColumnReferences and mark definition columns.
3600     *
3601     * UNPIVOT (yearly_total FOR order_mode IN (store AS 'direct', internet AS 'online'))
3602     * - yearly_total is the value column (DEFINITION - creates a new column, NOT a reference)
3603     * - order_mode is the FOR column (DEFINITION - creates a new column, NOT a reference)
3604     * - store, internet are source columns (REFERENCES - belong to source table)
3605     *
3606     * IMPORTANT: Value and FOR columns are DEFINITIONS that create new columns in the UNPIVOT
3607     * output. They should NOT be added to allColumnReferences (which tracks references).
3608     * The PivotNamespace already tracks these generated columns for resolution purposes.
3609     *
3610     * @param pivotClause The UNPIVOT clause
3611     * @param unpivotTable The UNPIVOT table for generated columns
3612     * @param sourceTable The source table for IN clause columns
3613     */
3614    private void addUnpivotColumns(TPivotClause pivotClause, TTable unpivotTable, TTable sourceTable) {
3615        if (pivotClause == null || unpivotTable == null) {
3616            return;
3617        }
3618
3619        // Mark value columns as UNPIVOT definitions (NOT column references)
3620        // These define new output columns like "yearly_total" in UNPIVOT (yearly_total FOR ...)
3621        // We still set sourceTable for resolution purposes, but mark them so they're not
3622        // collected as column references by isColumnReference().
3623        // Note: For single value column UNPIVOT (like Oracle), use getValueColumn() (deprecated singular)
3624        TObjectNameList valueColumns = pivotClause.getValueColumnList();
3625        if (valueColumns != null && valueColumns.size() > 0) {
3626            for (int i = 0; i < valueColumns.size(); i++) {
3627                TObjectName valueCol = valueColumns.getObjectName(i);
3628                if (valueCol != null) {
3629                    valueCol.setSourceTable(unpivotTable);
3630                    unpivotDefinitionColumns.add(valueCol);
3631                    // Add to output completeness list as a DEFINITION (no name resolution)
3632                    allColumnReferences.add(valueCol);
3633                }
3634            }
3635        } else {
3636            // Fallback to deprecated singular method for Oracle compatibility
3637            @SuppressWarnings("deprecation")
3638            TObjectName valueCol = pivotClause.getValueColumn();
3639            if (valueCol != null) {
3640                valueCol.setSourceTable(unpivotTable);
3641                unpivotDefinitionColumns.add(valueCol);
3642                // Add to output completeness list as a DEFINITION (no name resolution)
3643                allColumnReferences.add(valueCol);
3644            }
3645        }
3646
3647        // Mark FOR columns as UNPIVOT definitions (NOT column references)
3648        // These define new output columns like "order_mode" in UNPIVOT (... FOR order_mode IN ...)
3649        // We still set sourceTable for resolution purposes, but mark them so they're not
3650        // collected as column references by isColumnReference().
3651        // Note: For single FOR column UNPIVOT (like Oracle), use getPivotColumn() (deprecated singular)
3652        TObjectNameList pivotColumnList = pivotClause.getPivotColumnList();
3653        if (pivotColumnList != null && pivotColumnList.size() > 0) {
3654            for (int i = 0; i < pivotColumnList.size(); i++) {
3655                TObjectName forCol = pivotColumnList.getObjectName(i);
3656                if (forCol != null) {
3657                    forCol.setSourceTable(unpivotTable);
3658                    unpivotDefinitionColumns.add(forCol);
3659                    // Add to output completeness list as a DEFINITION (no name resolution)
3660                    allColumnReferences.add(forCol);
3661                }
3662            }
3663        } else {
3664            // Fallback to deprecated singular method for Oracle compatibility
3665            @SuppressWarnings("deprecation")
3666            TObjectName forCol = pivotClause.getPivotColumn();
3667            if (forCol != null) {
3668                forCol.setSourceTable(unpivotTable);
3669                unpivotDefinitionColumns.add(forCol);
3670                // Add to output completeness list as a DEFINITION (no name resolution)
3671                allColumnReferences.add(forCol);
3672            }
3673        }
3674
3675        // IN clause columns (e.g., store, internet in "IN (store AS 'direct', internet AS 'online')")
3676        // are references to columns in the source table. They should be added to allColumnReferences
3677        // so they appear in the output with source table attribution (e.g., pivot_table.store).
3678        //
3679        // We set their sourceTable to the source table and add them to allColumnReferences.
3680        // We also mark them as unpivotDefinitionColumns so they're not collected again by isColumnReference().
3681        // S2: vendor-aware key set so quoted-vs-unquoted UNPIVOT consumed
3682        // columns are correctly distinguished on Oracle / Postgres quoted
3683        // and BigQuery (columns insensitive). Lookup site below uses the
3684        // same keyForColumn() helper.
3685        Set<String> consumedColumns = new HashSet<>();  // Track consumed column names
3686        TUnpivotInClause unpivotInClause = pivotClause.getUnpivotInClause();
3687        if (unpivotInClause != null && unpivotInClause.getItems() != null && sourceTable != null) {
3688            for (int i = 0; i < unpivotInClause.getItems().size(); i++) {
3689                TUnpivotInClauseItem item = unpivotInClause.getItems().getElement(i);
3690                if (item != null) {
3691                    // Single column case
3692                    if (item.getColumn() != null) {
3693                        TObjectName col = item.getColumn();
3694                        col.setSourceTable(sourceTable);
3695                        // Mark as definition so it's not collected again in isColumnReference()
3696                        unpivotDefinitionColumns.add(col);
3697                        // Add to allColumnReferences - this IS a reference to a source table column
3698                        allColumnReferences.add(col);
3699                        // Track consumed column name (strip table prefix if present)
3700                        String colName = col.getColumnNameOnly();
3701                        if (colName != null) consumedColumns.add(keyForColumn(colName));
3702                    }
3703                    // Multi-column case
3704                    if (item.getColumnList() != null) {
3705                        for (int j = 0; j < item.getColumnList().size(); j++) {
3706                            TObjectName col = item.getColumnList().getObjectName(j);
3707                            if (col != null) {
3708                                col.setSourceTable(sourceTable);
3709                                // Mark as definition so it's not collected again in isColumnReference()
3710                                unpivotDefinitionColumns.add(col);
3711                                // Add to allColumnReferences - this IS a reference to a source table column
3712                                allColumnReferences.add(col);
3713                                // Track consumed column name
3714                                String colName = col.getColumnNameOnly();
3715                                if (colName != null) consumedColumns.add(keyForColumn(colName));
3716                            }
3717                        }
3718                    }
3719                }
3720            }
3721        }
3722
3723        // Collect value and FOR column names for exclusion
3724        // (reusing valueColumns and pivotColumnList already defined above)
3725        if (valueColumns != null) {
3726            for (int i = 0; i < valueColumns.size(); i++) {
3727                TObjectName vc = valueColumns.getObjectName(i);
3728                if (vc != null && vc.getColumnNameOnly() != null) {
3729                    consumedColumns.add(keyForColumn(vc.getColumnNameOnly()));
3730                }
3731            }
3732        }
3733        if (pivotColumnList != null) {
3734            for (int i = 0; i < pivotColumnList.size(); i++) {
3735                TObjectName fc = pivotColumnList.getObjectName(i);
3736                if (fc != null && fc.getColumnNameOnly() != null) {
3737                    consumedColumns.add(keyForColumn(fc.getColumnNameOnly()));
3738                }
3739            }
3740        }
3741
3742        // Add pass-through columns as part of the UNPIVOT virtual table's output schema.
3743        // Pass-through columns are source columns that are NOT consumed by UNPIVOT:
3744        // - NOT in the IN clause (consumed columns)
3745        // - NOT the value column (generated by UNPIVOT)
3746        // - NOT the FOR column (generated by UNPIVOT)
3747        //
3748        // This ensures pass-through columns appear in the output even when not explicitly
3749        // referenced in the outer SELECT, similar to how value and FOR columns are added.
3750        //
3751        // NOTE: The source subquery's own columns will ALSO be collected during normal
3752        // traversal (with pivotSourceProcessingDepth reset), so they'll appear with
3753        // source table attribution (e.g., #sample.col1). This gives us dual attribution:
3754        // - #sample.col1 (from source subquery)
3755        // - (pivot-table:unpvt).col1 (as part of UNPIVOT output schema)
3756        if (sourceTable != null && sourceTable.getSubquery() != null && unpivotTable != null) {
3757            TSelectSqlStatement sourceSubquery = sourceTable.getSubquery();
3758            TResultColumnList resultCols = sourceSubquery.getResultColumnList();
3759            if (resultCols != null) {
3760                for (int i = 0; i < resultCols.size(); i++) {
3761                    TResultColumn rc = resultCols.getResultColumn(i);
3762                    if (rc == null) continue;
3763
3764                    // Get the column name from the result column
3765                    // Handle both simple columns and aliased expressions
3766                    String columnName = null;
3767                    if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) {
3768                        columnName = rc.getAliasClause().getAliasName().toString();
3769                    } else if (rc.getExpr() != null) {
3770                        TExpression expr = rc.getExpr();
3771                        if (expr.getExpressionType() == EExpressionType.simple_object_name_t &&
3772                            expr.getObjectOperand() != null) {
3773                            columnName = expr.getObjectOperand().getColumnNameOnly();
3774                        }
3775                    }
3776
3777                    if (columnName == null || columnName.isEmpty()) continue;
3778
3779                    // Check if this is a pass-through column (not consumed).
3780                    // S2: route through the same vendor-aware keyForColumn()
3781                    // used at the storage sites above. IdentifierService
3782                    // strips quotes per vendor (so the manual "[...]" / "\"...\""
3783                    // strip is no longer needed here).
3784                    String normalizedName = keyForColumn(columnName);
3785                    if (!consumedColumns.contains(normalizedName)) {
3786                        // This is a pass-through column - create a synthetic entry
3787                        // We use TObjectName.createObjectName() which properly initializes tokens
3788                        // so getColumnNameOnly() returns the correct value
3789                        TObjectName passthroughCol = TObjectName.createObjectName(
3790                            dbVendor,
3791                            EDbObjectType.column,
3792                            new TSourceToken(columnName)
3793                        );
3794                        passthroughCol.setSourceTable(unpivotTable);
3795
3796                        // Mark as definition column so it's not processed as a reference in isColumnReference()
3797                        unpivotDefinitionColumns.add(passthroughCol);
3798
3799                        // Add to output
3800                        allColumnReferences.add(passthroughCol);
3801
3802                        if (DEBUG_SCOPE_BUILD) {
3803                            System.out.println("[DEBUG] Added UNPIVOT pass-through column: " + columnName);
3804                        }
3805                    }
3806                }
3807            }
3808        }
3809    }
3810
3811    /**
3812     * Find the matching pivot table in the current statement's tables collection.
3813     *
3814     * The visitor traverses through getRelations() which may contain TTable objects
3815     * that are different from those in statement.tables. Since the formatter uses
3816     * object identity to match sourceTable against statement.tables, we need to
3817     * return the TTable from statement.tables.
3818     *
3819     * @param visitedTable The TTable from visitor traversal
3820     * @param alias The alias to match
3821     * @return The matching TTable from statement.tables, or visitedTable if not found
3822     */
3823    private TTable findMatchingPivotTableInStatement(TTable visitedTable, String alias) {
3824        // Get the current statement from the scope
3825        if (currentSelectScope == null || !(currentSelectScope.getNode() instanceof TSelectSqlStatement)) {
3826            return visitedTable;
3827        }
3828
3829        TSelectSqlStatement stmt = (TSelectSqlStatement) currentSelectScope.getNode();
3830        if (stmt.tables == null) {
3831            return visitedTable;
3832        }
3833
3834        // Search for a pivot table with matching alias in statement.tables
3835        for (int i = 0; i < stmt.tables.size(); i++) {
3836            TTable stmtTable = stmt.tables.getTable(i);
3837            if (stmtTable == null) continue;
3838
3839            // Check if this is a pivot table with matching alias
3840            if (stmtTable.getTableType() == ETableSource.pivoted_table) {
3841                String stmtAlias = getTableAlias(stmtTable);
3842                // Try to get alias from pivot clause if not found
3843                if ((stmtAlias == null || stmtAlias.isEmpty() || stmtAlias.startsWith("null"))
3844                    && stmtTable.getPivotedTable() != null
3845                    && stmtTable.getPivotedTable().getPivotClause() != null) {
3846                    TPivotClause pc = stmtTable.getPivotedTable().getPivotClause();
3847                    if (pc.getAliasClause() != null && pc.getAliasClause().getAliasName() != null) {
3848                        stmtAlias = pc.getAliasClause().getAliasName().toString();
3849                    }
3850                }
3851
3852                // Match by alias (case-insensitive)
3853                if (alias != null && stmtAlias != null && alias.equalsIgnoreCase(stmtAlias)) {
3854                    return stmtTable;
3855                }
3856            }
3857        }
3858
3859        // Not found in statement.tables, return the original visited table
3860        return visitedTable;
3861    }
3862
3863    /**
3864     * Find the actual pivot table with PivotedTable info from the statement's joins.
3865     *
3866     * SQL Server UNPIVOT issue: When UNPIVOT is used on a subquery, the table from
3867     * statement.tables may have tableType=pivoted_table but getPivotedTable()=null.
3868     * The actual pivot clause is on a different TTable object from statement.joins.
3869     *
3870     * @param table The pivot table from statement.tables (may have getPivotedTable()=null)
3871     * @return The matching TTable from joins with getPivotedTable() populated, or null if not found
3872     */
3873    private TTable findPivotTableInJoins(TTable table) {
3874        // Get the current statement from the scope
3875        if (currentSelectScope == null || !(currentSelectScope.getNode() instanceof TSelectSqlStatement)) {
3876            return null;
3877        }
3878
3879        TSelectSqlStatement stmt = (TSelectSqlStatement) currentSelectScope.getNode();
3880
3881        // Search in joins for a pivot table with getPivotedTable() != null
3882        TJoinList joins = stmt.joins;
3883        if (joins != null) {
3884            for (int i = 0; i < joins.size(); i++) {
3885                TJoin join = joins.getJoin(i);
3886                if (join == null) continue;
3887
3888                TTable joinTable = join.getTable();
3889                if (joinTable != null &&
3890                    joinTable.getTableType() == ETableSource.pivoted_table &&
3891                    joinTable.getPivotedTable() != null) {
3892                    // Found a pivot table with the actual PivotedTable info
3893                    // Match by alias if available
3894                    String tableAlias = getTableAlias(table);
3895                    String joinTableAlias = getTableAlias(joinTable);
3896
3897                    // If the table from statement.tables has an alias, try to match
3898                    if (tableAlias != null && !tableAlias.isEmpty()) {
3899                        // Try to get alias from pivot clause if joinTableAlias is null or has the
3900                        // placeholder pattern "null(piviot_table)" or similar
3901                        if ((joinTableAlias == null || joinTableAlias.isEmpty() ||
3902                             joinTableAlias.startsWith("null")) &&
3903                            joinTable.getPivotedTable().getPivotClause() != null) {
3904                            TPivotClause pc = joinTable.getPivotedTable().getPivotClause();
3905                            if (pc.getAliasClause() != null && pc.getAliasClause().getAliasName() != null) {
3906                                joinTableAlias = pc.getAliasClause().getAliasName().toString();
3907                            }
3908                        }
3909                        if (tableAlias.equalsIgnoreCase(joinTableAlias)) {
3910                            return joinTable;
3911                        }
3912                    } else {
3913                        // No alias to match, return the first pivot table found
3914                        return joinTable;
3915                    }
3916                }
3917            }
3918        }
3919
3920        return null;
3921    }
3922
3923    /**
3924     * Find a table by alias or name in the current SELECT statement's FROM clause.
3925     * Used for linking EXCEPT columns to the star column's source table.
3926     *
3927     * @param aliasOrName The table alias or name to find
3928     * @return The matching TTable, or null if not found
3929     */
3930    private TTable findTableByAliasInCurrentScope(String aliasOrName) {
3931        if (aliasOrName == null || aliasOrName.isEmpty()) {
3932            return null;
3933        }
3934
3935        // Get the current statement from the scope
3936        if (currentSelectScope == null || !(currentSelectScope.getNode() instanceof TSelectSqlStatement)) {
3937            return null;
3938        }
3939
3940        TSelectSqlStatement stmt = (TSelectSqlStatement) currentSelectScope.getNode();
3941        if (stmt.tables == null) {
3942            return null;
3943        }
3944
3945        // Search for a table with matching alias or name
3946        for (int i = 0; i < stmt.tables.size(); i++) {
3947            TTable table = stmt.tables.getTable(i);
3948            if (table == null) continue;
3949
3950            // Check alias first
3951            String tableAlias = getTableAlias(table);
3952            if (tableAlias != null && !tableAlias.isEmpty() &&
3953                aliasOrName.equalsIgnoreCase(tableAlias)) {
3954                return table;
3955            }
3956
3957            // Check table name
3958            String tableName = table.getTableName() != null ? table.getTableName().toString() : null;
3959            if (tableName != null && aliasOrName.equalsIgnoreCase(tableName)) {
3960                return table;
3961            }
3962
3963            // For subqueries without explicit alias, check the full name
3964            String fullName = table.getFullName();
3965            if (fullName != null && aliasOrName.equalsIgnoreCase(fullName)) {
3966                return table;
3967            }
3968        }
3969
3970        return null;
3971    }
3972
3973    /**
3974     * Resolve the source namespace for a PIVOT table (Delta 2 - pass-through column resolution).
3975     *
3976     * The source namespace is used to resolve pass-through columns that are not
3977     * part of the PIVOT IN clause (e.g., ADDRESS_ID in "SELECT p.ADDRESS_ID, p.[1] FROM CTE PIVOT(...) p").
3978     *
3979     * @param sourceTable The source table of the PIVOT
3980     * @return The namespace for the source table, or null if not found
3981     */
3982    private INamespace resolveSourceNamespaceForPivot(TTable sourceTable) {
3983        if (sourceTable == null) {
3984            return null;
3985        }
3986
3987        String sourceName = sourceTable.getName();
3988
3989        // Case 1: Source is a CTE reference. Apply the same Mantis #4412
3990        // guard so a schema-qualified PIVOT source, or a self-reference
3991        // inside its own non-recursive CTE definition, falls through to
3992        // the physical-table branch below.
3993        CTENamespace cteNamespace = findCTEByName(sourceName);
3994        if (cteNamespace != null && !shouldTreatAsPhysicalTableNotCTE(sourceTable, cteNamespace)) {
3995            return cteNamespace;
3996        }
3997
3998        // Case 2: Source is a subquery
3999        if (sourceTable.getSubquery() != null) {
4000            // Create a SubqueryNamespace for the subquery
4001            SubqueryNamespace subqueryNs = new SubqueryNamespace(
4002                sourceTable.getSubquery(),
4003                getTableAlias(sourceTable),
4004                nameMatcher
4005            );
4006            subqueryNs.validate();
4007            return subqueryNs;
4008        }
4009
4010        // Case 3: Source is a physical table - create TableNamespace
4011        TableNamespace tableNs = new TableNamespace(sourceTable, nameMatcher, sqlEnv);
4012        tableNs.validate();
4013        return tableNs;
4014    }
4015
4016    /**
4017     * Process an UNNEST table expression.
4018     *
4019     * UNNEST flattens an array into rows, creating a virtual table:
4020     * <pre>
4021     * SELECT value FROM UNNEST(array_column)
4022     * SELECT element FROM UNNEST(['a', 'b', 'c']) AS element
4023     * SELECT * FROM UNNEST(array_column) WITH OFFSET
4024     * </pre>
4025     *
4026     * The UNNEST table provides:
4027     * 1. An implicit column for the unnested elements (named by alias or 'value')
4028     * 2. Optional WITH OFFSET column for array indices
4029     * 3. STRUCT field columns when unnesting ARRAY<STRUCT<...>>
4030     */
4031    private void processUnnestTable(TTable table) {
4032        String alias = getTableAlias(table);
4033
4034        // Create UnnestNamespace
4035        UnnestNamespace unnestNs = new UnnestNamespace(table, alias, nameMatcher);
4036        unnestNs.validate();
4037
4038        // Add to FROM scope
4039        currentFromScope.addChild(unnestNs, alias, false);
4040        tableToNamespaceMap.put(table, unnestNs);  // Store for legacy compatibility
4041
4042        // Process the array expression inside UNNEST to capture correlated references
4043        // e.g., UNNEST(nested_attribute) - nested_attribute should link to outer table
4044        TUnnestClause unnestClause = table.getUnnestClause();
4045        if (unnestClause != null && unnestClause.getArrayExpr() != null) {
4046            // Visit the array expression to collect column references
4047            // This will be handled by the expression visitor
4048            traverseExpressionForColumns(unnestClause.getArrayExpr());
4049        }
4050
4051        if (DEBUG_SCOPE_BUILD) {
4052            System.out.println("[DEBUG] Added UNNEST table to FromScope: alias=" + alias +
4053                " implicitColumn=" + unnestNs.getImplicitColumnName());
4054        }
4055    }
4056
4057    /**
4058     * Process a Snowflake stage table reference.
4059     * Stage tables reference files in storage (e.g., @stage/path/file.parquet).
4060     * They're treated as tables but without schema metadata - columns are accessed
4061     * via positional references ($1, $2) or JSON path access ($1:field).
4062     */
4063    private void processStageTable(TTable table) {
4064        String alias = getTableAlias(table);
4065        String tableName = table.getFullName();
4066
4067        // Create a TableNamespace for the stage table
4068        // Stage tables don't have metadata, so we use null for sqlEnv
4069        TableNamespace stageNs = new TableNamespace(table, nameMatcher, null);
4070        stageNs.validate();
4071
4072        // Add to FROM scope
4073        currentFromScope.addChild(stageNs, alias, false);
4074        lastProcessedFromTable = table;
4075        currentJoinChainTables.add(table);  // Track ALL tables in join chain for chained USING
4076        tableToNamespaceMap.put(table, stageNs);
4077
4078        if (DEBUG_SCOPE_BUILD) {
4079            System.out.println("[DEBUG] Added stage table to FromScope: alias=" + alias +
4080                " tableName=" + tableName);
4081        }
4082    }
4083
4084    /**
4085     * Process a Teradata TD_UNPIVOT table function.
4086     * TD_UNPIVOT transforms columns into rows. The columns are created during parsing
4087     * in TTDUnpivot.doParse() and linked to either the output table (VALUE_COLUMNS,
4088     * UNPIVOT_COLUMN) or the source table (COLUMN_LIST).
4089     *
4090     * This method collects those columns into allColumnReferences as definition columns
4091     * (they don't need name resolution - their sourceTable is already set).
4092     *
4093     * @see gudusoft.gsqlparser.nodes.teradata.TTDUnpivot
4094     */
4095    private void processTDUnpivotTable(TTable table) {
4096        TTDUnpivot tdUnpivot = table.getTdUnpivot();
4097        if (tdUnpivot == null) {
4098            return;
4099        }
4100
4101        // VALUE_COLUMNS: Output value columns of TD_UNPIVOT
4102        // These columns are created from string literals and linked to the output table
4103        TObjectNameList valueColumns = tdUnpivot.getValueColumns();
4104        if (valueColumns != null) {
4105            for (int i = 0; i < valueColumns.size(); i++) {
4106                TObjectName col = valueColumns.getObjectName(i);
4107                if (col != null) {
4108                    // Mark as definition column (already has sourceTable set during parsing)
4109                    unpivotDefinitionColumns.add(col);
4110                    // Add to output list
4111                    allColumnReferences.add(col);
4112                    if (DEBUG_SCOPE_BUILD) {
4113                        System.out.println("[DEBUG] TD_UNPIVOT valueColumn: " + col.getColumnNameOnly() +
4114                            " -> " + (col.getSourceTable() != null ? col.getSourceTable().getTableName() : "null"));
4115                    }
4116                }
4117            }
4118        }
4119
4120        // UNPIVOT_COLUMN: Output label column of TD_UNPIVOT (contains original column names)
4121        TObjectNameList unpivotColumns = tdUnpivot.getUnpivotColumns();
4122        if (unpivotColumns != null) {
4123            for (int i = 0; i < unpivotColumns.size(); i++) {
4124                TObjectName col = unpivotColumns.getObjectName(i);
4125                if (col != null) {
4126                    // Mark as definition column
4127                    unpivotDefinitionColumns.add(col);
4128                    // Add to output list
4129                    allColumnReferences.add(col);
4130                    if (DEBUG_SCOPE_BUILD) {
4131                        System.out.println("[DEBUG] TD_UNPIVOT unpivotColumn: " + col.getColumnNameOnly() +
4132                            " -> " + (col.getSourceTable() != null ? col.getSourceTable().getTableName() : "null"));
4133                    }
4134                }
4135            }
4136        }
4137
4138        // COLUMN_LIST: Source columns being unpivoted
4139        // These columns are linked to the source table (the table in the ON clause)
4140        TObjectNameList columnList = tdUnpivot.getColumnList();
4141        if (columnList != null) {
4142            for (int i = 0; i < columnList.size(); i++) {
4143                TObjectName col = columnList.getObjectName(i);
4144                if (col != null) {
4145                    // Mark as definition column
4146                    unpivotDefinitionColumns.add(col);
4147                    // Add to output list
4148                    allColumnReferences.add(col);
4149                    if (DEBUG_SCOPE_BUILD) {
4150                        System.out.println("[DEBUG] TD_UNPIVOT columnList: " + col.getColumnNameOnly() +
4151                            " -> " + (col.getSourceTable() != null ? col.getSourceTable().getTableName() : "null"));
4152                    }
4153                }
4154            }
4155        }
4156    }
4157
4158    /**
4159     * Process a VALUES table (inline data with column aliases).
4160     * Example: VALUES (1, 'a'), (2, 'b') AS t(id, name)
4161     * Used in Teradata MERGE: USING VALUES (:empno, :name, :salary) AS s(empno, name, salary)
4162     */
4163    private void processValuesTable(TTable table) {
4164        String alias = getTableAlias(table);
4165
4166        // Track column name definitions from the alias clause
4167        // These are column DEFINITIONS, not references - should NOT be collected as column refs
4168        // e.g., in "VALUES (1, 'a') AS t(id, name)", 'id' and 'name' are definitions
4169        TAliasClause aliasClause = table.getAliasClause();
4170        if (aliasClause != null) {
4171            TObjectNameList columns = aliasClause.getColumns();
4172            if (columns != null && columns.size() > 0) {
4173                for (int i = 0; i < columns.size(); i++) {
4174                    TObjectName colName = columns.getObjectName(i);
4175                    if (colName != null) {
4176                        valuesTableAliasColumns.add(colName);
4177                        if (DEBUG_SCOPE_BUILD) {
4178                            System.out.println("[DEBUG] Tracked VALUES alias column definition (will skip): " + colName.toString());
4179                        }
4180                    }
4181                }
4182            }
4183        }
4184
4185        // Create ValuesNamespace - columns are extracted from alias clause
4186        ValuesNamespace valuesNs = new ValuesNamespace(table, alias, nameMatcher);
4187        valuesNs.validate();
4188
4189        // Add to FROM scope with the table alias
4190        currentFromScope.addChild(valuesNs, alias, false);
4191        tableToNamespaceMap.put(table, valuesNs);  // Store for legacy compatibility
4192        lastProcessedFromTable = table;
4193        currentJoinChainTables.add(table);  // Track ALL tables in join chain for chained USING
4194
4195        if (DEBUG_SCOPE_BUILD) {
4196            System.out.println("[DEBUG] Added VALUES table to FromScope: alias=" + alias +
4197                ", columns=" + valuesNs.getAllColumnSources().keySet());
4198        }
4199    }
4200
4201    /**
4202     * Process a potential CTE reference
4203     */
4204    private void processCTEReference(TTable table) {
4205        String tableName = table.getName();
4206        CTENamespace cteNamespace = findCTEByName(tableName);
4207
4208        if (cteNamespace != null) {
4209            String alias = getTableAlias(table);
4210            currentFromScope.addChild(cteNamespace, alias, false);
4211        }
4212    }
4213
4214    /**
4215     * Decide whether a table that name-matches an enclosing CTE should still be
4216     * treated as a physical table rather than as a CTE reference.
4217     *
4218     * <p>Two situations trigger this guard:
4219     *
4220     * <ol>
4221     *   <li>The table reference carries a schema, database, or server prefix
4222     *       (e.g. {@code ZZZ_DEV.FOTC_BB_PRODUCT_INTER}). CTE names are
4223     *       unqualified across every supported dialect, so a qualified name
4224     *       cannot be a CTE reference.</li>
4225     *   <li>The table appears inside the CTE's own definition and we can be
4226     *       <em>certain</em> the CTE is non-recursive. Non-recursive CTEs are
4227     *       not visible to themselves, so a same-named table inside the body
4228     *       is the underlying physical table, not a self-reference. See
4229     *       Mantis #4412.</li>
4230     * </ol>
4231     *
4232     * <p>The non-recursive determination is intentionally conservative because
4233     * {@code TCTE.isRecursive()} is unreliable across dialects:
4234     * <ul>
4235     *   <li>BigQuery's grammar accepts {@code WITH RECURSIVE} but never sets
4236     *       the flag.</li>
4237     *   <li>PostgreSQL/Snowflake/Redshift/Hive/Teradata only mark
4238     *       {@code getCTE(0)} as recursive even when {@code WITH RECURSIVE}
4239     *       applies to the whole list.</li>
4240     *   <li>SQL Server allows recursive CTEs without any keyword.</li>
4241     * </ul>
4242     * To stay safe we treat a CTE as "potentially recursive" — and therefore
4243     * do not reroute its self-references — whenever any sibling in the same
4244     * {@code TCTEList} is marked recursive, or whenever the CTE body is a
4245     * combined query (the typical recursive shape).
4246     */
4247    private boolean shouldTreatAsPhysicalTableNotCTE(TTable table, CTENamespace cteNamespace) {
4248        if (table == null || cteNamespace == null) {
4249            return false;
4250        }
4251
4252        // Case 1: schema/database/server-qualified name cannot match a CTE.
4253        if (hasSchemaOrDatabaseQualifier(table)) {
4254            return true;
4255        }
4256
4257        // Case 2: definitely-non-recursive CTE self-reference inside its body.
4258        return isNonRecursiveSelfReference(table, cteNamespace);
4259    }
4260
4261    private boolean isNonRecursiveSelfReference(TTable table, CTENamespace cteNamespace) {
4262        TCTE cte = cteNamespace.getCTE();
4263        if (cte == null || cte.getSubquery() == null) {
4264            return false;
4265        }
4266
4267        // Don't reroute when there's any sign the CTE could be recursive.
4268        if (isPotentiallyRecursive(cte)) {
4269            return false;
4270        }
4271
4272        TSelectSqlStatement cteSubquery = cte.getSubquery();
4273        return gudusoft.gsqlparser.nodes.TParseTreeNode.subNodeInNode(table, cteSubquery);
4274    }
4275
4276    /**
4277     * A CTE is treated as potentially recursive whenever
4278     * {@link TCTE#isRecursive()} is true on this CTE, on any sibling in the
4279     * same {@link TCTEList} (covers the
4280     * "{@code WITH RECURSIVE a AS (...), b AS (...)}" case where dialect
4281     * grammars mark only the first CTE), or whenever the CTE body is a
4282     * combined query — the standard recursive shape used by SQL Server,
4283     * which has no {@code RECURSIVE} keyword.
4284     */
4285    private boolean isPotentiallyRecursive(TCTE cte) {
4286        if (cte == null) {
4287            return false;
4288        }
4289        if (cte.isRecursive()) {
4290            return true;
4291        }
4292
4293        TCTEList list = findEnclosingCTEList(cte);
4294        if (list != null) {
4295            for (int i = 0; i < list.size(); i++) {
4296                TCTE sibling = list.getCTE(i);
4297                if (sibling != null && sibling.isRecursive()) {
4298                    return true;
4299                }
4300            }
4301        }
4302
4303        TSelectSqlStatement body = cte.getSubquery();
4304        if (body != null && body.isCombinedQuery()) {
4305            return true;
4306        }
4307
4308        return false;
4309    }
4310
4311    /**
4312     * Walk the live scope stack to find the {@link TCTEList} that contains
4313     * {@code cte}. We avoid using {@link gudusoft.gsqlparser.nodes.TParseTreeNode#getParent()}
4314     * because TCTE does not always carry a parent pointer back to its list.
4315     */
4316    private TCTEList findEnclosingCTEList(TCTE cte) {
4317        if (cte == null) {
4318            return null;
4319        }
4320        for (int i = scopeStack.size() - 1; i >= 0; i--) {
4321            IScope scope = scopeStack.get(i);
4322            if (scope instanceof CTEScope) {
4323                TCTEList list = ((CTEScope) scope).getCTEList();
4324                if (list == null) {
4325                    continue;
4326                }
4327                for (int j = 0; j < list.size(); j++) {
4328                    if (list.getCTE(j) == cte) {
4329                        return list;
4330                    }
4331                }
4332            }
4333        }
4334        return null;
4335    }
4336
4337    /**
4338     * Returns true when the table reference has any non-empty
4339     * server/database/schema prefix, e.g. {@code db.schema.t} or
4340     * {@code schema.t}.
4341     */
4342    private boolean hasSchemaOrDatabaseQualifier(TTable table) {
4343        if (table == null) {
4344            return false;
4345        }
4346        String schema = table.getPrefixSchema();
4347        if (schema != null && !schema.isEmpty()) {
4348            return true;
4349        }
4350        String database = table.getPrefixDatabase();
4351        if (database != null && !database.isEmpty()) {
4352            return true;
4353        }
4354        String server = table.getPrefixServer();
4355        if (server != null && !server.isEmpty()) {
4356            return true;
4357        }
4358        return false;
4359    }
4360
4361    /**
4362     * Find a CTE by name in all enclosing CTE scopes
4363     */
4364    private CTENamespace findCTEByName(String name) {
4365        if (name == null) {
4366            return null;
4367        }
4368
4369        // Normalize name by stripping SQL Server bracket delimiters [name] -> name
4370        String normalizedName = stripBrackets(name);
4371
4372        // Search through scope stack for CTE scopes
4373        for (int i = scopeStack.size() - 1; i >= 0; i--) {
4374            IScope scope = scopeStack.get(i);
4375            if (scope instanceof CTEScope) {
4376                CTEScope cteScope = (CTEScope) scope;
4377                CTENamespace cte = cteScope.getCTE(normalizedName);
4378                if (cte != null) {
4379                    return cte;
4380                }
4381            }
4382        }
4383
4384        return null;
4385    }
4386
4387    /**
4388     * Strip SQL Server bracket delimiters from a name.
4389     * Converts "[name]" to "name", leaves unbracketed names unchanged.
4390     */
4391    private String stripBrackets(String name) {
4392        if (name == null) {
4393            return null;
4394        }
4395        if (name.startsWith("[") && name.endsWith("]") && name.length() > 2) {
4396            return name.substring(1, name.length() - 1);
4397        }
4398        return name;
4399    }
4400
4401    /**
4402     * Strip string delimiters (double quotes and single quotes) from a constant value.
4403     * Used for PIVOT IN clause constant values like "HOUSTON" or 'value'.
4404     * SQL Server brackets are preserved as they're often needed for special names.
4405     */
4406    private String stripStringDelimiters(String value) {
4407        if (value == null || value.isEmpty()) {
4408            return value;
4409        }
4410        // Strip double quotes (BigQuery style identifier)
4411        if (value.startsWith("\"") && value.endsWith("\"") && value.length() > 2) {
4412            return value.substring(1, value.length() - 1);
4413        }
4414        // Strip single quotes (string literal)
4415        if (value.startsWith("'") && value.endsWith("'") && value.length() > 2) {
4416            return value.substring(1, value.length() - 1);
4417        }
4418        // SQL Server brackets [] are preserved as they're part of the column identity
4419        return value;
4420    }
4421
4422    /**
4423     * Get the alias for a table, or the table name if no alias
4424     */
4425    private String getTableAlias(TTable table) {
4426        if (table.getAliasName() != null && !table.getAliasName().isEmpty()) {
4427            return table.getAliasName();
4428        }
4429        return table.getName();
4430    }
4431
4432    // ========== JOIN ==========
4433
4434    @Override
4435    public void preVisit(TJoinExpr joinExpr) {
4436        // JOIN expressions are traversed automatically
4437        // Left and right tables will trigger preVisit(TTable)
4438        // ON condition columns will trigger preVisit(TObjectName)
4439        if (DEBUG_SCOPE_BUILD) {
4440            System.out.println("[DEBUG] preVisit(TJoinExpr): " + joinExpr +
4441                               ", usingColumns=" + (joinExpr.getUsingColumns() != null ? joinExpr.getUsingColumns().size() : "null") +
4442                               ", leftTable=" + joinExpr.getLeftTable() +
4443                               ", rightTable=" + joinExpr.getRightTable() +
4444                               ", joinChainTables=" + currentJoinChainTables.size());
4445        }
4446
4447        // Handle USING columns in TJoinExpr (used when USE_JOINEXPR_INSTEAD_OF_JOIN is true)
4448        if (joinExpr.getUsingColumns() != null && joinExpr.getUsingColumns().size() > 0) {
4449            TTable rightTable = joinExpr.getRightTable();
4450
4451            // For TJoinExpr, the left side may be another TJoinExpr (nested joins) or a single table.
4452            // We need to collect ALL tables on the left side recursively.
4453            List<TTable> leftSideTables = collectTablesFromJoinExpr(joinExpr.getLeftTable());
4454
4455            if (DEBUG_SCOPE_BUILD) {
4456                System.out.println("[DEBUG] preVisit(TJoinExpr) USING: leftSideTables=" + leftSideTables.size());
4457                for (TTable t : leftSideTables) {
4458                    System.out.println("[DEBUG]   - " + t.getFullName());
4459                }
4460            }
4461
4462            if (rightTable != null) {
4463                currentUsingJoinRightTable = rightTable;
4464                // USING clause semantic: For chained joins like "t1 JOIN t2 USING (c1) JOIN t3 USING (c2)",
4465                // the USING column c2 should be linked to ALL tables on the left side (t1 and t2), not just one.
4466                // This is because the left side of the second join is the result of (t1 JOIN t2).
4467                for (int i = 0; i < joinExpr.getUsingColumns().size(); i++) {
4468                    TObjectName usingCol = joinExpr.getUsingColumns().getObjectName(i);
4469                    if (usingCol != null) {
4470                        // Create synthetic columns for ALL tables on the left side
4471                        // The first table gets the original USING column, the rest get clones
4472                        boolean isFirstTable = true;
4473                        for (TTable leftTable : leftSideTables) {
4474                            if (isFirstTable) {
4475                                // Original USING column -> first left table
4476                                usingColumnToLeftTable.put(usingCol, leftTable);
4477                                isFirstTable = false;
4478                            } else {
4479                                // Create synthetic column for additional left tables
4480                                TObjectName chainTableCol = usingCol.clone();
4481                                chainTableCol.setSourceTable(leftTable);
4482
4483                                // Add the synthetic chain table column to references
4484                                if (currentSelectScope != null) {
4485                                    columnToScopeMap.put(chainTableCol, currentSelectScope);
4486                                }
4487                                allColumnReferences.add(chainTableCol);
4488
4489                                // Track in USING map for resolution
4490                                usingColumnToLeftTable.put(chainTableCol, leftTable);
4491
4492                                if (DEBUG_SCOPE_BUILD) {
4493                                    System.out.println("[DEBUG] Created synthetic USING column for left table: " +
4494                                                       usingCol.getColumnNameOnly() + " -> " + leftTable.getFullName());
4495                                }
4496                            }
4497                        }
4498
4499                        // Create a synthetic column reference for the right table
4500                        // Clone it and set the clone's sourceTable to right table
4501                        TObjectName rightTableCol = usingCol.clone();
4502                        rightTableCol.setSourceTable(rightTable);
4503
4504                        // Add the synthetic right table column to references
4505                        if (currentSelectScope != null) {
4506                            columnToScopeMap.put(rightTableCol, currentSelectScope);
4507                        }
4508                        allColumnReferences.add(rightTableCol);
4509
4510                        // ONLY track the synthetic column in USING map for right table resolution
4511                        usingColumnToRightTable.put(rightTableCol, rightTable);
4512                    }
4513                }
4514            }
4515        }
4516    }
4517
4518    /**
4519     * Collect all tables from a TTable that might be a join structure.
4520     * If the table is a join (type=join), recursively collect tables from the join tree.
4521     * If it's a simple table (objectname), return just that table.
4522     */
4523    private List<TTable> collectTablesFromJoinExpr(TTable table) {
4524        List<TTable> tables = new ArrayList<>();
4525        if (table == null) {
4526            return tables;
4527        }
4528
4529        if (table.getTableType() == ETableSource.join && table.getJoinExpr() != null) {
4530            // This is a join structure - recursively collect from both sides
4531            TJoinExpr joinExpr = table.getJoinExpr();
4532            tables.addAll(collectTablesFromJoinExpr(joinExpr.getLeftTable()));
4533            tables.addAll(collectTablesFromJoinExpr(joinExpr.getRightTable()));
4534        } else {
4535            // Simple table - add it
4536            tables.add(table);
4537        }
4538
4539        return tables;
4540    }
4541
4542    @Override
4543    public void preVisit(TJoinItem joinItem) {
4544        // Track the right-side table for JOIN...USING column resolution priority
4545        // In "a JOIN table2 USING (id)", joinItem.getTable() is table2 (the right side)
4546        if (DEBUG_SCOPE_BUILD) {
4547            System.out.println("[DEBUG] preVisit(TJoinItem): " + joinItem +
4548                               ", usingColumns=" + (joinItem.getUsingColumns() != null ? joinItem.getUsingColumns().size() : "null") +
4549                               ", lastProcessedFromTable=" + lastProcessedFromTable +
4550                               ", joinChainTables=" + currentJoinChainTables.size());
4551        }
4552        if (joinItem.getUsingColumns() != null && joinItem.getUsingColumns().size() > 0) {
4553            TTable rightTable = joinItem.getTable();
4554            TTable leftTable = lastProcessedFromTable;  // The table processed before this JOIN
4555            if (rightTable != null) {
4556                currentUsingJoinRightTable = rightTable;
4557                // USING clause semantic: For chained joins like "t1 JOIN t2 USING (c1) JOIN t3 USING (c2)",
4558                // the USING column c2 should be linked to ALL tables on the left side (t1 and t2), not just t2.
4559                // This is because the left side of the second join is the result of (t1 JOIN t2).
4560                for (int i = 0; i < joinItem.getUsingColumns().size(); i++) {
4561                    TObjectName usingCol = joinItem.getUsingColumns().getObjectName(i);
4562                    if (usingCol != null) {
4563                        // Track original USING column -> immediate left table (for reference only)
4564                        // This is the primary left table association
4565                        if (leftTable != null) {
4566                            usingColumnToLeftTable.put(usingCol, leftTable);
4567                        }
4568
4569                        // Create synthetic columns for ALL tables in the join chain (left side of this join)
4570                        // This ensures that in "t1 JOIN t2 USING (c1) JOIN t3 USING (c2)", column c2
4571                        // is linked to both t1 and t2, not just t2.
4572                        for (TTable chainTable : currentJoinChainTables) {
4573                            if (chainTable != leftTable) {  // Skip leftTable - handled by original usingCol
4574                                TObjectName chainTableCol = usingCol.clone();
4575                                chainTableCol.setSourceTable(chainTable);
4576
4577                                // Add the synthetic chain table column to references
4578                                if (currentSelectScope != null) {
4579                                    columnToScopeMap.put(chainTableCol, currentSelectScope);
4580                                }
4581                                allColumnReferences.add(chainTableCol);
4582
4583                                // Track in USING map for resolution
4584                                usingColumnToLeftTable.put(chainTableCol, chainTable);
4585
4586                                if (DEBUG_SCOPE_BUILD) {
4587                                    System.out.println("[DEBUG] Created synthetic USING column for chain table: " +
4588                                                       usingCol.getColumnNameOnly() + " -> " + chainTable.getFullName());
4589                                }
4590                            }
4591                        }
4592
4593                        // Create a synthetic column reference for the right table
4594                        // The original usingCol has sourceTable = left table (set by parser)
4595                        // Clone it and set the clone's sourceTable to right table
4596                        TObjectName rightTableCol = usingCol.clone();
4597                        rightTableCol.setSourceTable(rightTable);
4598
4599                        // Add the synthetic right table column to references
4600                        if (currentSelectScope != null) {
4601                            columnToScopeMap.put(rightTableCol, currentSelectScope);
4602                        }
4603                        allColumnReferences.add(rightTableCol);
4604
4605                        // ONLY track the synthetic column in USING map for right table resolution
4606                        // Do NOT add the original usingCol - it should keep its left table resolution
4607                        usingColumnToRightTable.put(rightTableCol, rightTable);
4608                    }
4609                }
4610            }
4611        }
4612    }
4613
4614    @Override
4615    public void postVisit(TJoinItem joinItem) {
4616        // Clear the current USING join right table after processing
4617        if (joinItem.getUsingColumns() != null && joinItem.getUsingColumns().size() > 0) {
4618            currentUsingJoinRightTable = null;
4619        }
4620    }
4621
4622    // ========== Expressions ==========
4623
4624    @Override
4625    public void preVisit(TExpression expression) {
4626        // Handle function expressions (type function_t) - the visitor may not automatically
4627        // traverse to getFunctionCall() and its arguments
4628        if (expression.getExpressionType() == EExpressionType.function_t &&
4629            expression.getFunctionCall() != null) {
4630            TFunctionCall func = expression.getFunctionCall();
4631
4632            // Handle STRUCT and similar functions that store field values in getFieldValues()
4633            if (func.getFieldValues() != null && func.getFieldValues().size() > 0) {
4634                for (int i = 0; i < func.getFieldValues().size(); i++) {
4635                    TResultColumn fieldValue = func.getFieldValues().getResultColumn(i);
4636                    if (fieldValue != null && fieldValue.getExpr() != null) {
4637                        traverseExpressionForColumns(fieldValue.getExpr());
4638                    }
4639                }
4640            }
4641
4642            // Handle regular functions with getArgs() (e.g., TO_JSON_STRING, ARRAY_LENGTH, etc.)
4643            if (func.getArgs() != null && func.getArgs().size() > 0) {
4644                for (int i = 0; i < func.getArgs().size(); i++) {
4645                    TExpression argExpr = func.getArgs().getExpression(i);
4646                    if (argExpr != null) {
4647                        traverseExpressionForColumns(argExpr);
4648                    }
4649                }
4650            }
4651
4652            // Handle special function expressions (CAST, CONVERT, EXTRACT, etc.)
4653            // These functions store their arguments in expr1/expr2/expr3 instead of args
4654            if (func.getExpr1() != null) {
4655                traverseExpressionForColumns(func.getExpr1());
4656            }
4657            if (func.getExpr2() != null) {
4658                traverseExpressionForColumns(func.getExpr2());
4659            }
4660            if (func.getExpr3() != null) {
4661                traverseExpressionForColumns(func.getExpr3());
4662            }
4663        }
4664
4665        // Handle array expressions - objectOperand contains the column reference
4666        if (expression.getExpressionType() == EExpressionType.array_t &&
4667            expression.getObjectOperand() != null) {
4668            preVisit(expression.getObjectOperand());
4669        }
4670
4671        // Handle array access expressions (e.g., str2['ptype'] in SparkSQL/Hive)
4672        // The column reference is in the LeftOperand
4673        if (expression.getExpressionType() == EExpressionType.array_access_expr_t) {
4674            if (expression.getLeftOperand() != null) {
4675                traverseExpressionForColumns(expression.getLeftOperand());
4676            }
4677        }
4678
4679        // Handle CASE expressions - traverse all sub-expressions to collect column references
4680        if (expression.getExpressionType() == EExpressionType.case_t &&
4681            expression.getCaseExpression() != null) {
4682            if (DEBUG_SCOPE_BUILD) {
4683                System.out.println("[DEBUG] preVisit(TExpression): Found CASE expression, currentSelectScope=" +
4684                    (currentSelectScope != null ? "set" : "null"));
4685            }
4686            traverseExpressionForColumns(expression);
4687        }
4688
4689        // Handle lambda expressions - mark parameters as NOT column references
4690        // Lambda parameters are local function parameters, not table column references
4691        // e.g., in "aggregate(array, 0, (acc, x) -> acc + x)", acc and x are lambda parameters
4692        if (expression.getExpressionType() == EExpressionType.lambda_t) {
4693            TExpression paramExpr = expression.getLeftOperand();
4694            TExpression bodyExpr = expression.getRightOperand();
4695            if (paramExpr != null) {
4696                // Collect parameter names first
4697                Set<String> paramNames = new HashSet<>();
4698                collectLambdaParameterNames(paramExpr, paramNames);
4699
4700                // Push parameter names onto the stack for use in preVisit(TObjectName)
4701                lambdaParameterStack.push(paramNames);
4702
4703                // Mark the parameter definition TObjectNames
4704                collectLambdaParameterObjects(paramExpr);
4705
4706                // Mark all usages in the body that match parameter names
4707                if (bodyExpr != null && !paramNames.isEmpty()) {
4708                    markLambdaParameterUsages(bodyExpr, paramNames);
4709                }
4710            }
4711        }
4712
4713        // Handle typecast expressions - the visitor may not automatically traverse the left operand
4714        // e.g., for "$1:apMac::string", we need to traverse "$1:apMac" to collect the column reference
4715        if (expression.getExpressionType() == EExpressionType.typecast_t) {
4716            if (expression.getLeftOperand() != null) {
4717                traverseExpressionForColumns(expression.getLeftOperand());
4718            }
4719        }
4720
4721        // Handle named argument expressions (e.g., "INPUT => value" in Snowflake FLATTEN)
4722        // The left operand is the parameter name, NOT a column reference.
4723        // Mark it with ttobjNamedArgParameter objectType so all downstream consumers skip it.
4724        if (expression.getExpressionType() == EExpressionType.assignment_t) {
4725            TExpression leftOp = expression.getLeftOperand();
4726            if (leftOp != null &&
4727                leftOp.getExpressionType() == EExpressionType.simple_object_name_t &&
4728                leftOp.getObjectOperand() != null) {
4729                TObjectName paramName = leftOp.getObjectOperand();
4730                // Set the objectType to mark this as a named argument parameter
4731                // This marking persists on the AST node and will be respected by
4732                // all downstream consumers (resolver, data lineage analyzer, etc.)
4733                paramName.setObjectType(TObjectName.ttobjNamedArgParameter);
4734                namedArgumentParameters.add(paramName);
4735                if (DEBUG_SCOPE_BUILD) {
4736                    System.out.println("[DEBUG] Marked named argument parameter: " +
4737                        paramName.toString() + " with objectType=" + TObjectName.ttobjNamedArgParameter);
4738                }
4739            }
4740        }
4741    }
4742
4743    @Override
4744    public void postVisit(TExpression expression) {
4745        // Pop lambda parameter context when exiting a lambda expression
4746        if (expression.getExpressionType() == EExpressionType.lambda_t) {
4747            TExpression paramExpr = expression.getLeftOperand();
4748            if (paramExpr != null && !lambdaParameterStack.isEmpty()) {
4749                lambdaParameterStack.pop();
4750            }
4751        }
4752    }
4753
4754    /**
4755     * Recursively collect lambda parameter NAMES as strings.
4756     */
4757    private void collectLambdaParameterNames(TExpression paramExpr, Set<String> paramNames) {
4758        if (paramExpr == null) return;
4759
4760        // Single parameter: expression has objectOperand
4761        if (paramExpr.getObjectOperand() != null) {
4762            String name = paramExpr.getObjectOperand().toString();
4763            if (name != null && !name.isEmpty()) {
4764                paramNames.add(name.toLowerCase());
4765            }
4766            return;
4767        }
4768
4769        // Multiple parameters: expression has exprList
4770        if (paramExpr.getExprList() != null) {
4771            for (int i = 0; i < paramExpr.getExprList().size(); i++) {
4772                TExpression e = paramExpr.getExprList().getExpression(i);
4773                collectLambdaParameterNames(e, paramNames);
4774            }
4775        }
4776    }
4777
4778    /**
4779     * Recursively collect all TObjectName nodes from lambda parameter definitions.
4780     */
4781    private void collectLambdaParameterObjects(TExpression paramExpr) {
4782        if (paramExpr == null) return;
4783
4784        if (paramExpr.getObjectOperand() != null) {
4785            lambdaParameters.add(paramExpr.getObjectOperand());
4786            return;
4787        }
4788
4789        if (paramExpr.getExprList() != null) {
4790            for (int i = 0; i < paramExpr.getExprList().size(); i++) {
4791                TExpression e = paramExpr.getExprList().getExpression(i);
4792                collectLambdaParameterObjects(e);
4793            }
4794        }
4795    }
4796
4797    /**
4798     * Recursively find and mark all TObjectName usages in a lambda body that match parameter names.
4799     */
4800    private void markLambdaParameterUsages(TExpression bodyExpr, Set<String> paramNames) {
4801        if (bodyExpr == null) return;
4802
4803        // Use iterative DFS to avoid StackOverflowError for deeply nested expression chains
4804        Deque<TExpression> stack = new ArrayDeque<>();
4805        stack.push(bodyExpr);
4806        while (!stack.isEmpty()) {
4807            TExpression current = stack.pop();
4808            if (current == null) continue;
4809
4810            // Check if this expression is a simple column reference matching a parameter name
4811            if (current.getObjectOperand() != null) {
4812                TObjectName objName = current.getObjectOperand();
4813                String name = objName.toString();
4814                if (name != null && paramNames.contains(name.toLowerCase())) {
4815                    lambdaParameters.add(objName);
4816                }
4817            }
4818
4819            // Push sub-expressions onto stack (right first so left is processed first)
4820            if (current.getRightOperand() != null) {
4821                stack.push(current.getRightOperand());
4822            }
4823            if (current.getLeftOperand() != null) {
4824                stack.push(current.getLeftOperand());
4825            }
4826            if (current.getExprList() != null) {
4827                for (int i = current.getExprList().size() - 1; i >= 0; i--) {
4828                    stack.push(current.getExprList().getExpression(i));
4829                }
4830            }
4831
4832            // Handle CASE expressions
4833            if (current.getCaseExpression() != null) {
4834                TCaseExpression caseExpr = current.getCaseExpression();
4835                if (caseExpr.getElse_expr() != null) {
4836                    stack.push(caseExpr.getElse_expr());
4837                }
4838                if (caseExpr.getWhenClauseItemList() != null) {
4839                    for (int i = caseExpr.getWhenClauseItemList().size() - 1; i >= 0; i--) {
4840                        TWhenClauseItem item = caseExpr.getWhenClauseItemList().getWhenClauseItem(i);
4841                        if (item.getReturn_expr() != null) {
4842                            stack.push(item.getReturn_expr());
4843                        }
4844                        if (item.getComparison_expr() != null) {
4845                            stack.push(item.getComparison_expr());
4846                        }
4847                    }
4848                }
4849                if (caseExpr.getInput_expr() != null) {
4850                    stack.push(caseExpr.getInput_expr());
4851                }
4852            }
4853
4854            // Handle function calls
4855            if (current.getFunctionCall() != null) {
4856                TFunctionCall func = current.getFunctionCall();
4857                if (func.getArgs() != null) {
4858                    for (int i = func.getArgs().size() - 1; i >= 0; i--) {
4859                        stack.push(func.getArgs().getExpression(i));
4860                    }
4861                }
4862            }
4863        }
4864    }
4865
4866    // ========== Function Calls ==========
4867
4868    @Override
4869    public void preVisit(TFunctionCall functionCall) {
4870        // Handle SQL Server UPDATE() function in trigger context
4871        // UPDATE(column_name) is used in triggers to check if a column was updated
4872        // The column argument should be resolved to the trigger target table
4873        if ((dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) &&
4874            currentTriggerTargetTable != null &&
4875            functionCall.getFunctionName() != null) {
4876            String funcName = functionCall.getFunctionName().toString();
4877            if ("update".equalsIgnoreCase(funcName) || "columns_updated".equalsIgnoreCase(funcName)) {
4878                // UPDATE(column) - link the column argument to trigger target table
4879                if (functionCall.getArgs() != null && functionCall.getArgs().size() == 1) {
4880                    TExpression argExpr = functionCall.getArgs().getExpression(0);
4881                    if (argExpr != null && argExpr.getObjectOperand() != null) {
4882                        TObjectName columnName = argExpr.getObjectOperand();
4883                        columnName.setSourceTable(currentTriggerTargetTable);
4884
4885                        // Add to allColumnReferences if not already there
4886                        if (!allColumnReferences.contains(columnName)) {
4887                            allColumnReferences.add(columnName);
4888                        }
4889
4890                        // Map the column to the current scope
4891                        IScope currentScope = scopeStack.isEmpty() ? globalScope : scopeStack.peek();
4892                        columnToScopeMap.put(columnName, currentScope);
4893
4894                        if (DEBUG_SCOPE_BUILD) {
4895                            System.out.println("[DEBUG] preVisit(TFunctionCall/update): " +
4896                                columnName + " -> " + currentTriggerTargetTable.getFullName());
4897                        }
4898                    }
4899                }
4900            }
4901        }
4902
4903        // Mark keyword arguments in built-in functions so they are not treated as column references.
4904        // For example, SECOND in TIMESTAMP_DIFF(ts1, ts2, SECOND) should not be collected as a column.
4905        // We do this in preVisit because TObjectName nodes are visited after TFunctionCall.
4906        markFunctionKeywordArguments(functionCall);
4907
4908        // Handle special functions like STRUCT that store field values in getFieldValues()
4909        // instead of getArgs(). The visitor pattern may not automatically traverse these.
4910        if (functionCall.getFieldValues() != null && functionCall.getFieldValues().size() > 0) {
4911            // STRUCT and similar functions - traverse the field values to collect column references
4912            for (int i = 0; i < functionCall.getFieldValues().size(); i++) {
4913                TResultColumn fieldValue = functionCall.getFieldValues().getResultColumn(i);
4914                if (fieldValue != null && fieldValue.getExpr() != null) {
4915                    // Manually traverse the field value expression
4916                    traverseExpressionForColumns(fieldValue.getExpr());
4917                }
4918            }
4919        }
4920
4921        // Handle special function expressions (CAST, CONVERT, EXTRACT, etc.)
4922        // These functions store their arguments in expr1/expr2/expr3 instead of args
4923        // The visitor pattern may not automatically traverse these expressions.
4924        if (functionCall.getExpr1() != null) {
4925            traverseExpressionForColumns(functionCall.getExpr1());
4926        }
4927        if (functionCall.getExpr2() != null) {
4928            traverseExpressionForColumns(functionCall.getExpr2());
4929        }
4930        if (functionCall.getExpr3() != null) {
4931            traverseExpressionForColumns(functionCall.getExpr3());
4932        }
4933
4934        // Handle XML functions that store their arguments in special properties
4935        // These functions don't use getArgs() but have dedicated value expression lists
4936        // XMLELEMENT: getXMLElementValueExprList() contains the value expressions
4937        // XMLFOREST: getXMLForestValueList() contains the value expressions
4938        // XMLQUERY/XMLEXISTS: getXmlPassingClause().getPassingList() contains column refs
4939        // XMLAttributes: getXMLAttributesClause().getValueExprList() contains attributes
4940        if (functionCall.getXMLElementValueExprList() != null) {
4941            TResultColumnList xmlValueList = functionCall.getXMLElementValueExprList();
4942            for (int i = 0; i < xmlValueList.size(); i++) {
4943                TResultColumn rc = xmlValueList.getResultColumn(i);
4944                if (rc != null && rc.getExpr() != null) {
4945                    traverseExpressionForColumns(rc.getExpr());
4946                }
4947            }
4948        }
4949        if (functionCall.getXMLForestValueList() != null) {
4950            TResultColumnList xmlForestList = functionCall.getXMLForestValueList();
4951            for (int i = 0; i < xmlForestList.size(); i++) {
4952                TResultColumn rc = xmlForestList.getResultColumn(i);
4953                if (rc != null && rc.getExpr() != null) {
4954                    traverseExpressionForColumns(rc.getExpr());
4955                }
4956            }
4957        }
4958        if (functionCall.getXmlPassingClause() != null &&
4959            functionCall.getXmlPassingClause().getPassingList() != null) {
4960            TResultColumnList passingList = functionCall.getXmlPassingClause().getPassingList();
4961            for (int i = 0; i < passingList.size(); i++) {
4962                TResultColumn rc = passingList.getResultColumn(i);
4963                if (rc != null && rc.getExpr() != null) {
4964                    traverseExpressionForColumns(rc.getExpr());
4965                }
4966            }
4967        }
4968        if (functionCall.getXMLAttributesClause() != null &&
4969            functionCall.getXMLAttributesClause().getValueExprList() != null) {
4970            TResultColumnList attrList = functionCall.getXMLAttributesClause().getValueExprList();
4971            for (int i = 0; i < attrList.size(); i++) {
4972                TResultColumn rc = attrList.getResultColumn(i);
4973                if (rc != null && rc.getExpr() != null) {
4974                    traverseExpressionForColumns(rc.getExpr());
4975                }
4976            }
4977        }
4978        // Handle XMLCAST/XMLQUERY typeExpression - stores the inner expression to cast
4979        // For XMLCAST(expr AS type), the expr is stored in typeExpression
4980        if (functionCall.getTypeExpression() != null) {
4981            traverseExpressionForColumns(functionCall.getTypeExpression());
4982        }
4983
4984        // Skip if this is a table-valued function (from FROM clause)
4985        // Table functions like [exce].[sampleTable]() should not be treated as column.method()
4986        if (tableValuedFunctionCalls.contains(functionCall)) {
4987            return;
4988        }
4989
4990        // Handle OGC/spatial/CLR method calls on columns (SQL Server specific)
4991        // These can be in two forms:
4992        // 1. table.column.method() - 3-part name where:
4993        //    - databaseToken = table alias (e.g., "ad")
4994        //    - schemaToken = column name (e.g., "SpatialLocation")
4995        //    - objectToken = method name (e.g., "STDistance")
4996        //
4997        // 2. column.method() - 2-part name where:
4998        //    - schemaToken = column name (e.g., "SpatialLocation")
4999        //    - objectToken = method name (e.g., "STDistance")
5000        //    - databaseToken = null
5001        //    In this case, if there's only one table in FROM, infer the column belongs to it
5002        //
5003        // NOTE: This is SQL Server specific because Oracle uses schema.function() syntax
5004        // for package calls (e.g., DBMS_OUTPUT.PUT_LINE, ERRLOG.LOAD_ERR_DTL).
5005        //
5006        // NOTE: We must skip static type methods like "geography::STGeomFromText()"
5007        // These use the :: syntax and the "schemaToken" is actually a type name, not a column.
5008        //
5009        // We extract the column reference and link it to the source table
5010        // Only apply column method handling for SQL Server (not Oracle, etc.)
5011        if (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) {
5012            TObjectName funcName = functionCall.getFunctionName();
5013
5014            if (DEBUG_SCOPE_BUILD) {
5015                System.out.println("[DEBUG] preVisit(TFunctionCall): " + functionCall);
5016                System.out.println("[DEBUG]   funcName: " + funcName);
5017                if (funcName != null) {
5018                    System.out.println("[DEBUG]   schemaToken: " + funcName.getSchemaToken());
5019                    System.out.println("[DEBUG]   databaseToken: " + funcName.getDatabaseToken());
5020                    System.out.println("[DEBUG]   objectToken: " + funcName.getObjectToken());
5021                    System.out.println("[DEBUG]   currentFromScope: " + (currentFromScope != null ? "exists" : "null"));
5022                }
5023            }
5024
5025            if (funcName != null) {
5026                String funcNameStr = funcName.toString();
5027
5028                // Skip static type methods like "geography::STGeomFromText()"
5029                // These are identified by the "::" in the function name string
5030                if (funcNameStr != null && funcNameStr.contains("::")) {
5031                    if (DEBUG_SCOPE_BUILD) {
5032                        System.out.println("[DEBUG]   Skipping static type method: " + funcNameStr);
5033                    }
5034                    return; // Don't process static type methods as column references
5035                }
5036
5037                if (funcName.getSchemaToken() != null) {
5038                    // schemaToken might be a column name (column.method()) or a schema name (schema.function())
5039                    // We distinguish by:
5040                    // 1. First checking SQLEnv if the full function name exists as a registered function
5041                    // 2. Then checking if the first part is a known SQL Server system schema name
5042                    String possibleColumnOrSchema = funcName.getSchemaToken().toString();
5043
5044                    // Check SQLEnv first - if the function is registered, this is schema.function()
5045                    // This handles user-defined functions with custom schema names (e.g., dbo1.ufnGetInventoryStock)
5046                    if (sqlEnv != null && sqlEnv.searchFunction(funcNameStr) != null) {
5047                        if (DEBUG_SCOPE_BUILD) {
5048                            System.out.println("[DEBUG]   Skipping schema.function() call (found in SQLEnv): " + funcNameStr);
5049                        }
5050                        return; // Don't treat as column.method()
5051                    }
5052
5053                    // Also check for known SQL Server system schema names as a fallback
5054                    // (for functions not explicitly created in this batch but are system/built-in)
5055                    if (isSqlServerSchemaName(possibleColumnOrSchema)) {
5056                        if (DEBUG_SCOPE_BUILD) {
5057                            System.out.println("[DEBUG]   Skipping schema.function() call (system schema): " + funcNameStr);
5058                        }
5059                        return; // Don't treat as column.method()
5060                    }
5061
5062                    if (funcName.getDatabaseToken() != null) {
5063                        // Case 1: 3-part name (table.column.method)
5064                        String tableAlias = funcName.getDatabaseToken().toString();
5065                        if (DEBUG_SCOPE_BUILD) {
5066                            System.out.println("[DEBUG]   Detected 3-part name: " + tableAlias + "." + possibleColumnOrSchema + ".method");
5067                        }
5068                        handleMethodCallOnColumn(tableAlias, possibleColumnOrSchema);
5069                    } else if (currentFromScope != null) {
5070                        // Case 2: 2-part name (column.method) with schemaToken set
5071                        if (DEBUG_SCOPE_BUILD) {
5072                            System.out.println("[DEBUG]   Detected 2-part name (schemaToken): " + possibleColumnOrSchema + ".method");
5073                        }
5074                        handleUnqualifiedMethodCall(possibleColumnOrSchema);
5075                    }
5076                } else if (funcNameStr != null && funcNameStr.contains(".") && currentFromScope != null) {
5077                    // Alternative case: function name contains dots but schemaToken is null
5078                    // This happens in UPDATE SET clause: Location.SetXY(...)
5079                    // Or in SELECT with 3-part names: p.Demographics.value(...)
5080                    //
5081                    // We need to distinguish between:
5082                    // - 2-part: column.method() - first part is a column name
5083                    // - 3-part: table.column.method() - first part is a table alias
5084                    //
5085                    // We check if the first part matches a table alias in the current FROM scope
5086                    int firstDotPos = funcNameStr.indexOf('.');
5087                    if (firstDotPos > 0) {
5088                        String firstPart = funcNameStr.substring(0, firstDotPos);
5089                        String remainder = funcNameStr.substring(firstDotPos + 1);
5090
5091                        // Check if first part is a table alias
5092                        boolean firstPartIsTable = false;
5093                        for (ScopeChild child : currentFromScope.getChildren()) {
5094                            if (nameMatcher.matches(child.getAlias(), firstPart)) {
5095                                firstPartIsTable = true;
5096                                break;
5097                            }
5098                        }
5099
5100                        if (firstPartIsTable) {
5101                            // 3-part name: table.column.method()
5102                            // Extract column name from remainder (before the next dot, if any)
5103                            int secondDotPos = remainder.indexOf('.');
5104                            if (secondDotPos > 0) {
5105                                String columnName = remainder.substring(0, secondDotPos);
5106                                if (DEBUG_SCOPE_BUILD) {
5107                                    System.out.println("[DEBUG]   Detected 3-part name (parsed): " + firstPart + "." + columnName + ".method");
5108                                }
5109                                handleMethodCallOnColumn(firstPart, columnName);
5110                            }
5111                            // If no second dot, the structure is ambiguous (table.method?) - skip it
5112                        } else {
5113                            // 2-part name: column.method()
5114                            if (DEBUG_SCOPE_BUILD) {
5115                                System.out.println("[DEBUG]   Detected 2-part name (parsed): " + firstPart + ".method");
5116                            }
5117                            handleUnqualifiedMethodCall(firstPart);
5118                        }
5119                    }
5120                }
5121            }
5122        }
5123    }
5124
5125    /**
5126     * Mark keyword arguments in a function call so they are not treated as column references.
5127     * Uses TBuiltFunctionUtil to check which argument positions contain keywords.
5128     */
5129    private void markFunctionKeywordArguments(TFunctionCall functionCall) {
5130        if (functionCall.getArgs() == null || functionCall.getArgs().size() == 0) {
5131            return;
5132        }
5133        if (functionCall.getFunctionName() == null) {
5134            return;
5135        }
5136
5137        String functionName = functionCall.getFunctionName().toString();
5138        Set<Integer> keywordPositions = TBuiltFunctionUtil.argumentsIncludeKeyword(dbVendor, functionName);
5139
5140        if (keywordPositions == null || keywordPositions.isEmpty()) {
5141            return;
5142        }
5143
5144        TExpressionList args = functionCall.getArgs();
5145        for (Integer pos : keywordPositions) {
5146            int index = pos - 1; // TBuiltFunctionUtil uses 1-based positions
5147            if (index >= 0 && index < args.size()) {
5148                TExpression argExpr = args.getExpression(index);
5149                // Mark the objectOperand if it's a simple object name or constant
5150                if (argExpr != null) {
5151                    TObjectName objectName = argExpr.getObjectOperand();
5152                    if (objectName != null) {
5153                        functionKeywordArguments.add(objectName);
5154                        if (DEBUG_SCOPE_BUILD) {
5155                            System.out.println("[DEBUG] Marked function keyword argument: " +
5156                                objectName.toString() + " at position " + pos +
5157                                " in function " + functionName);
5158                        }
5159                    }
5160                }
5161            }
5162        }
5163    }
5164
5165    /**
5166     * Handle a qualified method call on a column: table.column.method()
5167     * Creates a synthetic column reference and links it to the source table.
5168     *
5169     * @param tableAlias the table alias or name
5170     * @param columnName the column name
5171     */
5172    private void handleMethodCallOnColumn(String tableAlias, String columnName) {
5173        if (currentFromScope == null) {
5174            return;
5175        }
5176
5177        for (ScopeChild child : currentFromScope.getChildren()) {
5178            if (nameMatcher.matches(child.getAlias(), tableAlias)) {
5179                // Found matching table - create column reference
5180                createAndRegisterColumnReference(tableAlias, columnName, child);
5181                break;
5182            }
5183        }
5184    }
5185
5186    /**
5187     * Handle an unqualified method call on a column: column.method()
5188     * Attempts to infer the table when there's only one table in FROM,
5189     * or when the column name uniquely identifies the source table.
5190     *
5191     * @param columnName the column name
5192     */
5193    private void handleUnqualifiedMethodCall(String columnName) {
5194        if (currentFromScope == null) {
5195            return;
5196        }
5197
5198        List<ScopeChild> children = currentFromScope.getChildren();
5199
5200        // Case 1: Only one table in FROM - column must belong to it
5201        if (children.size() == 1) {
5202            ScopeChild child = children.get(0);
5203            createAndRegisterColumnReference(child.getAlias(), columnName, child);
5204            return;
5205        }
5206
5207        // Case 2: Multiple tables - try to find which table has this column
5208        // This requires metadata from TableNamespace or SQLEnv
5209        ScopeChild matchedChild = null;
5210        int matchCount = 0;
5211
5212        for (ScopeChild child : children) {
5213            INamespace ns = child.getNamespace();
5214            if (ns != null) {
5215                // Check if this namespace has the column
5216                ColumnLevel level = ns.hasColumn(columnName);
5217                if (level == ColumnLevel.EXISTS) {
5218                    matchedChild = child;
5219                    matchCount++;
5220                } else if (level == ColumnLevel.MAYBE && matchedChild == null) {
5221                    // MAYBE means the table has no metadata, so column might exist
5222                    // Only use as fallback if no definite match found
5223                    matchedChild = child;
5224                }
5225            }
5226        }
5227
5228        // If exactly one table definitely has the column, use it
5229        // If no definite match but one MAYBE, use that as fallback
5230        if (matchedChild != null && (matchCount <= 1)) {
5231            createAndRegisterColumnReference(matchedChild.getAlias(), columnName, matchedChild);
5232        }
5233    }
5234
5235    /**
5236     * Create and register a synthetic column reference for a method call on a column.
5237     *
5238     * @param tableAlias the table alias or name
5239     * @param columnName the column name
5240     * @param scopeChild the ScopeChild containing the namespace
5241     */
5242    private void createAndRegisterColumnReference(String tableAlias, String columnName, ScopeChild scopeChild) {
5243        // Create TObjectName with proper tokens for table.column
5244        // Use TObjectName.createObjectName() which properly initializes objectToken and partToken
5245        TSourceToken tableToken = new TSourceToken(tableAlias);
5246        TSourceToken columnToken = new TSourceToken(columnName);
5247        TObjectName columnRef = TObjectName.createObjectName(
5248            dbVendor,
5249            EDbObjectType.column,
5250            tableToken,
5251            columnToken
5252        );
5253
5254        // Get the TTable from the namespace
5255        INamespace ns = scopeChild.getNamespace();
5256        TTable sourceTable = null;
5257        if (ns instanceof TableNamespace) {
5258            sourceTable = ((TableNamespace) ns).getTable();
5259        } else if (ns instanceof CTENamespace) {
5260            sourceTable = ((CTENamespace) ns).getReferencingTable();
5261        } else if (ns != null) {
5262            // For other namespace types (SubqueryNamespace, etc.), try getFinalTable()
5263            sourceTable = ns.getFinalTable();
5264        }
5265
5266        if (sourceTable != null) {
5267            columnRef.setSourceTable(sourceTable);
5268        }
5269
5270        // Determine the scope for this column
5271        IScope columnScope = determineColumnScope(columnRef);
5272        if (columnScope != null) {
5273            columnToScopeMap.put(columnRef, columnScope);
5274        }
5275        allColumnReferences.add(columnRef);
5276
5277        if (DEBUG_SCOPE_BUILD) {
5278            System.out.println("[DEBUG] Created synthetic column reference for method call: " +
5279                    tableAlias + "." + columnName + " -> " +
5280                    (sourceTable != null ? sourceTable.getFullName() : "unknown"));
5281        }
5282    }
5283
5284    /**
5285     * Traverse an expression to collect column references.
5286     * Uses iterative left-chain descent for pure binary expression chains
5287     * to avoid StackOverflowError for deeply nested AND/OR/arithmetic chains.
5288     */
5289    private void traverseExpressionForColumns(TExpression expr) {
5290        if (expr == null) return;
5291
5292        // For pure binary expression chains (AND/OR/arithmetic), use iterative descent
5293        // to avoid StackOverflowError. Binary expressions don't have objectOperand,
5294        // functionCall, exprList, subQuery, or caseExpression - only left/right operands.
5295        if (TExpression.isPureBinaryForDoParse(expr.getExpressionType())) {
5296            Deque<TExpression> rightChildren = new ArrayDeque<>();
5297            TExpression current = expr;
5298            while (current != null && TExpression.isPureBinaryForDoParse(current.getExpressionType())) {
5299                if (current.getRightOperand() != null) {
5300                    rightChildren.push(current.getRightOperand());
5301                }
5302                current = current.getLeftOperand();
5303            }
5304            // Process leftmost leaf (not a pure binary type, safe to recurse)
5305            if (current != null) {
5306                traverseExpressionForColumns(current);
5307            }
5308            // Process right children bottom-up (preserves left-to-right order)
5309            while (!rightChildren.isEmpty()) {
5310                traverseExpressionForColumns(rightChildren.pop());
5311            }
5312            return;
5313        }
5314
5315        // Handle lambda expressions - push parameter names before traversing body
5316        boolean isLambda = (expr.getExpressionType() == EExpressionType.lambda_t);
5317        if (isLambda && expr.getLeftOperand() != null) {
5318            Set<String> paramNames = new HashSet<>();
5319            collectLambdaParameterNames(expr.getLeftOperand(), paramNames);
5320            lambdaParameterStack.push(paramNames);
5321            // Also collect the parameter objects
5322            collectLambdaParameterObjects(expr.getLeftOperand());
5323        }
5324
5325        // Check for column reference in objectOperand (common in array access, simple names)
5326        // Skip constants (e.g., DAY in DATEDIFF(DAY, ...) is parsed as simple_constant_t)
5327        if (expr.getObjectOperand() != null &&
5328            expr.getExpressionType() != EExpressionType.simple_constant_t) {
5329            if (DEBUG_SCOPE_BUILD) {
5330                System.out.println("[DEBUG] traverseExpressionForColumns: Found objectOperand=" +
5331                    expr.getObjectOperand().toString() + " in expr type=" + expr.getExpressionType());
5332            }
5333            // This will trigger preVisit(TObjectName) if not a table reference
5334            preVisit(expr.getObjectOperand());
5335        }
5336
5337        // Traverse sub-expressions
5338        // For assignment_t expressions (named arguments like "INPUT => value" in Snowflake FLATTEN),
5339        // the left operand is the parameter name, NOT a column reference.
5340        // Only traverse the right operand (the value) for assignment_t expressions.
5341        boolean isNamedArgument = (expr.getExpressionType() == EExpressionType.assignment_t);
5342        if (expr.getLeftOperand() != null && !isNamedArgument) {
5343            traverseExpressionForColumns(expr.getLeftOperand());
5344        }
5345        if (expr.getRightOperand() != null) {
5346            traverseExpressionForColumns(expr.getRightOperand());
5347        }
5348
5349        // Pop lambda parameter context after traversing
5350        if (isLambda && expr.getLeftOperand() != null && !lambdaParameterStack.isEmpty()) {
5351            lambdaParameterStack.pop();
5352        }
5353
5354        // Traverse function call arguments
5355        if (expr.getFunctionCall() != null) {
5356            TFunctionCall func = expr.getFunctionCall();
5357            if (func.getArgs() != null) {
5358                for (int i = 0; i < func.getArgs().size(); i++) {
5359                    traverseExpressionForColumns(func.getArgs().getExpression(i));
5360                }
5361            }
5362            // Handle STRUCT field values recursively
5363            if (func.getFieldValues() != null) {
5364                for (int i = 0; i < func.getFieldValues().size(); i++) {
5365                    TResultColumn rc = func.getFieldValues().getResultColumn(i);
5366                    if (rc != null && rc.getExpr() != null) {
5367                        traverseExpressionForColumns(rc.getExpr());
5368                    }
5369                }
5370            }
5371            // Handle special function expressions (CAST, CONVERT, EXTRACT, etc.)
5372            // These functions store their arguments in expr1/expr2/expr3 instead of args
5373            if (func.getExpr1() != null) {
5374                traverseExpressionForColumns(func.getExpr1());
5375            }
5376            if (func.getExpr2() != null) {
5377                traverseExpressionForColumns(func.getExpr2());
5378            }
5379            if (func.getExpr3() != null) {
5380                traverseExpressionForColumns(func.getExpr3());
5381            }
5382            // Handle XML functions that store their arguments in special properties
5383            if (func.getXMLElementValueExprList() != null) {
5384                TResultColumnList xmlValueList = func.getXMLElementValueExprList();
5385                for (int j = 0; j < xmlValueList.size(); j++) {
5386                    TResultColumn rc = xmlValueList.getResultColumn(j);
5387                    if (rc != null && rc.getExpr() != null) {
5388                        traverseExpressionForColumns(rc.getExpr());
5389                    }
5390                }
5391            }
5392            if (func.getXMLForestValueList() != null) {
5393                TResultColumnList xmlForestList = func.getXMLForestValueList();
5394                for (int j = 0; j < xmlForestList.size(); j++) {
5395                    TResultColumn rc = xmlForestList.getResultColumn(j);
5396                    if (rc != null && rc.getExpr() != null) {
5397                        traverseExpressionForColumns(rc.getExpr());
5398                    }
5399                }
5400            }
5401            if (func.getXmlPassingClause() != null &&
5402                func.getXmlPassingClause().getPassingList() != null) {
5403                TResultColumnList passingList = func.getXmlPassingClause().getPassingList();
5404                for (int j = 0; j < passingList.size(); j++) {
5405                    TResultColumn rc = passingList.getResultColumn(j);
5406                    if (rc != null && rc.getExpr() != null) {
5407                        traverseExpressionForColumns(rc.getExpr());
5408                    }
5409                }
5410            }
5411            if (func.getXMLAttributesClause() != null &&
5412                func.getXMLAttributesClause().getValueExprList() != null) {
5413                TResultColumnList attrList = func.getXMLAttributesClause().getValueExprList();
5414                for (int j = 0; j < attrList.size(); j++) {
5415                    TResultColumn rc = attrList.getResultColumn(j);
5416                    if (rc != null && rc.getExpr() != null) {
5417                        traverseExpressionForColumns(rc.getExpr());
5418                    }
5419                }
5420            }
5421            // Handle XMLCAST typeExpression
5422            if (func.getTypeExpression() != null) {
5423                traverseExpressionForColumns(func.getTypeExpression());
5424            }
5425        }
5426
5427        // Traverse expression list (e.g., IN clause)
5428        if (expr.getExprList() != null) {
5429            for (int i = 0; i < expr.getExprList().size(); i++) {
5430                traverseExpressionForColumns(expr.getExprList().getExpression(i));
5431            }
5432        }
5433
5434        // Traverse subquery if present
5435        if (expr.getSubQuery() != null) {
5436            expr.getSubQuery().acceptChildren(this);
5437        }
5438
5439        // Traverse CASE expression
5440        if (expr.getExpressionType() == EExpressionType.case_t && expr.getCaseExpression() != null) {
5441            TCaseExpression caseExpr = expr.getCaseExpression();
5442
5443            // Traverse input expression (for simple CASE: CASE input_expr WHEN ...)
5444            if (caseExpr.getInput_expr() != null) {
5445                traverseExpressionForColumns(caseExpr.getInput_expr());
5446            }
5447
5448            // Traverse each WHEN...THEN clause
5449            if (caseExpr.getWhenClauseItemList() != null) {
5450                for (int i = 0; i < caseExpr.getWhenClauseItemList().size(); i++) {
5451                    TWhenClauseItem whenItem = caseExpr.getWhenClauseItemList().getWhenClauseItem(i);
5452                    if (whenItem != null) {
5453                        // Traverse WHEN condition
5454                        if (whenItem.getComparison_expr() != null) {
5455                            traverseExpressionForColumns(whenItem.getComparison_expr());
5456                        }
5457                        // Traverse PostgreSQL condition list
5458                        if (whenItem.getConditionList() != null) {
5459                            for (int j = 0; j < whenItem.getConditionList().size(); j++) {
5460                                traverseExpressionForColumns(whenItem.getConditionList().getExpression(j));
5461                            }
5462                        }
5463                        // Traverse THEN result
5464                        if (whenItem.getReturn_expr() != null) {
5465                            traverseExpressionForColumns(whenItem.getReturn_expr());
5466                        }
5467                    }
5468                }
5469            }
5470
5471            // Traverse ELSE expression
5472            if (caseExpr.getElse_expr() != null) {
5473                traverseExpressionForColumns(caseExpr.getElse_expr());
5474            }
5475        }
5476    }
5477
5478    // ========== Result Columns ==========
5479
5480    @Override
5481    public void preVisit(TResultColumn resultColumn) {
5482        // Mark that we're inside a result column context.
5483        // Lateral alias matching should ONLY apply inside result columns.
5484        inResultColumnContext = true;
5485
5486        // Track the current result column's alias to exclude from lateral alias matching.
5487        // A column reference inside the expression that DEFINES an alias cannot be
5488        // a reference TO that alias - it must be a reference to the source table column.
5489        // S2: must use the same vendor-aware key as the lateral alias set so the
5490        // exclusion check at isLateralColumnAlias() compares like-with-like (e.g.
5491        // Snowflake folds unquoted identifiers to UPPER; if this stayed
5492        // lowercase, the equals() check below would fail and a column reference
5493        // inside its own alias-defining expression would be wrongly treated as a
5494        // lateral alias reference, dropping it from the column-reference set).
5495        if (resultColumn.getAliasClause() != null && resultColumn.getAliasClause().getAliasName() != null) {
5496            currentResultColumnAlias = keyForColumn(normalizeAliasName(
5497                resultColumn.getAliasClause().getAliasName().toString()));
5498        } else {
5499            currentResultColumnAlias = null;
5500        }
5501
5502        // Collect SQL Server proprietary column aliases (column_alias = expression)
5503        // These should not be treated as column references
5504        if (resultColumn.getExpr() != null &&
5505            resultColumn.getExpr().getExpressionType() == EExpressionType.sqlserver_proprietary_column_alias_t) {
5506            TExpression leftOperand = resultColumn.getExpr().getLeftOperand();
5507            if (leftOperand != null &&
5508                leftOperand.getExpressionType() == EExpressionType.simple_object_name_t &&
5509                leftOperand.getObjectOperand() != null) {
5510                sqlServerProprietaryAliases.add(leftOperand.getObjectOperand());
5511                if (DEBUG_SCOPE_BUILD) {
5512                    System.out.println("[DEBUG] Collected SQL Server proprietary alias: " +
5513                            leftOperand.getObjectOperand().toString());
5514                }
5515            }
5516        }
5517
5518        // Handle BigQuery EXCEPT columns: SELECT * EXCEPT (column1, column2)
5519        // EXCEPT columns are added to allColumnReferences for DDL verification and lineage tracking,
5520        // but WITHOUT scope mapping to avoid triggering auto-inference in inner namespaces.
5521        // Star column expansion in TSQLResolver2 handles EXCEPT filtering directly.
5522        TObjectNameList exceptColumns = resultColumn.getExceptColumnList();
5523        if (exceptColumns != null && exceptColumns.size() > 0) {
5524            // Get the star column's source table from the expression
5525            TTable starSourceTable = null;
5526            if (resultColumn.getExpr() != null &&
5527                resultColumn.getExpr().getExpressionType() == EExpressionType.simple_object_name_t &&
5528                resultColumn.getExpr().getObjectOperand() != null) {
5529                TObjectName starColumn = resultColumn.getExpr().getObjectOperand();
5530                String starStr = starColumn.toString();
5531                if (starStr != null && starStr.endsWith("*")) {
5532                    // Get the table qualifier (e.g., "COMMON" from "COMMON.*")
5533                    String tableQualifier = starColumn.getTableString();
5534                    if (tableQualifier != null && !tableQualifier.isEmpty()) {
5535                        // Find the table by alias in the current scope
5536                        starSourceTable = findTableByAliasInCurrentScope(tableQualifier);
5537                    }
5538                }
5539            }
5540
5541            for (int i = 0; i < exceptColumns.size(); i++) {
5542                TObjectName exceptCol = exceptColumns.getObjectName(i);
5543                if (exceptCol != null) {
5544                    if (starSourceTable != null) {
5545                        // Qualified star (e.g., COMMON.*): Link EXCEPT column directly to source table
5546                        exceptCol.setSourceTable(starSourceTable);
5547                    }
5548                    // IMPORTANT: Add to allColumnReferences for tracking, but do NOT add to
5549                    // columnToScopeMap - this prevents triggering auto-inference in inner namespaces
5550                    allColumnReferences.add(exceptCol);
5551                    if (DEBUG_SCOPE_BUILD) {
5552                        System.out.println("[DEBUG] EXCEPT column added for tracking (no scope mapping): " +
5553                            exceptCol.toString() + (starSourceTable != null ? " -> " + starSourceTable.getFullName() : ""));
5554                    }
5555                }
5556            }
5557        }
5558
5559        // Handle BigQuery REPLACE columns: SELECT * REPLACE (expr AS identifier)
5560        // REPLACE columns create new columns that replace existing ones in star expansion.
5561        // Link them directly to the star's source table.
5562        java.util.ArrayList<gudusoft.gsqlparser.nodes.TReplaceExprAsIdentifier> replaceColumns =
5563            resultColumn.getReplaceExprAsIdentifiers();
5564        if (replaceColumns != null && replaceColumns.size() > 0) {
5565            // Get the star column's source table (similar to EXCEPT handling)
5566            TTable starSourceTable = null;
5567            if (resultColumn.getExpr() != null &&
5568                resultColumn.getExpr().getExpressionType() == EExpressionType.simple_object_name_t &&
5569                resultColumn.getExpr().getObjectOperand() != null) {
5570                TObjectName starColumn = resultColumn.getExpr().getObjectOperand();
5571                String starStr = starColumn.toString();
5572                if (starStr != null && starStr.endsWith("*")) {
5573                    String tableQualifier = starColumn.getTableString();
5574                    if (tableQualifier != null && !tableQualifier.isEmpty()) {
5575                        starSourceTable = findTableByAliasInCurrentScope(tableQualifier);
5576                    }
5577                }
5578            }
5579
5580            for (int i = 0; i < replaceColumns.size(); i++) {
5581                gudusoft.gsqlparser.nodes.TReplaceExprAsIdentifier replaceCol = replaceColumns.get(i);
5582                if (replaceCol != null && replaceCol.getIdentifier() != null) {
5583                    TObjectName replaceId = replaceCol.getIdentifier();
5584                    if (starSourceTable != null) {
5585                        // Qualified star: Link REPLACE identifier to the star's source table
5586                        replaceId.setSourceTable(starSourceTable);
5587                        allColumnReferences.add(replaceId);
5588                        if (DEBUG_SCOPE_BUILD) {
5589                            System.out.println("[DEBUG] REPLACE column linked to star source table: " +
5590                                replaceId.toString() + " -> " + starSourceTable.getFullName());
5591                        }
5592                    } else {
5593                        // Unqualified star: Add with scope mapping for normal resolution
5594                        if (currentSelectScope != null) {
5595                            columnToScopeMap.put(replaceId, currentSelectScope);
5596                        }
5597                        allColumnReferences.add(replaceId);
5598                        if (DEBUG_SCOPE_BUILD) {
5599                            System.out.println("[DEBUG] REPLACE column added for resolution (unqualified star): " +
5600                                replaceId.toString());
5601                        }
5602                    }
5603                }
5604            }
5605        }
5606
5607        // Handle CTAS target columns (CREATE TABLE AS SELECT)
5608        // In CTAS context, result columns become target table column definitions.
5609        // These are registered as "definition columns" (not reference columns):
5610        // - Added to allColumnReferences (for output)
5611        // - Added to ctasTargetColumns/tupleAliasColumns (definition set - prevents re-resolution)
5612        // - NOT added to columnToScopeMap (prevents NameResolver from overwriting sourceTable)
5613        //
5614        // IMPORTANT: Only handle CTAS target columns for the main SELECT of CTAS,
5615        // not for result columns inside CTEs or subqueries within the CTAS.
5616        // When inside a CTE definition (cteDefinitionDepth > 0), skip CTAS handling because
5617        // those result columns define CTE output, not CTAS target table columns.
5618        // Also check currentSelectScope == ctasMainSelectScope to exclude subqueries.
5619        if (currentCTASTargetTable != null && cteDefinitionDepth == 0 && currentSelectScope == ctasMainSelectScope) {
5620            boolean ctasColumnHandled = false;
5621
5622            if (resultColumn.getAliasClause() != null) {
5623                TObjectNameList tupleColumns = resultColumn.getAliasClause().getColumns();
5624
5625                // Case 1: Tuple aliases (e.g., AS (b1, b2, b3) in SparkSQL/Hive)
5626                if (tupleColumns != null && tupleColumns.size() > 0) {
5627                    for (int i = 0; i < tupleColumns.size(); i++) {
5628                        TObjectName tupleCol = tupleColumns.getObjectName(i);
5629                        if (tupleCol != null) {
5630                            // Mark as tuple alias column to skip in preVisit(TObjectName)
5631                            tupleAliasColumns.add(tupleCol);
5632                            // Set the source table to the CTAS target table
5633                            tupleCol.setSourceTable(currentCTASTargetTable);
5634                            // Add to all column references so it appears in output
5635                            allColumnReferences.add(tupleCol);
5636                            if (DEBUG_SCOPE_BUILD) {
5637                                System.out.println("[DEBUG] Registered CTAS tuple alias column: " +
5638                                        tupleCol.toString() + " -> " + currentCTASTargetTable.getFullName());
5639                            }
5640                        }
5641                    }
5642                    ctasColumnHandled = true;
5643                }
5644                // Case 2: Standard alias (AS alias / Teradata NAMED alias)
5645                else {
5646                    TObjectName aliasName = resultColumn.getAliasClause().getAliasName();
5647                    if (aliasName != null) {
5648                        // For alias names, partToken may be null but startToken has the actual text
5649                        // We need to set partToken so getColumnNameOnly() works correctly in the formatter
5650                        // This is done on a clone to avoid modifying the original AST node
5651                        if (aliasName.getPartToken() == null && aliasName.getStartToken() != null) {
5652                            // Clone the aliasName to avoid modifying the original AST
5653                            TObjectName ctasCol = aliasName.clone();
5654                            ctasCol.setPartToken(ctasCol.getStartToken());
5655                            ctasCol.setSourceTable(currentCTASTargetTable);
5656                            ctasTargetColumns.add(ctasCol);
5657                            allColumnReferences.add(ctasCol);
5658                            if (DEBUG_SCOPE_BUILD) {
5659                                System.out.println("[DEBUG] Registered CTAS target column (standard alias clone): " +
5660                                        ctasCol.getColumnNameOnly() + " -> " + currentCTASTargetTable.getFullName());
5661                            }
5662                            ctasColumnHandled = true;
5663                        } else {
5664                            String colName = aliasName.getColumnNameOnly();
5665                            if (colName != null && !colName.isEmpty()) {
5666                                // Register aliasName as CTAS target column DEFINITION
5667                                aliasName.setSourceTable(currentCTASTargetTable);
5668                                ctasTargetColumns.add(aliasName);
5669                                allColumnReferences.add(aliasName);
5670                                if (DEBUG_SCOPE_BUILD) {
5671                                    System.out.println("[DEBUG] Registered CTAS target column (standard alias): " +
5672                                            colName + " -> " + currentCTASTargetTable.getFullName());
5673                                }
5674                                ctasColumnHandled = true;
5675                            }
5676                        }
5677                    }
5678                }
5679            }
5680
5681            // Case 3: No alias, simple column reference or star (e.g., SELECT a FROM s, SELECT * FROM s)
5682            // The target column inherits the source column name.
5683            // Use clone pattern (like JOIN...USING) to avoid polluting source column's sourceTable.
5684            if (!ctasColumnHandled && resultColumn.getExpr() != null &&
5685                resultColumn.getExpr().getExpressionType() == EExpressionType.simple_object_name_t &&
5686                resultColumn.getExpr().getObjectOperand() != null) {
5687                TObjectName sourceCol = resultColumn.getExpr().getObjectOperand();
5688                // Get column name - use getColumnNameOnly() first, fall back to toString()
5689                String colName = sourceCol.getColumnNameOnly();
5690                if (colName == null || colName.isEmpty()) {
5691                    colName = sourceCol.toString();
5692                }
5693                if (colName != null && !colName.isEmpty()) {
5694                    // Clone the source column to create a synthetic CTAS target column
5695                    // The clone refers to original start/end tokens for proper name extraction
5696                    // This includes star columns (SELECT * FROM s) which should create t.*
5697                    TObjectName ctasCol = sourceCol.clone();
5698                    ctasCol.setSourceTable(currentCTASTargetTable);
5699                    ctasTargetColumns.add(ctasCol);
5700                    allColumnReferences.add(ctasCol);
5701                    if (DEBUG_SCOPE_BUILD) {
5702                        System.out.println("[DEBUG] Registered CTAS target column (simple ref clone): " +
5703                                colName + " -> " + currentCTASTargetTable.getFullName());
5704                    }
5705                }
5706            }
5707        } else {
5708            // NOT in CTAS context - track result column alias names to skip them
5709            // These are NOT column references - they're alias names given to expressions.
5710            // This includes standard "AS alias" and Teradata "NAMED alias" syntax.
5711            if (resultColumn.getAliasClause() != null) {
5712                TObjectName aliasName = resultColumn.getAliasClause().getAliasName();
5713                if (aliasName != null) {
5714                    resultColumnAliasNames.add(aliasName);
5715                    if (DEBUG_SCOPE_BUILD) {
5716                        System.out.println("[DEBUG] Tracked result column alias name (will skip): " + aliasName.toString());
5717                    }
5718                }
5719            }
5720        }
5721
5722        // Handle UPDATE SET clause columns
5723        // When inside an UPDATE statement, the left-hand side of SET assignments
5724        // (e.g., SET col = expr) should be linked to the target table
5725        if (currentUpdateTargetTable != null && resultColumn.getExpr() != null) {
5726            TExpression expr = resultColumn.getExpr();
5727            // SET clause uses assignment_t for "column = value" assignments in Teradata
5728            // or simple_comparison_t in some other databases
5729            if ((expr.getExpressionType() == EExpressionType.assignment_t ||
5730                 expr.getExpressionType() == EExpressionType.simple_comparison_t) &&
5731                expr.getLeftOperand() != null &&
5732                expr.getLeftOperand().getExpressionType() == EExpressionType.simple_object_name_t &&
5733                expr.getLeftOperand().getObjectOperand() != null) {
5734                TObjectName leftColumn = expr.getLeftOperand().getObjectOperand();
5735                // Link the SET clause column to the UPDATE target table
5736                leftColumn.setSourceTable(currentUpdateTargetTable);
5737                allColumnReferences.add(leftColumn);
5738                // Mark as SET clause target - should NOT be re-resolved through star column
5739                setClauseTargetColumns.add(leftColumn);
5740                // Also add to columnToScopeMap with the current UpdateScope
5741                if (currentUpdateScope != null) {
5742                    columnToScopeMap.put(leftColumn, currentUpdateScope);
5743                }
5744                if (DEBUG_SCOPE_BUILD) {
5745                    System.out.println("[DEBUG] Linked UPDATE SET column: " +
5746                            leftColumn.toString() + " -> " + currentUpdateTargetTable.getFullName());
5747                }
5748            }
5749        }
5750
5751        // Explicitly traverse typecast expressions in result columns
5752        // The visitor pattern may not automatically traverse nested expressions in typecast
5753        // This is important for Snowflake stage file columns like "$1:apMac::string"
5754        if (resultColumn.getExpr() != null &&
5755            resultColumn.getExpr().getExpressionType() == EExpressionType.typecast_t) {
5756            TExpression typecastExpr = resultColumn.getExpr();
5757            if (typecastExpr.getLeftOperand() != null) {
5758                traverseExpressionForColumns(typecastExpr.getLeftOperand());
5759            }
5760        }
5761    }
5762
5763    @Override
5764    public void postVisit(TResultColumn resultColumn) {
5765        // Clear the current result column alias when we leave the result column
5766        currentResultColumnAlias = null;
5767        // Mark that we're leaving the result column context
5768        inResultColumnContext = false;
5769    }
5770
5771    // ========== Column References ==========
5772
5773    @Override
5774    public void preVisit(TObjectName objectName) {
5775        // Skip if this is a table name reference
5776        if (tableNameReferences.contains(objectName)) {
5777            return;
5778        }
5779
5780        // Skip if this is a tuple alias column (already handled in preVisit(TResultColumn))
5781        if (tupleAliasColumns.contains(objectName)) {
5782            return;
5783        }
5784
5785        // Skip if this is a CTAS target column (already handled in preVisit(TResultColumn))
5786        // These are definition columns for the CTAS target table, not column references
5787        if (ctasTargetColumns.contains(objectName)) {
5788            return;
5789        }
5790
5791        // Skip if this is a VALUES table alias column definition (already handled in processValuesTable)
5792        // These are column NAME definitions like 'id', 'name' in "VALUES (...) AS t(id, name)"
5793        if (valuesTableAliasColumns.contains(objectName)) {
5794            return;
5795        }
5796
5797        // Skip if this is a PIVOT IN clause column (already handled in addPivotInClauseColumns)
5798        // These are pivot column DEFINITIONS, not references to source table columns.
5799        if (pivotInClauseColumns.contains(objectName)) {
5800            return;
5801        }
5802
5803        // Skip if this is a result column alias name (tracked in preVisit(TResultColumn))
5804        // These are NOT column references - they're alias names for expressions
5805        // e.g., "COUNT(1) AS cnt" or Teradata "COUNT(1)(NAMED cnt)"
5806        if (resultColumnAliasNames.contains(objectName)) {
5807            return;
5808        }
5809
5810        // Skip if this is a lambda parameter
5811        // Lambda parameters are local function parameters, not table column references
5812        // e.g., in "transform(array, x -> x + 1)", x is a lambda parameter
5813        if (lambdaParameters.contains(objectName) || isLambdaParameter(objectName)) {
5814            return;
5815        }
5816
5817        // Skip if this is a DDL target object name (file format, stage, pipe, etc.)
5818        // These are object names in DDL statements, not column references
5819        if (ddlTargetNames.contains(objectName)) {
5820            if (DEBUG_SCOPE_BUILD) {
5821                System.out.println("[DEBUG] Skipping DDL target name: " + objectName);
5822            }
5823            return;
5824        }
5825        if (DEBUG_SCOPE_BUILD && !ddlTargetNames.isEmpty()) {
5826            System.out.println("[DEBUG] DDL target check for " + objectName +
5827                " (hashCode=" + System.identityHashCode(objectName) +
5828                "): ddlTargetNames has " + ddlTargetNames.size() + " entries");
5829            for (TObjectName t : ddlTargetNames) {
5830                System.out.println("[DEBUG]   DDL target: " + t + " (hashCode=" + System.identityHashCode(t) + ")");
5831            }
5832        }
5833
5834        // Skip if this is clearly not a column reference
5835        if (!isColumnReference(objectName)) {
5836            return;
5837        }
5838
5839        // Special handling: Link Snowflake stage positional columns to the stage table
5840        // These columns ($1, $2, $1:path) need to be linked to the stage table in the FROM clause
5841        if (dbVendor == EDbVendor.dbvsnowflake &&
5842            objectName.getSourceTable() == null &&
5843            isSnowflakeStagePositionalColumn(objectName)) {
5844            TTable stageTable = findSnowflakeStageTableInScope();
5845            if (DEBUG_SCOPE_BUILD) {
5846                System.out.println("[DEBUG] findSnowflakeStageTableInScope returned: " +
5847                    (stageTable != null ? stageTable.getTableName() : "null") +
5848                    " for column " + objectName);
5849            }
5850            if (stageTable != null) {
5851                objectName.setSourceTable(stageTable);
5852                if (DEBUG_SCOPE_BUILD) {
5853                    System.out.println("[DEBUG] Linked Snowflake stage column " + objectName +
5854                        " to stage table " + stageTable.getTableName());
5855                }
5856            }
5857        }
5858
5859        // Determine the scope for this column
5860        IScope scope = determineColumnScope(objectName);
5861
5862        if (DEBUG_SCOPE_BUILD) {
5863            System.out.println("[DEBUG] preVisit(TObjectName): " + objectName.toString() +
5864                " scope=" + (scope != null ? scope.getScopeType() : "null") +
5865                " currentSelectScope=" + (currentSelectScope != null ? "exists" : "null") +
5866                " currentUpdateScope=" + (currentUpdateScope != null ? "exists" : "null"));
5867        }
5868
5869        // Record the mapping
5870        if (scope != null) {
5871            columnToScopeMap.put(objectName, scope);
5872            allColumnReferences.add(objectName);
5873        }
5874    }
5875
5876    /**
5877     * Check if a TObjectName is a column reference (not a table/schema/etc.)
5878     */
5879    private boolean isColumnReference(TObjectName objectName) {
5880        if (objectName == null) {
5881            return false;
5882        }
5883
5884        // reuse result from TCustomsqlstatement.isValidColumnName(EDbVendor pDBVendor)
5885        if (objectName.getObjectType() == TObjectName.ttobjNotAObject) return false;
5886        if (objectName.getDbObjectType() == EDbObjectType.hint) return false;
5887
5888        // Numeric literals (e.g., "5" in LIMIT 5, "10" in TOP 10) are not column references.
5889        // Some grammars wrap Number tokens in createObjectName(), making them look like TObjectName
5890        // with dbObjectType=column. Check the token text to filter these out.
5891        if (objectName.getPartToken() != null) {
5892            TSourceToken pt = objectName.getPartToken();
5893            if (pt.tokentype == ETokenType.ttnumber) return false;
5894            // Some lexers (e.g., Hive) assign ttkeyword to Number tokens
5895            String tokenText = pt.toString();
5896            if (tokenText.length() > 0 && isNumericLiteral(tokenText)) return false;
5897        }
5898
5899        // Skip if this is marked as a variable (e.g., DECLARE statement element name)
5900        if (objectName.getObjectType() == TObjectName.ttobjVariable) {
5901            return false;
5902        }
5903
5904        // Skip if it's already marked as a table reference
5905        if (tableNameReferences.contains(objectName)) {
5906            return false;
5907        }
5908
5909        // Skip if this is a variable declaration name (from DECLARE statement)
5910        if (variableDeclarationNames.contains(objectName)) {
5911            return false;
5912        }
5913
5914        // Skip if this is an UNPIVOT definition column (value or FOR column)
5915        // These are column DEFINITIONS that create new output columns, not references
5916        if (unpivotDefinitionColumns.contains(objectName)) {
5917            return false;
5918        }
5919
5920        // Skip if we're inside EXECUTE IMMEDIATE dynamic string expression
5921        // The variable name used for dynamic SQL is not a column reference
5922        if (insideExecuteImmediateDynamicExpr) {
5923            if (DEBUG_SCOPE_BUILD) {
5924                System.out.println("[DEBUG] Skipping identifier inside EXECUTE IMMEDIATE: " + objectName.toString());
5925            }
5926            return false;
5927        }
5928
5929        // Skip if column name is empty or null - this indicates a table alias or similar
5930        String columnName = objectName.getColumnNameOnly();
5931        if (columnName == null || columnName.isEmpty()) {
5932            return false;
5933        }
5934
5935        // Check object type
5936        // Column references typically have objectType that indicates column usage
5937        // or appear in contexts where columns are expected
5938
5939        // Skip if this is part of a CREATE TABLE column definition
5940        // Note: getParentObjectName() returns TObjectName (for qualified names), not the AST parent
5941        TParseTreeNode parent = objectName.getParentObjectName();
5942        if (parent instanceof TColumnDefinition) {
5943            return false;
5944        }
5945
5946        // Skip if this looks like a table name in FROM clause
5947        if (parent instanceof TTable) {
5948            return false;
5949        }
5950
5951        // Skip if this is a table alias (parent is TAliasClause)
5952        if (parent instanceof gudusoft.gsqlparser.nodes.TAliasClause) {
5953            return false;
5954        }
5955
5956        // Skip if this is a cursor name in cursor-related statements (DECLARE CURSOR, OPEN, FETCH, CLOSE, DEALLOCATE)
5957        if (isCursorName(objectName)) {
5958            return false;
5959        }
5960
5961        // Skip if this is a datepart keyword in a date function (e.g., DAY in DATEDIFF)
5962        // These have null parent but are known date/time keywords used in function arguments
5963        // We need to verify it's actually in a date function context by checking surrounding tokens
5964        if (parent == null && objectName.getTableToken() == null &&
5965            isSqlServerDatepartKeyword(columnName) &&
5966            isInDateFunctionContext(objectName)) {
5967            return false;
5968        }
5969
5970        // Skip if this was pre-marked as a function keyword argument in preVisit(TFunctionCall)
5971        // This handles keywords like SECOND in TIMESTAMP_DIFF(ts1, ts2, SECOND) for BigQuery/Snowflake
5972        if (functionKeywordArguments.contains(objectName)) {
5973            if (DEBUG_SCOPE_BUILD) {
5974                System.out.println("[DEBUG] Skipping pre-marked function keyword: " + objectName.toString());
5975            }
5976            return false;
5977        }
5978
5979        // Skip if this is a named argument parameter name (e.g., INPUT in "INPUT => value")
5980        // These are parameter names in named argument syntax, NOT column references.
5981        // Examples: Snowflake FLATTEN(INPUT => parse_json(col), outer => TRUE)
5982        // Check both the Set (for current ScopeBuilder run) and the objectType (for AST-level marking)
5983        if (namedArgumentParameters.contains(objectName) ||
5984            objectName.getObjectType() == TObjectName.ttobjNamedArgParameter) {
5985            if (DEBUG_SCOPE_BUILD) {
5986                System.out.println("[DEBUG] Skipping named argument parameter: " + objectName.toString());
5987            }
5988            return false;
5989        }
5990
5991        // Skip if this is a keyword argument in a built-in function (e.g., DAY in DATEDIFF)
5992        // This is a fallback using parent traversal (may not work if parent is null)
5993        if (isFunctionKeywordArgument(objectName)) {
5994            return false;
5995        }
5996
5997        // Skip if this is a variable or function parameter (e.g., p_date in CREATE FUNCTION)
5998        // EXCEPTION: In PL/SQL blocks, we need to collect variable references so TSQLResolver2
5999        // can distinguish between table columns and block variables during name resolution
6000        if (objectName.getLinkedVariable() != null) {
6001            if (currentPlsqlBlockScope == null) {  // Not in PL/SQL block
6002                return false;
6003            }
6004        }
6005        EDbObjectType dbObjType = objectName.getDbObjectType();
6006        if (dbObjType == EDbObjectType.variable || dbObjType == EDbObjectType.parameter) {
6007            // Special case: Snowflake stage file positional columns ($1, $2, $1:path, etc.)
6008            // These are parsed as "parameter" but are actually column references to stage files
6009            boolean isStagePositional = (dbVendor == EDbVendor.dbvsnowflake && isSnowflakeStagePositionalColumn(objectName));
6010            if (DEBUG_SCOPE_BUILD) {
6011                System.out.println("[DEBUG] dbObjType check: dbObjType=" + dbObjType +
6012                    " colNameOnly=" + objectName.getColumnNameOnly() +
6013                    " objStr=" + objectName.getObjectString() +
6014                    " isSnowflakeStagePositional=" + isStagePositional);
6015            }
6016            if (isStagePositional) {
6017                // Allow collection - will be linked to stage table during name resolution
6018                // Fall through to return true
6019            } else if (currentPlsqlBlockScope == null) {  // Not in PL/SQL block
6020                return false;
6021            } else {
6022                return false;
6023                // In PL/SQL block - allow collection so name resolution can distinguish
6024                // between table columns and block variables
6025            }
6026        }
6027
6028        // Skip if this name matches a registered parameter in the current PL/SQL block scope
6029        // BUT ONLY if we're NOT in a DML statement context (SELECT/INSERT/UPDATE/DELETE/MERGE)
6030        // This handles cases where the parser doesn't link the reference to the parameter declaration
6031        // (e.g., EXECUTE IMMEDIATE ddl_in where ddl_in is a procedure parameter)
6032        //
6033        // When inside a DML statement, we MUST allow collection so name resolution can distinguish
6034        // between table columns and block variables (e.g., "DELETE FROM emp WHERE ename = main.ename")
6035        if (currentPlsqlBlockScope != null) {
6036            // Check if we're inside any DML statement context
6037            // Note: currentInsertTargetTable is set when inside an INSERT statement
6038            boolean inDmlContext = (currentSelectScope != null || currentUpdateScope != null ||
6039                                    currentDeleteScope != null || currentMergeScope != null ||
6040                                    currentInsertTargetTable != null);
6041
6042            if (!inDmlContext) {
6043                // Not in DML context - this is likely a standalone expression like EXECUTE IMMEDIATE param
6044                // or a WHILE condition, etc.
6045                // Check if the name is a variable in the current scope OR any parent PL/SQL block scope.
6046                String nameOnly = objectName.getColumnNameOnly();
6047                if (nameOnly != null && isVariableInPlsqlScopeChain(nameOnly)) {
6048                    if (DEBUG_SCOPE_BUILD) {
6049                        System.out.println("[DEBUG] Skipping registered PL/SQL parameter/variable (not in DML): " + nameOnly);
6050                    }
6051                    return false;
6052                }
6053
6054                // Also skip qualified references (like TEMP1.M1) when not in DML context
6055                // BUT only if the prefix is NOT a trigger correlation variable (:new, :old, new, old)
6056                // These are likely PL/SQL object/record field accesses, not table columns
6057                if (objectName.getTableToken() != null) {
6058                    String prefix = objectName.getTableToken().toString().toLowerCase();
6059                    // Skip filtering for trigger correlation variables
6060                    if (!":new".equals(prefix) && !":old".equals(prefix) &&
6061                        !"new".equals(prefix) && !"old".equals(prefix)) {
6062                        if (DEBUG_SCOPE_BUILD) {
6063                            System.out.println("[DEBUG] Skipping qualified reference in non-DML PL/SQL context: " + objectName.toString());
6064                        }
6065                        return false;
6066                    }
6067                }
6068            }
6069        }
6070
6071        // Skip if this is a bind variable (e.g., :project_id in Oracle, @param in SQL Server)
6072        // BUT skip this check for Snowflake stage positional columns with JSON paths like $1:apMac
6073        // which are tokenized as bind variables due to the colon syntax
6074        if (objectName.getPartToken() != null && objectName.getPartToken().tokentype == ETokenType.ttbindvar) {
6075            // Check if this is a Snowflake stage JSON path column - these are NOT bind variables
6076            if (!(dbVendor == EDbVendor.dbvsnowflake && isSnowflakeStagePositionalColumn(objectName))) {
6077                if (DEBUG_SCOPE_BUILD) {
6078                    System.out.println("[DEBUG] partToken bindvar check: " + objectName.toString() +
6079                        " tokentype=" + objectName.getPartToken().tokentype);
6080                }
6081                return false;
6082            }
6083        }
6084        // Also check by column name pattern for bind variables
6085        // BUT skip this check for Snowflake stage positional columns with JSON paths like $1:apMac
6086        // where getColumnNameOnly() returns ":apMac" - this is NOT a bind variable
6087        String colName = objectName.getColumnNameOnly();
6088        if (colName != null && (colName.startsWith(":") || colName.startsWith("@"))) {
6089            // Check if this is a Snowflake stage JSON path column (e.g., $1:apMac)
6090            // In this case, colName is ":apMac" but it's a JSON path, not a bind variable
6091            boolean isStageCol = (dbVendor == EDbVendor.dbvsnowflake && isSnowflakeStagePositionalColumn(objectName));
6092            if (DEBUG_SCOPE_BUILD) {
6093                System.out.println("[DEBUG] bind variable check: colName=" + colName +
6094                    " objStr=" + objectName.getObjectString() +
6095                    " isSnowflakeStageCol=" + isStageCol);
6096            }
6097            if (!isStageCol) {
6098                return false;
6099            }
6100        }
6101
6102        // Skip built-in functions without parentheses (niladic functions)
6103        // These look like column references but are actually function calls
6104        // Examples: CURRENT_USER (SQL Server), CURRENT_DATETIME (BigQuery), etc.
6105        // Only check unqualified names - qualified names like "table.column" are real column references
6106        if (objectName.getTableToken() == null && isBuiltInFunctionName(colName)) {
6107            return false;
6108        }
6109
6110        // Skip Snowflake procedure system variables (e.g., sqlrowcount, sqlerrm, sqlstate)
6111        // These are special variables available in Snowflake stored procedures
6112        if (dbVendor == EDbVendor.dbvsnowflake && objectName.getTableToken() == null &&
6113            isSnowflakeProcedureSystemVariable(colName)) {
6114            if (DEBUG_SCOPE_BUILD) {
6115                System.out.println("[DEBUG] Skipping Snowflake procedure system variable: " + colName);
6116            }
6117            return false;
6118        }
6119
6120        // Skip if this is a double-quoted string literal in MySQL
6121        // In MySQL, "Z" is a string literal by default (unless ANSI_QUOTES mode)
6122        if (dbVendor == EDbVendor.dbvmysql && objectName.getPartToken() != null) {
6123            if (objectName.getPartToken().tokentype == ETokenType.ttdqstring) {
6124                return false;
6125            }
6126        }
6127
6128        // Skip if this is the alias part of SQL Server's proprietary column alias syntax
6129        // e.g., in "column_alias = expression", column_alias is an alias, not a column reference
6130        if (isSqlServerProprietaryColumnAlias(objectName, parent)) {
6131            return false;
6132        }
6133
6134        // Skip if this is a lateral column alias reference (Snowflake, BigQuery, etc.)
6135        // e.g., in "SELECT col as x, x + 1 as y", x is a lateral alias reference, not a source column
6136        // Only check unqualified names (no table prefix) - qualified names like "t.x" are not lateral aliases
6137        if (objectName.getTableToken() == null && isLateralColumnAlias(columnName)) {
6138            return false;
6139        }
6140
6141        // Handle PL/SQL package constants (e.g., sch.pk_constv2.c_cdsl in VALUES clause)
6142        // These are not resolvable as table columns, but we still collect them so they can
6143        // be output as "missed." columns when linkOrphanColumnToFirstTable=false, or linked
6144        // to the first physical table when linkOrphanColumnToFirstTable=true.
6145        // Note: The qualified prefix (schema.table) is preserved in the TObjectName tokens
6146        // (schemaToken, tableToken) for DataFlowAnalyzer to use when creating relationships.
6147        if (isPlsqlPackageConstant(objectName, parent)) {
6148            // Clear sourceTable since this is not a real table column
6149            // Don't set dbObjectType to variable so that populateOrphanColumns() in TSQLResolver2
6150            // will process it and set ownStmt, enabling linkOrphanColumnToFirstTable to work
6151            objectName.setSourceTable(null);
6152            if (DEBUG_SCOPE_BUILD) {
6153                System.out.println("[DEBUG] Collected PL/SQL package constant as orphan: " + objectName.toString());
6154            }
6155            // Return true to collect as orphan column
6156            return true;
6157        }
6158
6159        // Oracle PL/SQL special identifiers (pseudo-columns, implicit vars, exception handlers, cursor attrs)
6160        // Best practice: filter in ScopeBuilder (early), with strict gating:
6161        // - Oracle vendor only
6162        // - Quoted identifiers override keywords (e.g., "ROWNUM" is a user identifier)
6163        if (dbVendor == EDbVendor.dbvoracle && objectName.getQuoteType() == EQuoteType.notQuoted) {
6164            String nameOnly = objectName.getColumnNameOnly();
6165
6166            // 1) ROWNUM pseudo-column: completely exclude as requested
6167            if (nameOnly != null && "ROWNUM".equalsIgnoreCase(nameOnly)) {
6168                markNotAColumn(objectName, EDbObjectType.constant);
6169                return false;
6170            }
6171
6172            // 2) PL/SQL inquiry directives: $$PLSQL_UNIT, $$PLSQL_LINE, $$PLSQL_UNIT_OWNER, etc.
6173            // These are compile-time constants, not column references
6174            if (nameOnly != null && nameOnly.startsWith("$$")) {
6175                markNotAColumn(objectName, EDbObjectType.constant);
6176                return false;
6177            }
6178
6179            // 3) Inside PL/SQL blocks: filter implicit identifiers & cursor attributes
6180            if (currentPlsqlBlockScope != null) {
6181                if (nameOnly != null) {
6182                    String lower = nameOnly.toLowerCase(Locale.ROOT);
6183                    // SQLCODE (implicit variable) / SQLERRM (built-in; often written like a variable)
6184                    if ("sqlcode".equals(lower) || "sqlerrm".equals(lower)) {
6185                        markNotAColumn(objectName, EDbObjectType.variable);
6186                        return false;
6187                    }
6188                }
6189
6190                // Cursor attributes: SQL%NOTFOUND, cursor%ROWCOUNT, etc.
6191                if (isOraclePlsqlCursorAttribute(objectName)) {
6192                    markNotAColumn(objectName, EDbObjectType.variable);
6193                    return false;
6194                }
6195
6196                // Boolean literals: TRUE, FALSE (case-insensitive)
6197                // These are PL/SQL boolean constants, not column references
6198                if (nameOnly != null) {
6199                    String lower = nameOnly.toLowerCase(Locale.ROOT);
6200                    if ("true".equals(lower) || "false".equals(lower)) {
6201                        markNotAColumn(objectName, EDbObjectType.constant);
6202                        return false;
6203                    }
6204                }
6205
6206                // Oracle predefined exceptions (unqualified names in RAISE/WHEN clauses)
6207                // Examples: NO_DATA_FOUND, TOO_MANY_ROWS, CONFIGURATION_MISMATCH, etc.
6208                if (nameOnly != null && isOraclePredefinedException(nameOnly)) {
6209                    markNotAColumn(objectName, EDbObjectType.variable);
6210                    return false;
6211                }
6212
6213                // Collection methods: .COUNT, .LAST, .FIRST, .DELETE, .EXISTS, .PRIOR, .NEXT, .TRIM, .EXTEND
6214                // These appear as qualified names like "collection.COUNT" where COUNT is the method
6215                if (isOraclePlsqlCollectionMethod(objectName)) {
6216                    markNotAColumn(objectName, EDbObjectType.variable);
6217                    return false;
6218                }
6219
6220                // Cursor variable references: check if this is a known cursor variable
6221                // Example: emp in "OPEN emp FOR SELECT * FROM employees"
6222                // This is based on actual TCursorDeclStmt/TOpenforStmt declarations tracked in scope
6223                if (nameOnly != null && cursorVariableNames.contains(nameOnly.toLowerCase(Locale.ROOT))) {
6224                    markNotAColumn(objectName, EDbObjectType.variable);
6225                    if (DEBUG_SCOPE_BUILD) {
6226                        System.out.println("[DEBUG] Skipping cursor variable: " + nameOnly);
6227                    }
6228                    return false;
6229                }
6230
6231                // Record variable fields: when "table.column" refers to a record variable field
6232                // Example: rec.field_name where rec is declared as "rec record_type%ROWTYPE"
6233                // Check if the "table" part is a known variable in the current scope
6234                if (objectName.getTableToken() != null) {
6235                    String tablePrefix = objectName.getTableToken().toString();
6236                    if (tablePrefix != null && isVariableInPlsqlScopeChain(tablePrefix)) {
6237                        // The "table" part is actually a record variable, so this is a record field access
6238                        markNotAColumn(objectName, EDbObjectType.variable);
6239                        return false;
6240                    }
6241                    // Also check cursor FOR loop record names (e.g., "rec" in "for rec in (SELECT ...)")
6242                    if (tablePrefix != null && cursorForLoopRecordNames.contains(tablePrefix.toLowerCase(Locale.ROOT))) {
6243                        // The "table" part is a cursor FOR loop record, so this is a record field access
6244                        markNotAColumn(objectName, EDbObjectType.variable);
6245                        if (DEBUG_SCOPE_BUILD) {
6246                            System.out.println("[DEBUG] Skipping cursor FOR loop record field: " + objectName.toString());
6247                        }
6248                        return false;
6249                    }
6250
6251                    // Package member references (pkg.member or schema.pkg.member)
6252                    // Check if the table prefix matches a known package
6253                    if (tablePrefix != null && packageRegistry != null && packageRegistry.isPackage(tablePrefix)) {
6254                        OraclePackageNamespace pkgNs = packageRegistry.getPackage(tablePrefix);
6255                        String memberName = objectName.getColumnNameOnly();
6256                        if (pkgNs != null && memberName != null && pkgNs.hasMember(memberName)) {
6257                            markNotAColumn(objectName, EDbObjectType.variable);
6258                            if (DEBUG_SCOPE_BUILD) {
6259                                System.out.println("[DEBUG] Skipping package member reference: " +
6260                                    tablePrefix + "." + memberName);
6261                            }
6262                            return false;
6263                        }
6264                    }
6265                }
6266
6267                // Package-level variable (unqualified) when inside package body
6268                // Check if this is a known package member without qualification
6269                if (currentPackageScope != null && objectName.getTableToken() == null) {
6270                    OraclePackageNamespace pkgNs = currentPackageScope.getPackageNamespace();
6271                    if (pkgNs != null && nameOnly != null && pkgNs.hasMember(nameOnly)) {
6272                        markNotAColumn(objectName, EDbObjectType.variable);
6273                        if (DEBUG_SCOPE_BUILD) {
6274                            System.out.println("[DEBUG] Skipping unqualified package member: " + nameOnly);
6275                        }
6276                        return false;
6277                    }
6278                }
6279            }
6280        }
6281
6282        // Accept if it appears in an expression context
6283        if (parent instanceof TExpression) {
6284            return true;
6285        }
6286
6287        // Accept if it appears in a result column
6288        if (parent instanceof TResultColumn) {
6289            return true;
6290        }
6291
6292        // Default: accept as column reference
6293        return true;
6294    }
6295
6296    /**
6297     * Mark a TObjectName as "not a column" so downstream collectors/resolvers can skip it.
6298     */
6299    private void markNotAColumn(TObjectName objectName, EDbObjectType objType) {
6300        if (objectName == null) return;
6301        objectName.setValidate_column_status(TBaseType.MARKED_NOT_A_COLUMN_IN_COLUMN_RESOLVER);
6302        // IMPORTANT: setSourceTable(null) may set dbObjectType to column; override after
6303        objectName.setSourceTable(null);
6304        if (objType != null) {
6305            objectName.setDbObjectTypeDirectly(objType);
6306        }
6307    }
6308
6309    /**
6310     * Oracle PL/SQL cursor attributes are syntactically identified by '%' (e.g., SQL%NOTFOUND, cur%ROWCOUNT).
6311     * These are never table columns.
6312     */
6313    private boolean isOraclePlsqlCursorAttribute(TObjectName objectName) {
6314        if (dbVendor != EDbVendor.dbvoracle) return false;
6315        if (currentPlsqlBlockScope == null) return false;
6316        if (objectName == null) return false;
6317
6318        String text = objectName.toString();
6319        if (text == null) return false;
6320        int idx = text.lastIndexOf('%');
6321        if (idx < 0 || idx == text.length() - 1) return false;
6322
6323        String attr = text.substring(idx + 1).trim().toUpperCase(Locale.ROOT);
6324        // Common PL/SQL cursor attributes
6325        switch (attr) {
6326            case "FOUND":
6327            case "NOTFOUND":
6328            case "ROWCOUNT":
6329            case "ISOPEN":
6330            case "BULK_ROWCOUNT":
6331            case "BULK_EXCEPTIONS":
6332                return true;
6333            default:
6334                return false;
6335        }
6336    }
6337
6338    /**
6339     * Oracle predefined exceptions that should not be treated as column references.
6340     * These include standard PL/SQL exceptions and DBMS_* package exceptions.
6341     */
6342    private static final java.util.Set<String> ORACLE_PREDEFINED_EXCEPTIONS = new java.util.HashSet<>(java.util.Arrays.asList(
6343        // Standard PL/SQL exceptions
6344        "ACCESS_INTO_NULL", "CASE_NOT_FOUND", "COLLECTION_IS_NULL", "CURSOR_ALREADY_OPEN",
6345        "DUP_VAL_ON_INDEX", "INVALID_CURSOR", "INVALID_NUMBER", "LOGIN_DENIED",
6346        "NO_DATA_FOUND", "NO_DATA_NEEDED", "NOT_LOGGED_ON", "PROGRAM_ERROR",
6347        "ROWTYPE_MISMATCH", "SELF_IS_NULL", "STORAGE_ERROR", "SUBSCRIPT_BEYOND_COUNT",
6348        "SUBSCRIPT_OUTSIDE_LIMIT", "SYS_INVALID_ROWID", "TIMEOUT_ON_RESOURCE",
6349        "TOO_MANY_ROWS", "VALUE_ERROR", "ZERO_DIVIDE",
6350        // DBMS_STANDARD exceptions
6351        "CONFIGURATION_MISMATCH", "OTHERS"
6352    ));
6353
6354    private boolean isOraclePredefinedException(String name) {
6355        if (name == null) return false;
6356        return ORACLE_PREDEFINED_EXCEPTIONS.contains(name.toUpperCase(Locale.ROOT));
6357    }
6358
6359    /**
6360     * Oracle PL/SQL collection methods that should not be treated as column references.
6361     * Examples: my_collection.COUNT, my_array.LAST, my_table.DELETE
6362     */
6363    private boolean isOraclePlsqlCollectionMethod(TObjectName objectName) {
6364        if (dbVendor != EDbVendor.dbvoracle) return false;
6365        if (currentPlsqlBlockScope == null) return false;
6366        if (objectName == null) return false;
6367
6368        // Check if this is a qualified name (has a table/object prefix)
6369        // Collection methods appear as "collection_name.METHOD"
6370        if (objectName.getTableToken() == null) return false;
6371
6372        String methodName = objectName.getColumnNameOnly();
6373        if (methodName == null) return false;
6374
6375        String upper = methodName.toUpperCase(Locale.ROOT);
6376        switch (upper) {
6377            case "COUNT":
6378            case "FIRST":
6379            case "LAST":
6380            case "NEXT":
6381            case "PRIOR":
6382            case "EXISTS":
6383            case "DELETE":
6384            case "TRIM":
6385            case "EXTEND":
6386                return true;
6387            default:
6388                return false;
6389        }
6390    }
6391
6392    /**
6393     * Detect whether this TObjectName belongs to an Oracle exception handler condition.
6394     * Example: EXCEPTION WHEN no_data_found THEN ... / WHEN OTHERS THEN ...
6395     *
6396     * We use parent-chain context rather than a keyword list to avoid false positives.
6397     */
6398    private boolean isInsideOracleExceptionHandler(TObjectName objectName) {
6399        if (dbVendor != EDbVendor.dbvoracle) return false;
6400        if (currentPlsqlBlockScope == null) return false;
6401        if (objectName == null) return false;
6402
6403        TParseTreeNode node = objectName.getParentObjectName();
6404        while (node != null) {
6405            if (node instanceof TExceptionHandler) {
6406                return true;
6407            }
6408            node = node.getParentObjectName();
6409        }
6410        return false;
6411    }
6412
6413    /**
6414     * Check if a TObjectName is a keyword argument in a built-in function.
6415     * For example, DAY in DATEDIFF(DAY, start_date, end_date) is a keyword, not a column.
6416     * Uses TBuiltFunctionUtil to check against the configured keyword argument positions.
6417     */
6418    private boolean isFunctionKeywordArgument(TObjectName objectName) {
6419        // Traverse up to find the containing expression
6420        TParseTreeNode parent = objectName.getParentObjectName();
6421
6422        // Debug output for function keyword detection
6423        if (DEBUG_SCOPE_BUILD) {
6424            System.out.println("[DEBUG] isFunctionKeywordArgument: objectName=" + objectName.toString() +
6425                " parent=" + (parent != null ? parent.getClass().getSimpleName() : "null"));
6426        }
6427
6428        if (!(parent instanceof TExpression)) {
6429            return false;
6430        }
6431
6432        TExpression expr = (TExpression) parent;
6433
6434        // Check if the expression type indicates this could be a keyword argument
6435        // Keywords like DAY, MONTH, YEAR in DATEDIFF/DATEADD are parsed as simple_constant_t
6436        // but still have objectOperand set, so we need to check both types
6437        EExpressionType exprType = expr.getExpressionType();
6438        if (exprType != EExpressionType.simple_object_name_t &&
6439            exprType != EExpressionType.simple_constant_t) {
6440            return false;
6441        }
6442
6443        // Traverse up to find the containing function call
6444        TParseTreeNode exprParent = expr.getParentObjectName();
6445
6446        // The expression might be inside a TExpressionList (function args)
6447        if (exprParent instanceof TExpressionList) {
6448            TExpressionList argList = (TExpressionList) exprParent;
6449            TParseTreeNode argListParent = argList.getParentObjectName();
6450
6451            if (argListParent instanceof TFunctionCall) {
6452                TFunctionCall functionCall = (TFunctionCall) argListParent;
6453                return checkFunctionKeywordPosition(functionCall, argList, expr, objectName);
6454            }
6455        }
6456
6457        // Direct parent might be TFunctionCall in some cases
6458        if (exprParent instanceof TFunctionCall) {
6459            TFunctionCall functionCall = (TFunctionCall) exprParent;
6460            TExpressionList args = functionCall.getArgs();
6461            if (args != null) {
6462                return checkFunctionKeywordPosition(functionCall, args, expr, objectName);
6463            }
6464        }
6465
6466        return false;
6467    }
6468
6469    /**
6470     * Check if the expression is at a keyword argument position in the function call.
6471     */
6472    private boolean checkFunctionKeywordPosition(TFunctionCall functionCall,
6473                                                  TExpressionList argList,
6474                                                  TExpression expr,
6475                                                  TObjectName objectName) {
6476        // Find the position of this expression in the argument list
6477        int position = -1;
6478        for (int i = 0; i < argList.size(); i++) {
6479            if (argList.getExpression(i) == expr) {
6480                position = i + 1; // TBuiltFunctionUtil uses 1-based positions
6481                break;
6482            }
6483        }
6484
6485        if (position < 0) {
6486            return false;
6487        }
6488
6489        // Get function name
6490        String functionName = functionCall.getFunctionName() != null
6491                ? functionCall.getFunctionName().toString()
6492                : null;
6493        if (functionName == null || functionName.isEmpty()) {
6494            return false;
6495        }
6496
6497        // Check against the configured keyword argument positions
6498        // Use the ScopeBuilder's dbVendor since TFunctionCall.dbvendor may not be set
6499        Set<Integer> keywordPositions = TBuiltFunctionUtil.argumentsIncludeKeyword(
6500                dbVendor, functionName);
6501
6502        if (keywordPositions != null && keywordPositions.contains(position)) {
6503            if (DEBUG_SCOPE_BUILD) {
6504                System.out.println("[DEBUG] Skipping function keyword argument: " +
6505                        objectName.toString() + " at position " + position +
6506                        " in function " + functionName);
6507            }
6508            return true;
6509        }
6510
6511        return false;
6512    }
6513
6514    /**
6515     * Check if a TObjectName is a PL/SQL package constant (not a table column).
6516     *
6517     * This filter applies ONLY when all of the following are true:
6518     * - Vendor is Oracle
6519     * - We are inside a PL/SQL block (currentPlsqlBlockScope != null)
6520     * - The TObjectName is multi-part: schema.object.part (e.g., sch.pk_constv2.c_cdsl)
6521     * - The TObjectName is used in an expression/value context (not DDL/definition context)
6522     *
6523     * Decision rule (no naming heuristics):
6524     * - Try to resolve the prefix (schema.object) as a table/alias/CTE in the current scope
6525     * - If neither resolves -> it's a package constant, NOT a column reference
6526     * - If either resolves -> it's a real column reference
6527     *
6528     * @param objectName The TObjectName to check
6529     * @param parent The parent node (may be null for some TObjectName nodes)
6530     * @return true if this is a PL/SQL package constant (should NOT be collected as column)
6531     */
6532    private boolean isPlsqlPackageConstant(TObjectName objectName, TParseTreeNode parent) {
6533        // Gating condition 1: Oracle vendor only
6534        if (dbVendor != EDbVendor.dbvoracle) {
6535            return false;
6536        }
6537
6538        // Gating condition 2: Must be inside a PL/SQL block
6539        if (currentPlsqlBlockScope == null) {
6540            return false;
6541        }
6542
6543        // Gating condition 3: Must be multi-part name (schema.object.part)
6544        // e.g., sch.pk_constv2.c_cdsl where:
6545        //   - schemaToken = "sch"
6546        //   - tableToken = "pk_constv2"
6547        //   - partToken = "c_cdsl"
6548        if (objectName.getSchemaToken() == null ||
6549            objectName.getTableToken() == null ||
6550            objectName.getPartToken() == null) {
6551            return false;
6552        }
6553
6554        // Gating condition 4: Should not be in DDL definition context
6555        // Skip if parent indicates a DDL context (these are already filtered earlier,
6556        // but check here for safety). For VALUES clause expressions, parent is often null.
6557        if (parent instanceof TColumnDefinition || parent instanceof TTable) {
6558            return false;
6559        }
6560
6561        // Now check if the prefix is resolvable as a table/alias in the current scope
6562        IScope scope = determineColumnScope(objectName);
6563        if (scope == null) {
6564            // Conservative: if we can't determine scope, don't filter
6565            return false;
6566        }
6567
6568        String schemaName = objectName.getSchemaString();   // "sch"
6569        String qualifier = objectName.getTableString();      // "pk_constv2"
6570
6571        // Try to resolve as table alias or table name
6572        INamespace ns1 = scope.resolveTable(qualifier);
6573
6574        // Try to resolve as schema-qualified table name
6575        INamespace ns2 = null;
6576        if (schemaName != null && !schemaName.isEmpty()) {
6577            ns2 = scope.resolveTable(schemaName + "." + qualifier);
6578        }
6579
6580        if (ns1 == null && ns2 == null) {
6581            // Not resolvable as a table/alias in this scope
6582            // -> Treat as package constant, NOT a column reference
6583            if (DEBUG_SCOPE_BUILD) {
6584                System.out.println("[DEBUG] Filtered PL/SQL package constant: " +
6585                    objectName.toString() + " (prefix '" + schemaName + "." + qualifier +
6586                    "' not resolvable in scope)");
6587            }
6588            return true;
6589        }
6590
6591        // Resolvable as a table/alias -> treat as real column reference
6592        return false;
6593    }
6594
6595
6596    /**
6597     * SQL Server datepart keywords - used in DATEDIFF, DATEADD, DATEPART, DATENAME functions.
6598     * These are parsed as TObjectName but are actually date/time keywords, not columns.
6599     */
6600    private static final Set<String> SQL_SERVER_DATEPART_KEYWORDS = new HashSet<>(Arrays.asList(
6601        // Standard datepart values
6602        "year", "yy", "yyyy",
6603        "quarter", "qq", "q",
6604        "month", "mm", "m",
6605        "dayofyear", "dy", "y",
6606        "day", "dd", "d",
6607        "week", "wk", "ww",
6608        "weekday", "dw", "w",
6609        "hour", "hh",
6610        "minute", "mi", "n",
6611        "second", "ss", "s",
6612        "millisecond", "ms",
6613        "microsecond", "mcs",
6614        "nanosecond", "ns",
6615        // ISO week
6616        "iso_week", "isowk", "isoww",
6617        // Timezone offset
6618        "tzoffset", "tz"
6619    ));
6620
6621    /**
6622     * Check if a name is a niladic function (built-in function without parentheses)
6623     * for the current database vendor.
6624     *
6625     * Uses TNiladicFunctionUtil with builtinFunctions/niladicFunctions.properties file.
6626     *
6627     * Niladic functions are functions that can be called without parentheses and look like
6628     * column references but are actually function calls returning values.
6629     * Examples: CURRENT_USER (SQL Server), CURRENT_DATETIME (BigQuery), SYSDATE (Oracle)
6630     *
6631     * Note: Regular built-in functions that require parentheses (like DAY(date), COUNT(*))
6632     * are NOT filtered here because they would have parentheses in the SQL and thus be
6633     * parsed as TFunctionCall nodes, not TObjectName.
6634     *
6635     * @param name The identifier name to check
6636     * @return true if it's a known niladic function for the current vendor
6637     */
6638    private boolean isBuiltInFunctionName(String name) {
6639        if (name == null || name.isEmpty()) {
6640            return false;
6641        }
6642
6643        boolean isNiladic = TNiladicFunctionUtil.isNiladicFunction(dbVendor, name);
6644
6645        if (DEBUG_SCOPE_BUILD && isNiladic) {
6646            System.out.println("[DEBUG] Identified niladic function: " + name + " for vendor " + dbVendor);
6647        }
6648
6649        return isNiladic;
6650    }
6651
6652    /**
6653     * Check if the given name is a Snowflake procedure system variable.
6654     * These are special variables available in Snowflake stored procedures:
6655     * - SQLROWCOUNT: Number of rows affected by the last SQL statement
6656     * - SQLERRM: Error message of the last SQL statement
6657     * - SQLSTATE: SQL state code of the last SQL statement
6658     * - SQLCODE: Deprecated, replaced by SQLSTATE
6659     */
6660    private boolean isSnowflakeProcedureSystemVariable(String name) {
6661        if (name == null || name.isEmpty()) {
6662            return false;
6663        }
6664        String upperName = name.toUpperCase();
6665        return "SQLROWCOUNT".equals(upperName) ||
6666               "SQLERRM".equals(upperName) ||
6667               "SQLSTATE".equals(upperName) ||
6668               "SQLCODE".equals(upperName);
6669    }
6670
6671    /**
6672     * Find a Snowflake stage table in the current scope.
6673     * Stage tables are identified by their table name starting with '@' or being a quoted
6674     * string starting with '@' (e.g., '@stage/path' or '@schema.stage_name').
6675     *
6676     * @return The stage table if found, null otherwise
6677     */
6678    private TTable findSnowflakeStageTableInScope() {
6679        // Check if we have a current select scope with a FROM scope
6680        if (currentSelectScope == null) {
6681            if (DEBUG_SCOPE_BUILD) {
6682                System.out.println("[DEBUG] findSnowflakeStageTableInScope: currentSelectScope is null");
6683            }
6684            return null;
6685        }
6686
6687        FromScope fromScope = currentSelectScope.getFromScope();
6688        if (fromScope == null) {
6689            if (DEBUG_SCOPE_BUILD) {
6690                System.out.println("[DEBUG] findSnowflakeStageTableInScope: fromScope is null");
6691            }
6692            return null;
6693        }
6694
6695        if (DEBUG_SCOPE_BUILD) {
6696            System.out.println("[DEBUG] findSnowflakeStageTableInScope: fromScope has " +
6697                fromScope.getChildren().size() + " children");
6698        }
6699
6700        // Search for stage tables through the FromScope's children (namespaces)
6701        for (ScopeChild child : fromScope.getChildren()) {
6702            INamespace namespace = child.getNamespace();
6703            if (namespace != null) {
6704                TTable table = namespace.getFinalTable();
6705                if (DEBUG_SCOPE_BUILD) {
6706                    System.out.println("[DEBUG]   child: alias=" + child.getAlias() +
6707                        " table=" + (table != null ? table.getTableName() : "null") +
6708                        " isStage=" + (table != null ? isSnowflakeStageTable(table) : "N/A"));
6709                }
6710                if (table != null && isSnowflakeStageTable(table)) {
6711                    return table;
6712                }
6713            }
6714        }
6715
6716        return null;
6717    }
6718
6719    /**
6720     * Check if a TTable is a Snowflake stage table.
6721     * Stage tables can be identified by:
6722     * - Table type is stageReference
6723     * - Table name starting with '@' (internal stage)
6724     * - Quoted string starting with '@' (external stage with path)
6725     *
6726     * @param table The table to check
6727     * @return true if this is a stage table
6728     */
6729    private boolean isSnowflakeStageTable(TTable table) {
6730        if (table == null) {
6731            return false;
6732        }
6733
6734        // Check for stageReference table type (e.g., @schema.stage_name/path)
6735        if (table.getTableType() == ETableSource.stageReference) {
6736            return true;
6737        }
6738
6739        if (table.getTableName() == null) {
6740            return false;
6741        }
6742
6743        String tableName = table.getTableName().toString();
6744        if (tableName == null || tableName.isEmpty()) {
6745            return false;
6746        }
6747
6748        // Check for stage table patterns:
6749        // - @stage_name
6750        // - '@stage/path/'
6751        // - @schema.stage_name
6752        return tableName.startsWith("@") ||
6753               tableName.startsWith("'@") ||
6754               tableName.startsWith("\"@");
6755    }
6756
6757    /**
6758     * Check if a TObjectName represents a Snowflake stage file positional column.
6759     * Snowflake stage files allow positional column references like $1, $2, etc.,
6760     * and JSON path access like $1:field.
6761     *
6762     * Patterns recognized:
6763     * - Simple positional: $1, $2, $10, etc. (columnNameOnly = "$1")
6764     * - JSON path: $1:fieldName (objectString = "$1", columnNameOnly = ":fieldName")
6765     *
6766     * @param objectName The object name to check
6767     * @return true if this is a Snowflake stage file positional column
6768     */
6769    private boolean isSnowflakeStagePositionalColumn(TObjectName objectName) {
6770        if (objectName == null) {
6771            return false;
6772        }
6773
6774        String colName = objectName.getColumnNameOnly();
6775        String objStr = objectName.getObjectString();
6776
6777        // Pattern 1: Simple positional column ($1, $2, etc.)
6778        // columnNameOnly = "$1", objectString = ""
6779        if (colName != null && colName.length() >= 2 && colName.startsWith("$")) {
6780            char secondChar = colName.charAt(1);
6781            if (Character.isDigit(secondChar)) {
6782                // Verify all remaining chars are digits
6783                int i = 2;
6784                while (i < colName.length() && Character.isDigit(colName.charAt(i))) {
6785                    i++;
6786                }
6787                if (i == colName.length()) {
6788                    return true;
6789                }
6790            }
6791        }
6792
6793        // Pattern 2: JSON path access ($1:fieldName)
6794        // objectString = "$1", columnNameOnly = ":fieldName"
6795        if (objStr != null && objStr.length() >= 2 && objStr.startsWith("$")) {
6796            char secondChar = objStr.charAt(1);
6797            if (Character.isDigit(secondChar)) {
6798                // Verify remaining chars are digits
6799                int i = 2;
6800                while (i < objStr.length() && Character.isDigit(objStr.charAt(i))) {
6801                    i++;
6802                }
6803                // If objectString is pure positional ($1, $12, etc.) and columnNameOnly starts with ':'
6804                if (i == objStr.length() && colName != null && colName.startsWith(":")) {
6805                    return true;
6806                }
6807            }
6808        }
6809
6810        return false;
6811    }
6812
6813    /**
6814     * Check if a variable name exists in the current PL/SQL block scope chain.
6815     * This walks up the scope chain from the current scope to all parent PL/SQL scopes.
6816     * Used to filter out variable references that should not be collected as column references.
6817     *
6818     * @param variableName The variable name to check (case-insensitive)
6819     * @return true if the variable exists in any scope in the chain
6820     */
6821    private boolean isVariableInPlsqlScopeChain(String variableName) {
6822        // Check the current scope first
6823        if (currentPlsqlBlockScope != null &&
6824            currentPlsqlBlockScope.getVariableNamespace().hasColumn(variableName) == ColumnLevel.EXISTS) {
6825            return true;
6826        }
6827
6828        // Check all parent scopes in the stack
6829        // The stack contains saved parent scopes when we enter nested blocks
6830        for (PlsqlBlockScope parentScope : plsqlBlockScopeStack) {
6831            if (parentScope.getVariableNamespace().hasColumn(variableName) == ColumnLevel.EXISTS) {
6832                return true;
6833            }
6834        }
6835
6836        return false;
6837    }
6838
6839    /**
6840     * Check if a TObjectName is a lambda expression parameter by examining the parent chain.
6841     * Lambda parameters (e.g., x in "x -> x + 1" or acc, x in "(acc, x) -> acc + x")
6842     * should not be treated as column references.
6843     *
6844     * This method is called when lambdaParameters set hasn't been populated yet
6845     * (because TObjectName is visited before TExpression for the lambda).
6846     *
6847     * @param objectName The object name to check
6848     * @return true if it's a lambda parameter
6849     */
6850    private boolean isLambdaParameter(TObjectName objectName) {
6851        if (objectName == null) return false;
6852        if (lambdaParameterStack.isEmpty()) return false;
6853
6854        // Check if this objectName's name matches any parameter in the current lambda context
6855        String name = objectName.toString();
6856        if (name == null) return false;
6857        String nameLower = name.toLowerCase();
6858
6859        // Check all lambda contexts on the stack (for nested lambdas)
6860        for (Set<String> paramNames : lambdaParameterStack) {
6861            if (paramNames.contains(nameLower)) {
6862                // Also add to lambdaParameters for future reference
6863                lambdaParameters.add(objectName);
6864                return true;
6865            }
6866        }
6867
6868        return false;
6869    }
6870
6871    /**
6872     * Check if a name is a known SQL Server schema name.
6873     * Schema names in SQL Server include system schemas and common user schemas.
6874     *
6875     * When a 2-part function name like "dbo.ufnGetInventoryStock" is encountered,
6876     * we need to distinguish between:
6877     * - schema.function() call (e.g., dbo.ufnGetInventoryStock) - NOT a column reference
6878     * - column.method() call (e.g., Demographics.value) - IS a column reference
6879     *
6880     * We use schema name detection to identify the former case.
6881     *
6882     * @param name The potential schema name to check
6883     * @return true if it's a known SQL Server schema name
6884     */
6885    private boolean isSqlServerSchemaName(String name) {
6886        if (name == null || name.isEmpty()) {
6887            return false;
6888        }
6889
6890        String upperName = name.toUpperCase();
6891
6892        // System schemas
6893        if (upperName.equals("DBO") ||
6894            upperName.equals("SYS") ||
6895            upperName.equals("INFORMATION_SCHEMA") ||
6896            upperName.equals("GUEST") ||
6897            upperName.equals("DB_OWNER") ||
6898            upperName.equals("DB_ACCESSADMIN") ||
6899            upperName.equals("DB_SECURITYADMIN") ||
6900            upperName.equals("DB_DDLADMIN") ||
6901            upperName.equals("DB_BACKUPOPERATOR") ||
6902            upperName.equals("DB_DATAREADER") ||
6903            upperName.equals("DB_DATAWRITER") ||
6904            upperName.equals("DB_DENYDATAREADER") ||
6905            upperName.equals("DB_DENYDATAWRITER")) {
6906            return true;
6907        }
6908
6909        return false;
6910    }
6911
6912    /**
6913     * Check if a TObjectName is a cursor name in cursor-related statements.
6914     * Cursor names appear in DECLARE CURSOR, OPEN, FETCH, CLOSE, DEALLOCATE statements
6915     * and should not be treated as column references.
6916     *
6917     * @param objectName The object name to check
6918     * @return true if it's a cursor name in a cursor-related statement
6919     */
6920    private static boolean isNumericLiteral(String text) {
6921        if (text == null || text.isEmpty()) return false;
6922        boolean hasDigit = false;
6923        boolean hasDot = false;
6924        for (int i = 0; i < text.length(); i++) {
6925            char c = text.charAt(i);
6926            if (c >= '0' && c <= '9') {
6927                hasDigit = true;
6928            } else if (c == '.' && !hasDot) {
6929                hasDot = true;
6930            } else {
6931                return false;
6932            }
6933        }
6934        return hasDigit;
6935    }
6936
6937    private boolean isCursorName(TObjectName objectName) {
6938        // Traverse up the parent chain to find cursor-related statements
6939        if (objectName.getDbObjectType() == EDbObjectType.cursor) return true;
6940
6941        TParseTreeNode node = objectName.getParentObjectName();
6942        while (node != null) {
6943            // SQL Server cursor statements
6944            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare) {
6945                gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare declare =
6946                    (gudusoft.gsqlparser.stmt.mssql.TMssqlDeclare) node;
6947                if (declare.getCursorName() == objectName) {
6948                    return true;
6949                }
6950            }
6951            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlOpen) {
6952                gudusoft.gsqlparser.stmt.mssql.TMssqlOpen open =
6953                    (gudusoft.gsqlparser.stmt.mssql.TMssqlOpen) node;
6954                if (open.getCursorName() == objectName) {
6955                    return true;
6956                }
6957            }
6958            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlFetch) {
6959                gudusoft.gsqlparser.stmt.mssql.TMssqlFetch fetch =
6960                    (gudusoft.gsqlparser.stmt.mssql.TMssqlFetch) node;
6961                if (fetch.getCursorName() == objectName) {
6962                    return true;
6963                }
6964            }
6965            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlClose) {
6966                gudusoft.gsqlparser.stmt.mssql.TMssqlClose close =
6967                    (gudusoft.gsqlparser.stmt.mssql.TMssqlClose) node;
6968                if (close.getCursorName() == objectName) {
6969                    return true;
6970                }
6971            }
6972            if (node instanceof gudusoft.gsqlparser.stmt.mssql.TMssqlDeallocate) {
6973                gudusoft.gsqlparser.stmt.mssql.TMssqlDeallocate deallocate =
6974                    (gudusoft.gsqlparser.stmt.mssql.TMssqlDeallocate) node;
6975                if (deallocate.getCursorName() == objectName) {
6976                    return true;
6977                }
6978            }
6979            // Generic cursor statements (used by other databases)
6980            if (node instanceof gudusoft.gsqlparser.stmt.TDeclareCursorStmt) {
6981                gudusoft.gsqlparser.stmt.TDeclareCursorStmt declare =
6982                    (gudusoft.gsqlparser.stmt.TDeclareCursorStmt) node;
6983                if (declare.getCursorName() == objectName) {
6984                    return true;
6985                }
6986            }
6987            if (node instanceof gudusoft.gsqlparser.stmt.TOpenStmt) {
6988                gudusoft.gsqlparser.stmt.TOpenStmt open =
6989                    (gudusoft.gsqlparser.stmt.TOpenStmt) node;
6990                if (open.getCursorName() == objectName) {
6991                    return true;
6992                }
6993            }
6994            if (node instanceof gudusoft.gsqlparser.stmt.TFetchStmt) {
6995                gudusoft.gsqlparser.stmt.TFetchStmt fetch =
6996                    (gudusoft.gsqlparser.stmt.TFetchStmt) node;
6997                if (fetch.getCursorName() == objectName) {
6998                    return true;
6999                }
7000            }
7001            if (node instanceof gudusoft.gsqlparser.stmt.TCloseStmt) {
7002                gudusoft.gsqlparser.stmt.TCloseStmt close =
7003                    (gudusoft.gsqlparser.stmt.TCloseStmt) node;
7004                if (close.getCursorName() == objectName) {
7005                    return true;
7006                }
7007            }
7008            node = node.getParentObjectName();
7009        }
7010        return false;
7011    }
7012
7013    /**
7014     * Check if a name is a SQL Server datepart keyword.
7015     * These keywords are used in DATEDIFF, DATEADD, DATEPART, DATENAME functions
7016     * and should not be treated as column references.
7017     *
7018     * @param name The identifier name to check
7019     * @return true if it's a known SQL Server datepart keyword
7020     */
7021    private boolean isSqlServerDatepartKeyword(String name) {
7022        if (name == null) {
7023            return false;
7024        }
7025        // Only apply this check for SQL Server and Azure SQL Database
7026        if (dbVendor != EDbVendor.dbvmssql && dbVendor != EDbVendor.dbvazuresql) {
7027            return false;
7028        }
7029        return SQL_SERVER_DATEPART_KEYWORDS.contains(name.toLowerCase());
7030    }
7031
7032    /**
7033     * SQL Server date functions that take a datepart keyword as first argument.
7034     */
7035    private static final Set<String> SQL_SERVER_DATE_FUNCTIONS = new HashSet<>(Arrays.asList(
7036        "datediff", "dateadd", "datepart", "datename", "datetrunc",
7037        "datediff_big"  // SQL Server 2016+
7038    ));
7039
7040    /**
7041     * Check if a TObjectName is in a date function context.
7042     * This verifies that the token is preceded by "FUNCTION_NAME(" pattern.
7043     *
7044     * @param objectName The object name to check
7045     * @return true if it appears to be a datepart argument in a date function
7046     */
7047    private boolean isInDateFunctionContext(TObjectName objectName) {
7048        TSourceToken startToken = objectName.getStartToken();
7049        if (startToken == null) {
7050            return false;
7051        }
7052
7053        // Use the token's container to access the token list
7054        TCustomSqlStatement stmt = objectName.getGsqlparser() != null ?
7055            (objectName.getGsqlparser().sqlstatements != null &&
7056             objectName.getGsqlparser().sqlstatements.size() > 0 ?
7057             objectName.getGsqlparser().sqlstatements.get(0) : null) : null;
7058        if (stmt == null || stmt.sourcetokenlist == null) {
7059            return false;
7060        }
7061        TSourceTokenList tokenList = stmt.sourcetokenlist;
7062
7063        // Find the position of our token
7064        int pos = startToken.posinlist;
7065        if (pos < 0) {
7066            return false;
7067        }
7068
7069        // Look for opening paren before this token (skipping whitespace)
7070        int parenPos = pos - 1;
7071        while (parenPos >= 0 && tokenList.get(parenPos).tokentype == ETokenType.ttwhitespace) {
7072            parenPos--;
7073        }
7074        if (parenPos < 0) {
7075            return false;
7076        }
7077
7078        TSourceToken parenToken = tokenList.get(parenPos);
7079        if (parenToken.tokentype != ETokenType.ttleftparenthesis) {
7080            return false;
7081        }
7082
7083        // Look for function name before the opening paren (skipping whitespace)
7084        int funcPos = parenPos - 1;
7085        while (funcPos >= 0 && tokenList.get(funcPos).tokentype == ETokenType.ttwhitespace) {
7086            funcPos--;
7087        }
7088        if (funcPos < 0) {
7089            return false;
7090        }
7091
7092        TSourceToken funcToken = tokenList.get(funcPos);
7093        String funcName = funcToken.toString().toLowerCase();
7094        return SQL_SERVER_DATE_FUNCTIONS.contains(funcName);
7095    }
7096
7097    /**
7098     * Check if a TObjectName is the alias part of SQL Server's proprietary column alias syntax.
7099     * In SQL Server, "column_alias = expression" is a valid way to alias a column.
7100     * The left side (column_alias) should not be treated as a column reference.
7101     *
7102     * Example: SELECT day_diff = DATEDIFF(DAY, start_date, end_date)
7103     * Here "day_diff" is an alias, not a column from any table.
7104     *
7105     * This method checks against the set populated by preVisit(TResultColumn).
7106     */
7107    private boolean isSqlServerProprietaryColumnAlias(TObjectName objectName, TParseTreeNode parent) {
7108        if (sqlServerProprietaryAliases.contains(objectName)) {
7109            if (DEBUG_SCOPE_BUILD) {
7110                System.out.println("[DEBUG] Skipping SQL Server proprietary column alias: " +
7111                        objectName.toString());
7112            }
7113            return true;
7114        }
7115        return false;
7116    }
7117
7118    /**
7119     * Determine which scope a column reference belongs to
7120     */
7121    private IScope determineColumnScope(TObjectName objectName) {
7122        // Special handling for ORDER BY columns in combined queries (UNION/INTERSECT/EXCEPT)
7123        // The parser moves ORDER BY from a branch to the combined query, but the column
7124        // should be resolved in the branch's scope where it originally appeared.
7125        if (currentSelectScope != null) {
7126            TSelectSqlStatement selectStmt = getStatementForScope(currentSelectScope);
7127            if (selectStmt != null && selectStmt.isCombinedQuery()) {
7128                // Check if column is in an ORDER BY clause
7129                if (isInOrderByClause(objectName, selectStmt)) {
7130                    // Find the branch that contains this column's position
7131                    SelectScope branchScope = findBranchScopeByPosition(selectStmt, objectName);
7132                    if (branchScope != null) {
7133                        return branchScope;
7134                    }
7135                }
7136            }
7137            return currentSelectScope;
7138        }
7139
7140        // Or use current UpdateScope if processing UPDATE statement
7141        if (currentUpdateScope != null) {
7142            return currentUpdateScope;
7143        }
7144
7145        // Or use current MergeScope if processing MERGE statement
7146        if (currentMergeScope != null) {
7147            return currentMergeScope;
7148        }
7149
7150        // Or use current DeleteScope if processing DELETE statement
7151        if (currentDeleteScope != null) {
7152            return currentDeleteScope;
7153        }
7154
7155        // Fallback to top of stack
7156        return scopeStack.isEmpty() ? globalScope : scopeStack.peek();
7157    }
7158
7159    /**
7160     * Get the TSelectSqlStatement for a SelectScope
7161     */
7162    private TSelectSqlStatement getStatementForScope(SelectScope scope) {
7163        for (Map.Entry<TSelectSqlStatement, SelectScope> entry : statementScopeMap.entrySet()) {
7164            if (entry.getValue() == scope) {
7165                return entry.getKey();
7166            }
7167        }
7168        return null;
7169    }
7170
7171    /**
7172     * Check if an object name is inside an ORDER BY clause
7173     */
7174    private boolean isInOrderByClause(TObjectName objectName, TSelectSqlStatement stmt) {
7175        TOrderBy orderBy = stmt.getOrderbyClause();
7176        if (orderBy == null) {
7177            return false;
7178        }
7179        // Check if objectName's position is within ORDER BY's position range
7180        long objOffset = objectName.getStartToken().posinlist;
7181        long orderByStart = orderBy.getStartToken().posinlist;
7182        long orderByEnd = orderBy.getEndToken().posinlist;
7183        return objOffset >= orderByStart && objOffset <= orderByEnd;
7184    }
7185
7186    /**
7187     * Find the branch SelectScope that contains the given column's position.
7188     * Returns null if no matching branch is found.
7189     */
7190    private SelectScope findBranchScopeByPosition(TSelectSqlStatement combinedStmt, TObjectName objectName) {
7191        if (!combinedStmt.isCombinedQuery()) {
7192            return null;
7193        }
7194
7195        long columnLine = objectName.getStartToken().lineNo;
7196
7197        // Search through branches recursively
7198        return findBranchScopeByLineRecursive(combinedStmt, columnLine);
7199    }
7200
7201    /**
7202     * Iteratively search for the branch that contains the given line number.
7203     * Uses explicit stack to avoid StackOverflow on deeply nested UNION chains.
7204     */
7205    private SelectScope findBranchScopeByLineRecursive(TSelectSqlStatement stmt, long targetLine) {
7206        Deque<TSelectSqlStatement> stack = new ArrayDeque<>();
7207        stack.push(stmt);
7208
7209        while (!stack.isEmpty()) {
7210            TSelectSqlStatement current = stack.pop();
7211
7212            if (!current.isCombinedQuery()) {
7213                // This is a leaf branch - check if it contains the target line
7214                if (current.tables != null && current.tables.size() > 0) {
7215                    for (int i = 0; i < current.tables.size(); i++) {
7216                        TTable table = current.tables.getTable(i);
7217                        if (table.getStartToken().lineNo == targetLine) {
7218                            return statementScopeMap.get(current);
7219                        }
7220                    }
7221                }
7222                // Alternative: check if statement's range includes the target line
7223                long stmtStartLine = current.getStartToken().lineNo;
7224                long stmtEndLine = current.getEndToken().lineNo;
7225                if (targetLine >= stmtStartLine && targetLine <= stmtEndLine) {
7226                    return statementScopeMap.get(current);
7227                }
7228            } else {
7229                // Combined query - push children (right first so left is processed first)
7230                if (current.getRightStmt() != null) {
7231                    stack.push(current.getRightStmt());
7232                }
7233                if (current.getLeftStmt() != null) {
7234                    stack.push(current.getLeftStmt());
7235                }
7236            }
7237        }
7238        return null;
7239    }
7240
7241    // ========== Accessors ==========
7242
7243    public GlobalScope getGlobalScope() {
7244        return globalScope;
7245    }
7246
7247    public INameMatcher getNameMatcher() {
7248        return nameMatcher;
7249    }
7250
7251    public Map<TUpdateSqlStatement, UpdateScope> getUpdateScopeMap() {
7252        return Collections.unmodifiableMap(updateScopeMap);
7253    }
7254
7255    public Map<TDeleteSqlStatement, DeleteScope> getDeleteScopeMap() {
7256        return Collections.unmodifiableMap(deleteScopeMap);
7257    }
7258
7259    /**
7260     * Get the mapping of USING columns to their right-side tables.
7261     * In JOIN...USING syntax, USING columns should preferentially resolve
7262     * to the right-side (physical) table for TGetTableColumn compatibility.
7263     *
7264     * @return Map of USING column TObjectName -> right-side TTable
7265     */
7266    public Map<TObjectName, TTable> getUsingColumnToRightTable() {
7267        return Collections.unmodifiableMap(usingColumnToRightTable);
7268    }
7269
7270    /**
7271     * Get the set of virtual trigger tables (deleted/inserted in SQL Server triggers).
7272     * These tables should be excluded from table output since their columns are
7273     * resolved to the trigger's target table.
7274     *
7275     * @return Set of TTable objects that are virtual trigger tables
7276     */
7277    public Set<TTable> getVirtualTriggerTables() {
7278        return Collections.unmodifiableSet(virtualTriggerTables);
7279    }
7280
7281    /**
7282     * Get the set of SET clause target columns (UPDATE SET left-side columns).
7283     * These columns already have sourceTable correctly set to the UPDATE target table
7284     * and should NOT be re-resolved through star column push-down.
7285     *
7286     * @return Set of TObjectName nodes that are SET clause target columns
7287     */
7288    public Set<TObjectName> getSetClauseTargetColumns() {
7289        return Collections.unmodifiableSet(setClauseTargetColumns);
7290    }
7291
7292    /**
7293     * Get the set of INSERT ALL target columns (from TInsertIntoValue columnList).
7294     * These columns already have sourceTable correctly set to the INSERT target table
7295     * and should NOT be re-resolved against the subquery scope.
7296     *
7297     * @return Set of TObjectName nodes that are INSERT ALL target columns
7298     */
7299    public Set<TObjectName> getInsertAllTargetColumns() {
7300        return Collections.unmodifiableSet(insertAllTargetColumns);
7301    }
7302
7303    /**
7304     * Get the map of MERGE INSERT VALUES columns to their USING (source) table.
7305     * After name resolution, the resolver should restore sourceTable for these columns
7306     * to ensure they correctly link to the USING table per MERGE semantics.
7307     *
7308     * @return Map of TObjectName to their USING table
7309     */
7310    public Map<TObjectName, TTable> getMergeInsertValuesColumns() {
7311        return Collections.unmodifiableMap(mergeInsertValuesColumns);
7312    }
7313}