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