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