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