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