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