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