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