001package gudusoft.gsqlparser.resolver2; 002 003import gudusoft.gsqlparser.TBaseType; 004import gudusoft.gsqlparser.TCustomSqlStatement; 005import gudusoft.gsqlparser.IRelation; 006import gudusoft.gsqlparser.TLog; 007import gudusoft.gsqlparser.TSourceToken; 008import gudusoft.gsqlparser.TStatementList; 009import gudusoft.gsqlparser.ETableSource; 010import gudusoft.gsqlparser.EDbVendor; 011import gudusoft.gsqlparser.EDbObjectType; 012import gudusoft.gsqlparser.EErrorType; 013import gudusoft.gsqlparser.ESqlClause; 014import gudusoft.gsqlparser.ESqlStatementType; 015import gudusoft.gsqlparser.TSyntaxError; 016import gudusoft.gsqlparser.stmt.dax.TDaxStmt; 017import gudusoft.gsqlparser.stmt.TAlterTableStatement; 018import gudusoft.gsqlparser.stmt.TCreateTableSqlStatement; 019import gudusoft.gsqlparser.stmt.TInsertSqlStatement; 020import gudusoft.gsqlparser.stmt.TUpdateSqlStatement; 021import gudusoft.gsqlparser.stmt.TDeleteSqlStatement; 022import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 023import gudusoft.gsqlparser.compiler.TContext; 024import gudusoft.gsqlparser.nodes.TObjectName; 025import gudusoft.gsqlparser.nodes.TObjectNameList; 026import gudusoft.gsqlparser.nodes.TTable; 027import gudusoft.gsqlparser.nodes.TJoinExpr; 028import gudusoft.gsqlparser.nodes.TParseTreeNode; 029import gudusoft.gsqlparser.nodes.TParseTreeVisitor; 030import gudusoft.gsqlparser.nodes.TResultColumn; 031import gudusoft.gsqlparser.nodes.TResultColumnList; 032import gudusoft.gsqlparser.nodes.TQualifyClause; 033import gudusoft.gsqlparser.nodes.TExpression; 034import gudusoft.gsqlparser.EExpressionType; 035import gudusoft.gsqlparser.resolver2.model.ColumnSource; 036import gudusoft.gsqlparser.resolver2.model.FromScopeIndex; 037import gudusoft.gsqlparser.resolver2.model.ResolutionContext; 038import gudusoft.gsqlparser.resolver2.model.ResolutionResult; 039import gudusoft.gsqlparser.resolver2.model.ResolutionStatistics; 040import gudusoft.gsqlparser.resolver2.ResolutionStatus; 041import gudusoft.gsqlparser.resolver2.result.IResolutionResult; 042import gudusoft.gsqlparser.resolver2.result.ResolutionResultImpl; 043import gudusoft.gsqlparser.resolver2.scope.FromScope; 044import gudusoft.gsqlparser.resolver2.scope.GlobalScope; 045import gudusoft.gsqlparser.resolver2.scope.IScope; 046import gudusoft.gsqlparser.resolver2.scope.SelectScope; 047import gudusoft.gsqlparser.resolver2.scope.CTEScope; 048import gudusoft.gsqlparser.resolver2.scope.GroupByScope; 049import gudusoft.gsqlparser.resolver2.scope.HavingScope; 050import gudusoft.gsqlparser.resolver2.scope.OrderByScope; 051import gudusoft.gsqlparser.resolver2.scope.UpdateScope; 052import gudusoft.gsqlparser.resolver2.scope.DeleteScope; 053import gudusoft.gsqlparser.resolver2.namespace.INamespace; 054import gudusoft.gsqlparser.resolver2.namespace.TableNamespace; 055import gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace; 056import gudusoft.gsqlparser.resolver2.namespace.CTENamespace; 057import gudusoft.gsqlparser.nodes.TCTE; 058import gudusoft.gsqlparser.nodes.TCTEList; 059import gudusoft.gsqlparser.nodes.TUnnestClause; 060import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 061import gudusoft.gsqlparser.resolver2.iterative.ConvergenceDetector; 062import gudusoft.gsqlparser.resolver2.iterative.ResolutionPass; 063import gudusoft.gsqlparser.resolver2.enhancement.NamespaceEnhancer; 064import gudusoft.gsqlparser.resolver2.enhancement.EnhancementResult; 065import gudusoft.gsqlparser.resolver2.enhancement.CollectedColumnRef; 066import gudusoft.gsqlparser.resolver2.metadata.BatchMetadataCollector; 067import gudusoft.gsqlparser.resolver2.context.DatabaseContextTracker; 068import gudusoft.gsqlparser.resolver2.namespace.CTENamespace; 069import gudusoft.gsqlparser.sqlenv.TSQLEnv; 070import gudusoft.gsqlparser.TAttributeNode; 071 072import java.util.ArrayList; 073import java.util.HashMap; 074import java.util.HashSet; 075import java.util.IdentityHashMap; 076import java.util.List; 077import java.util.Map; 078import java.util.Set; 079 080// ScopeBuilder for visitor-based scope construction 081import gudusoft.gsqlparser.resolver2.ScopeBuilder; 082import gudusoft.gsqlparser.resolver2.ScopeBuildResult; 083 084/** 085 * New SQL Resolver - Phase 2 Enhanced Framework 086 * 087 * This is the main entry point for the new resolution architecture. 088 * Provides improved column-to-table resolution with: 089 * - Clear scope-based name resolution 090 * - Full candidate collection for ambiguous cases 091 * - Confidence-scored inference 092 * - Better tracing and debugging 093 * 094 * Usage: 095 * <pre> 096 * TSQLResolver2 resolver = new TSQLResolver2(context, statements); 097 * boolean success = resolver.resolve(); 098 * ResolutionStatistics stats = resolver.getStatistics(); 099 * </pre> 100 * 101 * Phase 1 capabilities: 102 * - Basic SELECT statement resolution 103 * - Table and subquery namespaces 104 * - Qualified and unqualified column references 105 * - FROM clause scope management 106 * 107 * Phase 2 capabilities: 108 * - JOIN scope handling with nullable semantics 109 * - CTE (WITH clause) resolution 110 * - Iterative resolution framework (auto-converges after first pass if no iteration needed) 111 * 112 * Future phases will add: 113 * - Evidence-based inference 114 * - Star column expansion 115 */ 116public class TSQLResolver2 { 117 118 private final TContext globalContext; 119 private final TStatementList sqlStatements; 120 private final TSQLResolverConfig config; 121 private final ResolutionContext resolutionContext; 122 private final NameResolver nameResolver; 123 124 /** Global scope (root of scope tree) */ 125 private GlobalScope globalScope; 126 127 /** Convergence detector for iterative resolution */ 128 private ConvergenceDetector convergenceDetector; 129 130 /** History of all resolution passes */ 131 private final List<ResolutionPass> passHistory; 132 133 /** 134 * Scope cache for iterative resolution. 135 * Maps statements to their scope trees to avoid rebuilding scopes on each pass. 136 * Key: TCustomSqlStatement, Value: SelectScope (or other scope type) 137 */ 138 private final java.util.Map<Object, IScope> statementScopeCache; 139 140 /** 141 * Column-to-Scope mapping for iterative resolution (Principle 1: Scope完全复用). 142 * Built once in Pass 1, reused in Pass 2+ to avoid rebuilding scopes. 143 * Maps each TObjectName (column reference) to the IScope where it should be resolved. 144 */ 145 private final java.util.Map<TObjectName, IScope> columnToScopeMap; 146 147 /** 148 * FromScope index cache for O(1) table/namespace lookups (Performance Optimization B). 149 * Maps FromScope instances to their pre-built indexes. 150 * Built lazily on first access, cleared at the start of each resolve() call. 151 * Uses IdentityHashMap because we need object identity, not equals(). 152 */ 153 private final Map<IScope, FromScopeIndex> fromScopeIndexCache; 154 155 /** 156 * Cache for Teradata NAMED alias lookup. 157 * Maps SELECT statements to their alias index (alias name -> TResultColumn). 158 * Uses IdentityHashMap because we need object identity, not equals(). 159 * Optimization C: Reduces O(cols * select_items) to O(cols) for Teradata. 160 */ 161 private final Map<TSelectSqlStatement, Map<String, TResultColumn>> teradataNamedAliasCache; 162 163 /** 164 * All column references collected during Pass 1 (Principle 1: Scope完全复用). 165 * Used in Pass 2+ to re-resolve names without rebuilding the scope tree. 166 */ 167 private final List<TObjectName> allColumnReferences; 168 169 /** 170 * ScopeBuilder for visitor-based scope construction. 171 * Replaces manual scope building with proper nested scope handling. 172 */ 173 private final ScopeBuilder scopeBuilder; 174 175 /** 176 * Result from ScopeBuilder containing the complete scope tree. 177 * This is populated in Pass 1 and reused in Pass 2+. 178 */ 179 private ScopeBuildResult scopeBuildResult; 180 181 /** 182 * NamespaceEnhancer for explicit column collection and enhancement. 183 * Handles the explicit namespace enhancement phase between resolution passes. 184 * Columns are collected during resolution and added to namespaces explicitly. 185 */ 186 private NamespaceEnhancer namespaceEnhancer; 187 188 /** 189 * Create resolver with default configuration 190 */ 191 public TSQLResolver2(TContext context, TStatementList statements) { 192 this(context, statements, TSQLResolverConfig.createDefault()); 193 } 194 195 /** 196 * Create resolver with custom configuration 197 */ 198 public TSQLResolver2(TContext context, TStatementList statements, TSQLResolverConfig config) { 199 this.globalContext = context; 200 this.sqlStatements = statements; 201 this.config = config; 202 this.resolutionContext = new ResolutionContext(); 203 this.nameResolver = new NameResolver(config, resolutionContext); 204 this.passHistory = new ArrayList<>(); 205 this.statementScopeCache = new java.util.HashMap<>(); 206 this.columnToScopeMap = new java.util.HashMap<>(); 207 this.fromScopeIndexCache = new IdentityHashMap<>(); 208 this.teradataNamedAliasCache = new IdentityHashMap<>(); 209 this.allColumnReferences = new ArrayList<>(); 210 211 // Initialize ScopeBuilder for visitor-based scope construction 212 this.scopeBuilder = new ScopeBuilder(context, config.getNameMatcher()); 213 // Pass guessColumnStrategy from config for namespace isolation (prevents test side effects) 214 if (config.hasCustomGuessColumnStrategy()) { 215 this.scopeBuilder.setGuessColumnStrategy(config.getGuessColumnStrategy()); 216 } 217 218 // If context is null, try to get TSQLEnv from statements 219 // This allows TSQLEnv to flow from parser.setSqlEnv() through statements 220 if (statements != null && statements.size() > 0) { 221 try { 222 TCustomSqlStatement firstStmt = statements.get(0); 223 if (firstStmt != null && firstStmt.getGlobalScope() != null && 224 firstStmt.getGlobalScope().getSqlEnv() != null) { 225 this.scopeBuilder.setSqlEnv(firstStmt.getGlobalScope().getSqlEnv()); 226 } 227 } catch (Exception e) { 228 // Silently ignore - SQLEnv is optional enhancement 229 } 230 } 231 232 // Initialize convergence detector for iterative resolution 233 this.convergenceDetector = new ConvergenceDetector( 234 config.getMaxIterations(), 235 config.getStablePassesForConvergence(), 236 config.getMinProgressRate() 237 ); 238 239 // Initialize namespace enhancer for explicit column collection 240 // Debug mode follows the global resolver log setting 241 this.namespaceEnhancer = new NamespaceEnhancer(TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE); 242 } 243 244 /** 245 * Set the TSQLEnv to use for table metadata lookup. 246 * This allows external callers to provide TSQLEnv if automatic detection fails. 247 * 248 * @param sqlEnv the SQL environment containing table metadata 249 */ 250 public void setSqlEnv(gudusoft.gsqlparser.sqlenv.TSQLEnv sqlEnv) { 251 if (scopeBuilder != null) { 252 scopeBuilder.setSqlEnv(sqlEnv); 253 } 254 } 255 256 /** 257 * Get the TSQLEnv used for table metadata lookup. 258 * 259 * @return the SQL environment, or null if not set 260 */ 261 public gudusoft.gsqlparser.sqlenv.TSQLEnv getSqlEnv() { 262 return scopeBuilder != null ? scopeBuilder.getSqlEnv() : null; 263 } 264 265 /** 266 * Get the set of virtual trigger tables (deleted/inserted in SQL Server triggers). 267 * These tables should be excluded from table output since their columns are 268 * resolved to the trigger's target table. 269 * 270 * @return Set of TTable objects that are virtual trigger tables 271 */ 272 public java.util.Set<gudusoft.gsqlparser.nodes.TTable> getVirtualTriggerTables() { 273 return scopeBuilder != null ? scopeBuilder.getVirtualTriggerTables() : java.util.Collections.emptySet(); 274 } 275 276 /** 277 * Get the SQL statements being resolved. 278 * 279 * @return the list of SQL statements 280 */ 281 public TStatementList getStatements() { 282 return sqlStatements; 283 } 284 285 // Performance timing fields (instance-level for single resolve() call) 286 private long timeScopeBuilder = 0; 287 private long timeNameResolution = 0; 288 private long timeEnhancement = 0; 289 private long timeLegacySync = 0; 290 private long timeOther = 0; 291 292 // Global accumulators for profiling across all resolve() calls 293 private static long globalTimeScopeBuilder = 0; 294 private static long globalTimeNameResolution = 0; 295 private static long globalTimeEnhancement = 0; 296 private static long globalTimeLegacySync = 0; 297 private static long globalTimeOther = 0; 298 private static int globalResolveCount = 0; 299 300 /** 301 * Reset global timing accumulators. 302 */ 303 public static void resetGlobalTimings() { 304 globalTimeScopeBuilder = 0; 305 globalTimeNameResolution = 0; 306 globalTimeEnhancement = 0; 307 globalTimeLegacySync = 0; 308 globalTimeOther = 0; 309 globalResolveCount = 0; 310 // Reset detailed legacy sync timings 311 globalTimeClearLinked = 0; 312 globalTimeFillAttributes = 0; 313 globalTimeSyncColumns = 0; 314 globalTimePopulateOrphans = 0; 315 globalTimeClearHints = 0; 316 } 317 318 /** 319 * Get global performance timing breakdown for profiling across all resolve() calls. 320 * @return formatted timing information 321 */ 322 public static String getGlobalPerformanceTimings() { 323 long total = globalTimeScopeBuilder + globalTimeNameResolution + globalTimeEnhancement + globalTimeLegacySync + globalTimeOther; 324 return String.format( 325 "TSQLResolver2 Global Timings (across %d resolve() calls):\n" + 326 " ScopeBuilder: %d ms (%.1f%%)\n" + 327 " NameResolution: %d ms (%.1f%%)\n" + 328 " Enhancement: %d ms (%.1f%%)\n" + 329 " LegacySync: %d ms (%.1f%%)\n" + 330 " Other: %d ms (%.1f%%)\n" + 331 " Total: %d ms", 332 globalResolveCount, 333 globalTimeScopeBuilder, total > 0 ? 100.0 * globalTimeScopeBuilder / total : 0, 334 globalTimeNameResolution, total > 0 ? 100.0 * globalTimeNameResolution / total : 0, 335 globalTimeEnhancement, total > 0 ? 100.0 * globalTimeEnhancement / total : 0, 336 globalTimeLegacySync, total > 0 ? 100.0 * globalTimeLegacySync / total : 0, 337 globalTimeOther, total > 0 ? 100.0 * globalTimeOther / total : 0, 338 total); 339 } 340 341 /** 342 * Get performance timing breakdown for profiling. 343 * @return formatted timing information 344 */ 345 public String getPerformanceTimings() { 346 long total = timeScopeBuilder + timeNameResolution + timeEnhancement + timeLegacySync + timeOther; 347 return String.format( 348 "TSQLResolver2 Timings:\n" + 349 " ScopeBuilder: %d ms (%.1f%%)\n" + 350 " NameResolution: %d ms (%.1f%%)\n" + 351 " Enhancement: %d ms (%.1f%%)\n" + 352 " LegacySync: %d ms (%.1f%%)\n" + 353 " Other: %d ms (%.1f%%)\n" + 354 " Total: %d ms", 355 timeScopeBuilder, total > 0 ? 100.0 * timeScopeBuilder / total : 0, 356 timeNameResolution, total > 0 ? 100.0 * timeNameResolution / total : 0, 357 timeEnhancement, total > 0 ? 100.0 * timeEnhancement / total : 0, 358 timeLegacySync, total > 0 ? 100.0 * timeLegacySync / total : 0, 359 timeOther, total > 0 ? 100.0 * timeOther / total : 0, 360 total); 361 } 362 363 /** 364 * Perform resolution on all SQL statements 365 */ 366 public boolean resolve() { 367 // Reset timing counters 368 timeScopeBuilder = 0; 369 timeNameResolution = 0; 370 timeEnhancement = 0; 371 timeLegacySync = 0; 372 timeOther = 0; 373 374 // Setup logging 375 TLog.clearLogs(); 376 if (!TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 377 TLog.disableLog(); 378 } else { 379 TLog.enableAllLevelLog(); 380 } 381 382 try { 383 logInfo("Starting TSQLResolver2.resolve()"); 384 385 long startTime = System.currentTimeMillis(); 386 387 // Delta 1: Collect metadata from DDL statements if no SQLEnv provided 388 if (getSqlEnv() == null) { 389 collectBatchMetadata(); 390 } 391 392 // Delta 4: Track database context from USE/SET statements 393 trackDatabaseContext(); 394 395 // Phase 1: Build global scope (once for all passes) 396 buildGlobalScope(); 397 398 timeOther += System.currentTimeMillis() - startTime; 399 400 // Phase 2: Perform iterative resolution 401 // (automatically completes after first pass if no second pass is needed) 402 return performIterativeResolution(); 403 404 } catch (Exception e) { 405 logError("Exception in TSQLResolver2.resolve(): " + e.getMessage()); 406 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 407 e.printStackTrace(); 408 } 409 return false; 410 } finally { 411 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 412 TBaseType.dumpLogs(false); 413 } 414 } 415 } 416 417 /** 418 * Perform iterative resolution. 419 * Automatically converges after first pass if no additional passes are needed. 420 * 421 * Architecture: 422 * - Pass 1: Build scope tree + initial name resolution 423 * - Pass 2-N: Reuse scope tree, collect evidence, infer columns, re-resolve names 424 * 425 * This separation allows: 426 * 1. Scopes to accumulate inferred columns across iterations 427 * 2. Later scopes to reference earlier scopes' inferred columns 428 * 3. Forward references to be resolved in subsequent passes 429 */ 430 private boolean performIterativeResolution() { 431 logInfo("Performing iterative resolution (max iterations: " + config.getMaxIterations() + ")"); 432 433 int passNumber = 1; 434 ResolutionStatistics previousStats = null; 435 boolean continueIterating = true; 436 boolean scopesBuilt = false; 437 438 while (continueIterating) { 439 logInfo("=== Pass " + passNumber + " ==="); 440 441 // Create a resolution pass 442 ResolutionPass pass = new ResolutionPass(passNumber, previousStats); 443 444 if (passNumber == 1) { 445 // ========== PASS 1: Build scope tree + initial resolution ========== 446 logInfo("Pass 1: Building scope tree using ScopeBuilder and performing initial resolution"); 447 448 // Clear all state for fresh start 449 resolutionContext.clear(); 450 columnToScopeMap.clear(); 451 fromScopeIndexCache.clear(); 452 dmlIndexCache.clear(); 453 teradataNamedAliasCache.clear(); 454 allColumnReferences.clear(); 455 456 // Use ScopeBuilder to build complete scope tree (handles all nesting correctly) 457 long scopeBuilderStart = System.currentTimeMillis(); 458 scopeBuildResult = scopeBuilder.build(sqlStatements); 459 460 // Get global scope from builder 461 globalScope = scopeBuildResult.getGlobalScope(); 462 463 // Copy column references and scope mappings from ScopeBuildResult 464 columnToScopeMap.putAll(scopeBuildResult.getColumnToScopeMap()); 465 allColumnReferences.addAll(scopeBuildResult.getAllColumnReferences()); 466 timeScopeBuilder += System.currentTimeMillis() - scopeBuilderStart; 467 468 logInfo("ScopeBuilder complete: " + scopeBuildResult.getStatistics()); 469 logInfo("Built " + scopeBuildResult.getStatementScopeMap().size() + " SelectScopes"); 470 471 // Initialize NamespaceEnhancer with scope tree (caches star namespaces) 472 namespaceEnhancer.initialize(scopeBuildResult); 473 namespaceEnhancer.startPass(passNumber); 474 475 // Get SET clause target columns that should not be re-resolved 476 Set<TObjectName> setClauseTargetColumns = scopeBuilder.getSetClauseTargetColumns(); 477 478 // Get INSERT ALL target columns that should not be re-resolved 479 Set<TObjectName> insertAllTargetColumns = scopeBuilder.getInsertAllTargetColumns(); 480 481 // Perform initial name resolution for all collected columns 482 logInfo("Performing initial name resolution for " + allColumnReferences.size() + " column references"); 483 long nameResStart = System.currentTimeMillis(); 484 for (TObjectName objName : allColumnReferences) { 485 // Skip SET clause target columns - they already have sourceTable correctly set 486 // to the UPDATE target table and should NOT be resolved through star columns 487 if (setClauseTargetColumns.contains(objName)) { 488 continue; 489 } 490 491 // Skip INSERT ALL target columns - they already have sourceTable correctly set 492 // to the INSERT target table and should NOT be resolved against the subquery scope 493 if (insertAllTargetColumns.contains(objName)) { 494 continue; 495 } 496 497 IScope scope = columnToScopeMap.get(objName); 498 if (scope != null) { 499 nameResolver.resolve(objName, scope); 500 501 // Handle USING column priority for JOIN...USING syntax 502 handleUsingColumnResolution(objName); 503 504 // Handle Teradata NAMED alias resolution 505 handleTeradataNamedAliasResolution(objName); 506 handleQualifyClauseAliasResolution(objName); 507 508 // Handle subquery aliased/calculated column resolution 509 // Ensures aliased columns don't incorrectly trace to base tables 510 handleSubqueryAliasedColumnResolution(objName); 511 512 // Collect unresolved references for enhancement 513 collectForEnhancementIfNeeded(objName, scope); 514 } 515 } 516 timeNameResolution += System.currentTimeMillis() - nameResStart; 517 518 // Explicit Enhancement Phase: Add collected columns to namespaces 519 long enhanceStart = System.currentTimeMillis(); 520 EnhancementResult enhanceResult = namespaceEnhancer.enhance(); 521 timeEnhancement += System.currentTimeMillis() - enhanceStart; 522 logInfo("Pass 1 enhancement: " + enhanceResult.getTotalAdded() + " columns added to namespaces"); 523 524 scopesBuilt = true; 525 logInfo("Pass 1 complete. Resolved " + allColumnReferences.size() + " column references."); 526 527 528 } else { 529 // ========== PASS 2+: Explicit Enhancement + Re-resolve ========== 530 logInfo("Pass " + passNumber + ": Explicit namespace enhancement and re-resolution"); 531 532 // ======== Phase A: Start New Pass ======== 533 namespaceEnhancer.startPass(passNumber); 534 535 // ======== Phase B: Clear Resolution Results (keep scopes!) ======== 536 logInfo("Phase B: Clearing resolution results (scopes preserved)"); 537 resolutionContext.clear(); 538 539 // ======== Phase C: Re-resolve with Enhanced Namespaces ======== 540 logInfo("Phase C: Re-resolving with enhanced namespaces"); 541 542 // Get SET clause target columns that should not be re-resolved 543 Set<TObjectName> setClauseTargetColumns = scopeBuilder.getSetClauseTargetColumns(); 544 545 // Get INSERT ALL target columns that should not be re-resolved 546 Set<TObjectName> insertAllTargetColumns = scopeBuilder.getInsertAllTargetColumns(); 547 548 // Re-resolve all column references using their original scopes 549 // Scopes are reused from Pass 1, but namespaces may have been enhanced 550 for (TObjectName objName : allColumnReferences) { 551 // Skip SET clause target columns - they already have sourceTable correctly set 552 // to the UPDATE target table and should NOT be resolved through star columns 553 if (setClauseTargetColumns.contains(objName)) { 554 continue; 555 } 556 557 // Skip INSERT ALL target columns - they already have sourceTable correctly set 558 // to the INSERT target table and should NOT be resolved against the subquery scope 559 if (insertAllTargetColumns.contains(objName)) { 560 continue; 561 } 562 563 IScope scope = columnToScopeMap.get(objName); 564 if (scope != null) { 565 nameResolver.resolve(objName, scope); 566 567 // Handle USING column priority for JOIN...USING syntax 568 handleUsingColumnResolution(objName); 569 570 // Handle Teradata NAMED alias resolution 571 handleTeradataNamedAliasResolution(objName); 572 handleQualifyClauseAliasResolution(objName); 573 574 // Handle subquery aliased/calculated column resolution 575 // Ensures aliased columns don't incorrectly trace to base tables 576 handleSubqueryAliasedColumnResolution(objName); 577 578 // Collect for next enhancement pass if still targets star namespace 579 collectForEnhancementIfNeeded(objName, scope); 580 } 581 } 582 583 // ======== Phase D: Explicit Namespace Enhancement ======== 584 logInfo("Phase D: Explicit namespace enhancement"); 585 EnhancementResult enhanceResult = namespaceEnhancer.enhance(); 586 logInfo("Pass " + passNumber + " enhancement: " + 587 enhanceResult.getTotalAdded() + " columns added, " + 588 enhanceResult.getTotalSkipped() + " skipped (existing)"); 589 590 // Legacy support: also run old evidence collection (if needed) 591 if (config.isEvidenceCollectionEnabled()) { 592 runLegacyEvidenceCollection(); 593 } 594 } 595 596 // Get statistics after this pass 597 ResolutionStatistics currentStats = getStatistics(); 598 pass.complete(currentStats); 599 600 // Record this pass 601 convergenceDetector.recordPass(pass); 602 passHistory.add(pass); 603 604 logInfo(pass.getSummary()); 605 606 // Check convergence 607 ConvergenceDetector.ConvergenceResult convergence = convergenceDetector.checkConvergence(); 608 if (convergence.hasConverged()) { 609 logInfo("Convergence detected: " + convergence.getReason()); 610 pass.setStopReason(convergence.getReason()); 611 continueIterating = false; 612 } else { 613 // Prepare for next pass 614 previousStats = currentStats; 615 passNumber++; 616 } 617 } 618 619 // Create cloned columns for star column tracing 620 // This is a CORE part of TSQLResolver2 - when a column traces through a CTE/subquery 621 // with SELECT * to a physical table, we create a cloned TObjectName with sourceTable 622 // pointing to the traced physical table. This ensures complete lineage tracking. 623 createTracedColumnClones(); 624 625 // Sync to legacy structures if enabled 626 if (config.isLegacyCompatibilityEnabled()) { 627 long syncStart = System.currentTimeMillis(); 628 syncToLegacyStructures(); 629 timeLegacySync += System.currentTimeMillis() - syncStart; 630 } 631 632 // Print final statistics 633 logInfo("Iterative resolution complete after " + passHistory.size() + " passes"); 634 ResolutionStatistics finalStats = getStatistics(); 635 logInfo("Final statistics: " + finalStats); 636 637 // Print namespace enhancement summary if in debug mode 638 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 639 logInfo("=== Namespace Enhancement Summary ==="); 640 logInfo("Total columns added: " + namespaceEnhancer.getTotalColumnsAdded()); 641 } 642 643 // Print performance timing breakdown 644 logInfo(getPerformanceTimings()); 645 646 // Accumulate to global timings for profiling 647 globalTimeScopeBuilder += timeScopeBuilder; 648 globalTimeNameResolution += timeNameResolution; 649 globalTimeEnhancement += timeEnhancement; 650 globalTimeLegacySync += timeLegacySync; 651 globalTimeOther += timeOther; 652 globalResolveCount++; 653 654 return true; 655 } 656 657 /** 658 * Run legacy evidence collection (deprecated). 659 * Kept for backward compatibility. 660 */ 661 @SuppressWarnings("deprecation") 662 private void runLegacyEvidenceCollection() { 663 logInfo("Running legacy evidence collection (deprecated)"); 664 665 gudusoft.gsqlparser.resolver2.inference.EvidenceCollector evidenceCollector = 666 new gudusoft.gsqlparser.resolver2.inference.EvidenceCollector(); 667 668 int evidenceCount = 0; 669 for (int i = 0; i < sqlStatements.size(); i++) { 670 Object stmt = sqlStatements.get(i); 671 if (stmt instanceof TSelectSqlStatement) { 672 List<gudusoft.gsqlparser.resolver2.inference.InferenceEvidence> stmtEvidence = 673 evidenceCollector.collectFromSelect((TSelectSqlStatement) stmt); 674 evidenceCount += stmtEvidence.size(); 675 } 676 } 677 678 logInfo("Legacy evidence collection: " + evidenceCount + " items"); 679 } 680 681 /** 682 * Get the namespace enhancer for external access to enhancement history. 683 * 684 * @return the namespace enhancer 685 */ 686 public NamespaceEnhancer getNamespaceEnhancer() { 687 return namespaceEnhancer; 688 } 689 690 /** 691 * Get a detailed enhancement report. 692 * 693 * @return detailed report string 694 */ 695 public String getEnhancementReport() { 696 return namespaceEnhancer.generateReport(); 697 } 698 699 /** 700 * Re-process a statement for name resolution only (without rebuilding scopes). 701 * This is used in Pass 2+ to re-resolve names using enhanced scopes. 702 * 703 * CRITICAL (Principle 1: Scope完全复用): 704 * - Scope tree is built ONCE in Pass 1 and completely reused in Pass 2+ 705 * - This method MUST NOT call processStatement() which rebuilds scopes 706 * - Instead, it iterates through allColumnReferences and re-resolves each 707 * column using its original scope from columnToScopeMap 708 * 709 * This allows: 710 * - Namespaces to be enhanced across iterations (Principle 2) 711 * - Star columns to benefit from reverse inference (Principle 3) 712 * - All previous inference results to be preserved 713 */ 714 private void reprocessStatementNamesOnly(Object statement) { 715 logDebug("Re-resolving column references without rebuilding scopes"); 716 717 // Re-resolve all column references using their original scopes 718 // The scopes are reused from Pass 1, but their namespaces may have been enhanced 719 for (TObjectName objName : allColumnReferences) { 720 IScope scope = columnToScopeMap.get(objName); 721 if (scope != null) { 722 // Re-resolve this column using the (potentially enhanced) scope 723 nameResolver.resolve(objName, scope); 724 725 // Handle USING column priority for JOIN...USING syntax 726 handleUsingColumnResolution(objName); 727 728 // Handle Teradata NAMED alias resolution 729 handleTeradataNamedAliasResolution(objName); 730 handleQualifyClauseAliasResolution(objName); 731 732 // Collect for next enhancement pass if still unresolved 733 collectForEnhancementIfNeeded(objName, scope); 734 } else { 735 logError("No scope found for column: " + objName); 736 } 737 } 738 } 739 740 /** 741 * Handle special resolution for USING columns in JOIN...USING syntax. 742 * In "a JOIN table2 USING (id)", the USING column exists in BOTH tables. 743 * - The synthetic column (clone) resolves to the right-side table (table2) 744 * - The original USING column resolves to the left-side table (a) 745 * 746 * @param objName The column reference 747 */ 748 private void handleUsingColumnResolution(TObjectName objName) { 749 if (objName == null || scopeBuildResult == null) return; 750 751 // Check if this is a synthetic USING column (should resolve to right table) 752 TTable rightTable = scopeBuildResult.getUsingColumnRightTable(objName); 753 if (rightTable != null) { 754 // This is the synthetic USING column - set its sourceTable to the right-side table 755 objName.setSourceTable(rightTable); 756 757 // Create a proper resolution with the right-side table 758 gudusoft.gsqlparser.resolver2.model.ColumnSource source = 759 new gudusoft.gsqlparser.resolver2.model.ColumnSource( 760 null, // no namespace for USING columns 761 objName.getColumnNameOnly(), 762 null, // no definition node 763 1.0, // high confidence 764 "using_column_right", 765 rightTable // override table - the right-side table of the JOIN 766 ); 767 gudusoft.gsqlparser.resolver2.model.ResolutionResult result = 768 gudusoft.gsqlparser.resolver2.model.ResolutionResult.exactMatch(source); 769 770 // Update the TObjectName's resolution so formatter uses correct finalTable 771 objName.setResolution(result); 772 773 // Also register in ResolutionContext so getReferencesTo(table) can find it 774 resolutionContext.registerResolution(objName, result); 775 776 logDebug("USING column " + objName.getColumnNameOnly() + 777 " -> right-side table " + rightTable.getName()); 778 return; 779 } 780 781 // Check if this is the original USING column (should resolve to left table) 782 TTable leftTable = scopeBuildResult.getUsingColumnLeftTable(objName); 783 if (leftTable != null) { 784 // This is the original USING column - set its sourceTable to the left-side table 785 objName.setSourceTable(leftTable); 786 787 // Create a proper resolution with the left-side table 788 gudusoft.gsqlparser.resolver2.model.ColumnSource source = 789 new gudusoft.gsqlparser.resolver2.model.ColumnSource( 790 null, // no namespace for USING columns 791 objName.getColumnNameOnly(), 792 null, // no definition node 793 1.0, // high confidence 794 "using_column_left", 795 leftTable // override table - the left-side table of the JOIN 796 ); 797 gudusoft.gsqlparser.resolver2.model.ResolutionResult result = 798 gudusoft.gsqlparser.resolver2.model.ResolutionResult.exactMatch(source); 799 800 // Update the TObjectName's resolution so formatter uses correct finalTable 801 objName.setResolution(result); 802 803 // Also register in ResolutionContext so getReferencesTo(table) can find it 804 resolutionContext.registerResolution(objName, result); 805 806 logDebug("USING column " + objName.getColumnNameOnly() + 807 " -> left-side table " + leftTable.getName()); 808 } 809 } 810 811 /** 812 * Handle Teradata NAMED alias resolution. 813 * 814 * <p>In Teradata, NAMED aliases defined in the SELECT list (using the {@code (NAMED alias)} syntax) 815 * can be referenced in the WHERE and QUALIFY clauses of the same SELECT statement. This is different 816 * from standard SQL where column aliases are only visible in ORDER BY.</p> 817 * 818 * <p>This method checks if a resolved column matches a NAMED alias from the enclosing SELECT list. 819 * If it does, the resolution is updated to indicate this is a calculated column (alias), not a 820 * physical column from the table.</p> 821 * 822 * <p>Example:</p> 823 * <pre> 824 * SELECT USI_ID, SUBS_ID, 825 * (CAST(:param AS TIMESTAMP(0)))(NAMED REPORT_DTTM) 826 * FROM PRD2_ODW.SUBS_USI_HISTORY 827 * WHERE stime <= REPORT_DTTM AND etime > REPORT_DTTM 828 * </pre> 829 * <p>Here, REPORT_DTTM references in WHERE should NOT be linked to PRD2_ODW.SUBS_USI_HISTORY 830 * because REPORT_DTTM is a NAMED alias, not a physical column.</p> 831 * 832 * @param objName The column reference to check 833 */ 834 private void handleTeradataNamedAliasResolution(TObjectName objName) { 835 if (objName == null || sqlStatements == null || sqlStatements.size() == 0) return; 836 837 // Only applies to Teradata 838 EDbVendor dbVendor = sqlStatements.get(0).dbvendor; 839 if (dbVendor != EDbVendor.dbvteradata) return; 840 841 String columnName = objName.getColumnNameOnly(); 842 if (columnName == null || columnName.isEmpty()) return; 843 844 // Only apply to UNQUALIFIED column references (no table prefix) 845 // If a column has a table qualifier like "CP.CALC_PLATFORM_ID", it's clearly 846 // referencing a specific table's column, not a NAMED alias 847 if (objName.getTableToken() != null) return; 848 849 // Get the scope for this column reference 850 IScope scope = columnToScopeMap.get(objName); 851 if (scope == null) return; 852 853 // Find the enclosing SELECT statement from the scope 854 TSelectSqlStatement enclosingSelect = findEnclosingSelectFromScope(scope); 855 if (enclosingSelect == null) return; 856 857 // Optimization C: Use cached index for O(1) lookup instead of O(N) iteration 858 Map<String, TResultColumn> aliasIndex = getTeradataNamedAliasIndex(enclosingSelect); 859 if (aliasIndex == null || aliasIndex.isEmpty()) return; 860 861 // Look up the result column by alias name (case-insensitive, stored as lowercase) 862 TResultColumn resultCol = aliasIndex.get(columnName.toLowerCase()); 863 if (resultCol == null) return; 864 865 // Skip if objName is part of this result column's expression 866 // This handles cases like "CAST(ID AS DECIMAL) AS ID" where the ID inside 867 // CAST is the source column, not a reference to the ID alias 868 if (isColumnWithinResultColumn(objName, resultCol)) { 869 return; 870 } 871 872 // Found a matching NAMED alias 873 // Clear the source table since this is an alias, not a physical column 874 objName.setSourceTable(null); 875 876 // Create a new ColumnSource with the TResultColumn as the definition node 877 // This will make isCalculatedColumn() return true 878 ColumnSource source = new ColumnSource( 879 null, // namespace - not from a table 880 columnName, 881 resultCol, // definition node - the TResultColumn with the alias 882 1.0, // high confidence 883 "teradata_named_alias" 884 ); 885 ResolutionResult result = ResolutionResult.exactMatch(source); 886 objName.setResolution(result); 887 resolutionContext.registerResolution(objName, result); 888 889 logDebug("Teradata NAMED alias: " + columnName + " -> alias from SELECT list"); 890 } 891 892 /** 893 * Handle QUALIFY clause alias resolution for Snowflake, BigQuery, and Databricks. 894 * 895 * <p>In Snowflake, BigQuery, and Databricks, column aliases defined in the SELECT list 896 * can be referenced in the QUALIFY clause. This is different from standard SQL where 897 * column aliases are only visible in ORDER BY.</p> 898 * 899 * <p>This method checks if a column reference in the QUALIFY clause matches an alias 900 * from the enclosing SELECT list. If it does, the resolution is updated to indicate 901 * this is a calculated column (alias), not a physical column from the table.</p> 902 * 903 * <p>Example:</p> 904 * <pre> 905 * SELECT RoomNumber, RoomType, BlockFloor, 906 * ROW_NUMBER() OVER (PARTITION BY RoomType ORDER BY BlockFloor) AS row_num 907 * FROM Hospital.Room 908 * QUALIFY row_num = 1 909 * </pre> 910 * <p>Here, row_num in QUALIFY should NOT be linked to Hospital.Room because 911 * row_num is an alias for the window function, not a physical column.</p> 912 * 913 * @param objName The column reference to check 914 */ 915 private void handleQualifyClauseAliasResolution(TObjectName objName) { 916 if (objName == null || sqlStatements == null || sqlStatements.size() == 0) return; 917 918 // Only applies to databases that support QUALIFY with alias visibility 919 EDbVendor dbVendor = sqlStatements.get(0).dbvendor; 920 if (dbVendor != EDbVendor.dbvsnowflake && 921 dbVendor != EDbVendor.dbvbigquery && 922 dbVendor != EDbVendor.dbvdatabricks) return; 923 924 String columnName = objName.getColumnNameOnly(); 925 if (columnName == null || columnName.isEmpty()) return; 926 927 // Only apply to UNQUALIFIED column references (no table prefix) 928 if (objName.getTableToken() != null) return; 929 930 // Check if this column is within a QUALIFY clause 931 if (!isInQualifyClause(objName)) return; 932 933 // Get the scope for this column reference 934 IScope scope = columnToScopeMap.get(objName); 935 if (scope == null) return; 936 937 // Find the enclosing SELECT statement from the scope 938 TSelectSqlStatement enclosingSelect = findEnclosingSelectFromScope(scope); 939 if (enclosingSelect == null) return; 940 941 // Look for a matching alias in the SELECT list 942 TResultColumnList resultColumns = enclosingSelect.getResultColumnList(); 943 if (resultColumns == null || resultColumns.size() == 0) return; 944 945 TResultColumn matchingResultCol = null; 946 for (int i = 0; i < resultColumns.size(); i++) { 947 TResultColumn resultCol = resultColumns.getResultColumn(i); 948 if (resultCol == null) continue; 949 950 // Check if this result column has an alias matching the column name 951 if (resultCol.getAliasClause() != null && 952 resultCol.getAliasClause().getAliasName() != null) { 953 String aliasName = resultCol.getAliasClause().getAliasName().toString(); 954 if (aliasName != null && aliasName.equalsIgnoreCase(columnName)) { 955 matchingResultCol = resultCol; 956 break; 957 } 958 } 959 } 960 961 if (matchingResultCol == null) return; 962 963 // Found a matching alias - clear the source table since this is an alias, not a physical column 964 objName.setSourceTable(null); 965 966 // Create a new ColumnSource with the TResultColumn as the definition node 967 // This will make isCalculatedColumn() return true 968 ColumnSource source = new ColumnSource( 969 null, // namespace - not from a table 970 columnName, 971 matchingResultCol, // definition node - the TResultColumn with the alias 972 1.0, // high confidence 973 "qualify_clause_alias" 974 ); 975 ResolutionResult result = ResolutionResult.exactMatch(source); 976 objName.setResolution(result); 977 resolutionContext.registerResolution(objName, result); 978 979 logDebug("QUALIFY clause alias: " + columnName + " -> alias from SELECT list"); 980 } 981 982 /** 983 * Check if a column reference is within a QUALIFY clause. 984 * 985 * @param objName The column reference to check 986 * @return true if the column is within a QUALIFY clause 987 */ 988 private boolean isInQualifyClause(TObjectName objName) { 989 if (objName == null) return false; 990 991 // Get the column's scope to find the enclosing SELECT statement 992 IScope scope = columnToScopeMap.get(objName); 993 if (scope == null) return false; 994 995 TSelectSqlStatement enclosingSelect = findEnclosingSelectFromScope(scope); 996 if (enclosingSelect == null) return false; 997 998 // Check if this SELECT has a QUALIFY clause 999 TQualifyClause qualifyClause = enclosingSelect.getQualifyClause(); 1000 if (qualifyClause == null) return false; 1001 1002 // Check if the column's token position is within the QUALIFY clause's range 1003 if (objName.getStartToken() != null && qualifyClause.getStartToken() != null && 1004 qualifyClause.getEndToken() != null) { 1005 long objPos = objName.getStartToken().posinlist; 1006 long qualifyStart = qualifyClause.getStartToken().posinlist; 1007 long qualifyEnd = qualifyClause.getEndToken().posinlist; 1008 1009 return objPos >= qualifyStart && objPos <= qualifyEnd; 1010 } 1011 1012 return false; 1013 } 1014 1015 /** 1016 * Gets or builds the Teradata NAMED alias index for a SELECT statement. 1017 * Optimization C: Caches the alias map for O(1) lookup instead of O(N) iteration. 1018 * 1019 * @param selectStmt The SELECT statement to get/build the index for 1020 * @return Map from lowercase alias name to TResultColumn, or null if no aliases 1021 */ 1022 private Map<String, TResultColumn> getTeradataNamedAliasIndex(TSelectSqlStatement selectStmt) { 1023 if (selectStmt == null) return null; 1024 1025 // Check cache first 1026 Map<String, TResultColumn> index = teradataNamedAliasCache.get(selectStmt); 1027 if (index != null) { 1028 return index; 1029 } 1030 1031 // Build index for this SELECT statement 1032 TResultColumnList resultColumns = selectStmt.getResultColumnList(); 1033 if (resultColumns == null || resultColumns.size() == 0) { 1034 // Cache empty map to avoid rebuilding 1035 index = java.util.Collections.emptyMap(); 1036 teradataNamedAliasCache.put(selectStmt, index); 1037 return index; 1038 } 1039 1040 index = new java.util.HashMap<>(); 1041 for (int i = 0; i < resultColumns.size(); i++) { 1042 TResultColumn resultCol = resultColumns.getResultColumn(i); 1043 if (resultCol == null) continue; 1044 1045 // Check if this result column has a NAMED alias 1046 if (resultCol.getAliasClause() != null && 1047 resultCol.getAliasClause().getAliasName() != null) { 1048 String aliasName = resultCol.getAliasClause().getAliasName().toString(); 1049 if (aliasName != null && !aliasName.isEmpty()) { 1050 // Store with lowercase key for case-insensitive matching 1051 index.put(aliasName.toLowerCase(), resultCol); 1052 } 1053 } 1054 } 1055 1056 // Cache the index (even if empty, to avoid rebuilding) 1057 teradataNamedAliasCache.put(selectStmt, index); 1058 1059 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE && !index.isEmpty()) { 1060 logDebug("Built Teradata NAMED alias index for SELECT with " + index.size() + " aliases"); 1061 } 1062 1063 return index; 1064 } 1065 1066 /** 1067 * Check if a column reference (TObjectName) is within a result column's expression. 1068 * This is used to prevent treating source columns in expressions like "CAST(ID AS DECIMAL) AS ID" 1069 * as references to the alias. 1070 * 1071 * @param objName The column reference to check 1072 * @param resultCol The result column to check against 1073 * @return true if objName is within resultCol's expression tree 1074 */ 1075 private boolean isColumnWithinResultColumn(TObjectName objName, TResultColumn resultCol) { 1076 if (objName == null || resultCol == null) return false; 1077 1078 // Get the expression of the result column 1079 TExpression expr = resultCol.getExpr(); 1080 if (expr == null) return false; 1081 1082 // Check by comparing start/end positions 1083 // If objName's position is within resultCol's expression, it's part of it 1084 long objStart = objName.getStartToken() != null ? objName.getStartToken().posinlist : -1; 1085 long objEnd = objName.getEndToken() != null ? objName.getEndToken().posinlist : -1; 1086 long exprStart = expr.getStartToken() != null ? expr.getStartToken().posinlist : -1; 1087 long exprEnd = expr.getEndToken() != null ? expr.getEndToken().posinlist : -1; 1088 1089 if (objStart >= 0 && exprStart >= 0 && objEnd >= 0 && exprEnd >= 0) { 1090 return objStart >= exprStart && objEnd <= exprEnd; 1091 } 1092 1093 return false; 1094 } 1095 1096 /** 1097 * Handle subquery aliased/calculated column resolution. 1098 * 1099 * <p>When a column reference resolves through a subquery (or CTE containing subqueries), 1100 * and the underlying column is an alias or calculated expression, we should NOT trace 1101 * it to the base table. This method ensures that such columns have their sourceTable 1102 * cleared to prevent incorrect attribution.</p> 1103 * 1104 * <p>This is essential for queries like:</p> 1105 * <pre> 1106 * WITH DataCTE AS ( 1107 * SELECT t.col, COUNT(*) AS cnt FROM table1 t ... 1108 * ) 1109 * SELECT * FROM DataCTE 1110 * </pre> 1111 * <p>The 'cnt' column should NOT be traced to 'table1' because it's a calculated column.</p> 1112 * 1113 * @param objName The column reference to check 1114 */ 1115 private void handleSubqueryAliasedColumnResolution(TObjectName objName) { 1116 if (objName == null) return; 1117 1118 // Check if column has a table qualifier pointing to a subquery/CTE 1119 // If so, we should KEEP the sourceTable link for lineage tracing 1120 // The qualifier explicitly tells us which subquery the column belongs to 1121 String tableQualifier = objName.getTableString(); 1122 if (tableQualifier != null && !tableQualifier.isEmpty()) { 1123 IScope scope = columnToScopeMap.get(objName); 1124 if (scope != null) { 1125 TTable qualifiedTable = findTableByQualifier(scope, tableQualifier); 1126 if (qualifiedTable != null && 1127 (qualifiedTable.getSubquery() != null || qualifiedTable.getCTE() != null)) { 1128 // Column has qualifier pointing to a subquery/CTE 1129 // Keep the sourceTable link for lineage tracing (e.g., a.num_emp -> subquery a) 1130 // Don't clear sourceTable - this link is correct and needed 1131 logDebug("Subquery/CTE qualified column: " + objName.toString() + 1132 " - keeping sourceTable link to " + tableQualifier); 1133 return; 1134 } 1135 } 1136 } 1137 1138 // For unqualified columns (or columns qualified with base tables), 1139 // check if this is a calculated column or alias that should not trace to base tables 1140 ColumnSource source = objName.getColumnSource(); 1141 if (source != null) { 1142 if (source.isCalculatedColumn() || source.isColumnAlias()) { 1143 TTable currentSource = objName.getSourceTable(); 1144 if (currentSource != null) { 1145 // Only clear if sourceTable is a base table (not subquery/CTE) 1146 // For subquery/CTE references, keep the link for lineage tracing 1147 if (currentSource.getSubquery() == null && currentSource.getCTE() == null) { 1148 objName.setSourceTable(null); 1149 logDebug("Calculated/alias column: " + objName.getColumnNameOnly() + 1150 " cleared sourceTable (was " + currentSource.getName() + ") - not linked to base table"); 1151 } 1152 } 1153 } 1154 } 1155 } 1156 1157 /** 1158 * Gets or builds the FromScopeIndex for a scope (Performance Optimization B). 1159 * 1160 * <p>This method implements lazy initialization: the index is built on first access 1161 * and cached for subsequent lookups within the same resolution pass.</p> 1162 * 1163 * @param scope The scope to get the index for (SelectScope, UpdateScope, or FromScope) 1164 * @return The cached or newly built FromScopeIndex, or null if scope has no FROM clause 1165 */ 1166 private FromScopeIndex getFromScopeIndex(IScope scope) { 1167 if (scope == null) { 1168 return null; 1169 } 1170 1171 // Get the actual FromScope to use as cache key 1172 IScope fromScope = null; 1173 if (scope instanceof SelectScope) { 1174 fromScope = ((SelectScope) scope).getFromScope(); 1175 } else if (scope instanceof gudusoft.gsqlparser.resolver2.scope.UpdateScope) { 1176 fromScope = ((gudusoft.gsqlparser.resolver2.scope.UpdateScope) scope).getFromScope(); 1177 } else if (scope instanceof FromScope) { 1178 fromScope = scope; 1179 } 1180 1181 if (fromScope == null) { 1182 return null; 1183 } 1184 1185 // Check cache first (lazy initialization) 1186 FromScopeIndex index = fromScopeIndexCache.get(fromScope); 1187 if (index == null) { 1188 // Build index and cache it 1189 index = new FromScopeIndex(fromScope.getChildren()); 1190 fromScopeIndexCache.put(fromScope, index); 1191 1192 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1193 logDebug("Built FromScopeIndex for scope: " + index); 1194 } 1195 } 1196 1197 return index; 1198 } 1199 1200 /** 1201 * Find a table by its qualifier (alias or name) in the scope. 1202 * Uses FromScopeIndex for O(1) lookup instead of O(N) linear scan. 1203 */ 1204 private TTable findTableByQualifier(IScope scope, String qualifier) { 1205 if (scope == null || qualifier == null) return null; 1206 1207 // Use indexed lookup (Performance Optimization B) 1208 FromScopeIndex index = getFromScopeIndex(scope); 1209 if (index != null) { 1210 return index.findTableByQualifier(qualifier); 1211 } 1212 1213 return null; 1214 } 1215 1216 /** 1217 * Check if a column name is an alias (not a passthrough column) in the subquery. 1218 */ 1219 private boolean isColumnAnAliasInSubquery(TSelectSqlStatement subquery, String columnName) { 1220 if (subquery == null || columnName == null) return false; 1221 1222 TResultColumnList resultCols = subquery.getResultColumnList(); 1223 if (resultCols == null) return false; 1224 1225 for (int i = 0; i < resultCols.size(); i++) { 1226 TResultColumn rc = resultCols.getResultColumn(i); 1227 if (rc == null) continue; 1228 1229 // Check if this result column has an alias matching the column name 1230 if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) { 1231 String alias = rc.getAliasClause().getAliasName().toString(); 1232 if (alias != null && alias.equalsIgnoreCase(columnName)) { 1233 // Found matching alias - check if it's a calculated column 1234 TExpression expr = rc.getExpr(); 1235 if (expr != null) { 1236 // Not a simple column reference = calculated 1237 if (expr.getExpressionType() != EExpressionType.simple_object_name_t) { 1238 return true; 1239 } 1240 } 1241 } 1242 } 1243 1244 // Also check for SQL Server proprietary alias syntax: alias = expr 1245 // In this case, the alias is the column name itself 1246 String colName = getResultColumnName(rc); 1247 if (colName != null && colName.equalsIgnoreCase(columnName)) { 1248 TExpression expr = rc.getExpr(); 1249 if (expr != null && expr.getExpressionType() != EExpressionType.simple_object_name_t) { 1250 return true; 1251 } 1252 } 1253 } 1254 return false; 1255 } 1256 1257 /** 1258 * Get the column name from a result column (handles aliases and SQL Server proprietary syntax). 1259 */ 1260 private String getResultColumnName(TResultColumn rc) { 1261 if (rc == null) return null; 1262 1263 // Check for explicit alias 1264 if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) { 1265 return rc.getAliasClause().getAliasName().toString(); 1266 } 1267 1268 // Check for SQL Server proprietary alias: alias = expr 1269 // In this case, the expression itself contains the alias 1270 TExpression expr = rc.getExpr(); 1271 if (expr != null && expr.getExpressionType() == EExpressionType.assignment_t) { 1272 // The left side is the alias 1273 if (expr.getLeftOperand() != null && expr.getLeftOperand().getObjectOperand() != null) { 1274 return expr.getLeftOperand().getObjectOperand().toString(); 1275 } 1276 } 1277 1278 return null; 1279 } 1280 1281 /** 1282 * Find the enclosing SELECT statement from a scope. 1283 * Traverses up the scope hierarchy to find a SelectScope and gets its node. 1284 * 1285 * @param scope The scope to start from 1286 * @return The enclosing SELECT statement, or null if not found 1287 */ 1288 private TSelectSqlStatement findEnclosingSelectFromScope(IScope scope) { 1289 if (scope == null) return null; 1290 1291 IScope currentScope = scope; 1292 int maxIterations = 100; // Prevent infinite loops 1293 int iterations = 0; 1294 1295 while (currentScope != null && iterations < maxIterations) { 1296 iterations++; 1297 1298 // Check if current scope is a SelectScope 1299 if (currentScope instanceof SelectScope) { 1300 TParseTreeNode node = currentScope.getNode(); 1301 if (node instanceof TSelectSqlStatement) { 1302 return (TSelectSqlStatement) node; 1303 } 1304 } 1305 1306 // Move up to parent scope 1307 currentScope = currentScope.getParent(); 1308 } 1309 return null; 1310 } 1311 1312 /** 1313 * Collect a column reference for namespace enhancement if it targets a star namespace. 1314 * This is called during resolution to gather columns that need to be added to namespaces. 1315 * 1316 * @param objName The column reference 1317 * @param scope The scope where the column should be resolved 1318 */ 1319 private void collectForEnhancementIfNeeded(TObjectName objName, IScope scope) { 1320 if (objName == null || scope == null) return; 1321 1322 String columnName = objName.getColumnNameOnly(); 1323 if (columnName == null || columnName.isEmpty()) return; 1324 1325 // Get the resolution result to check status 1326 gudusoft.gsqlparser.resolver2.model.ResolutionResult result = objName.getResolution(); 1327 1328 // Find candidate namespace from scope's FROM clause 1329 INamespace candidateNamespace = findCandidateNamespace(objName, scope); 1330 1331 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1332 logInfo("[TSQLResolver2] collectForEnhancement: column=" + columnName + 1333 ", candidateNs=" + (candidateNamespace != null ? candidateNamespace.getDisplayName() : "null") + 1334 ", hasStar=" + (candidateNamespace != null ? candidateNamespace.hasStarColumn() : "N/A")); 1335 } 1336 1337 if (candidateNamespace != null) { 1338 // Determine confidence based on context 1339 double confidence = 0.7; // Default for unqualified reference 1340 String evidence = "outer_reference"; 1341 1342 // Higher confidence for qualified references (e.g., "a.column") 1343 if (objName.getTableToken() != null) { 1344 confidence = 0.9; 1345 evidence = "qualified_reference"; 1346 } 1347 1348 // Collect for enhancement 1349 namespaceEnhancer.collectColumnRef( 1350 columnName, 1351 candidateNamespace, 1352 objName, 1353 confidence, 1354 evidence 1355 ); 1356 } 1357 } 1358 1359 /** 1360 * Find the candidate namespace for a column reference. 1361 * Looks at the scope's FROM clause to find namespaces with star columns. 1362 * Uses FromScopeIndex for O(1) lookup instead of O(N) linear scan. 1363 */ 1364 private INamespace findCandidateNamespace(TObjectName objName, IScope scope) { 1365 // Use indexed lookup (Performance Optimization B) 1366 FromScopeIndex index = getFromScopeIndex(scope); 1367 if (index == null) { 1368 return null; 1369 } 1370 1371 String tablePrefix = objName.getTableToken() != null ? 1372 objName.getTableToken().toString() : null; 1373 1374 return index.findCandidateNamespace(tablePrefix); 1375 } 1376 1377 /** 1378 * Delta 1: Collect metadata from DDL statements in the batch. 1379 * 1380 * If no SQLEnv is provided, this method extracts table/column metadata 1381 * from CREATE TABLE and CREATE VIEW statements in the SQL batch and 1382 * creates a TSQLEnv for use during resolution. 1383 * 1384 * This enables standalone resolution of SQL batches that contain both 1385 * DDL and DML without requiring external metadata. 1386 */ 1387 private void collectBatchMetadata() { 1388 if (sqlStatements == null || sqlStatements.size() == 0) { 1389 return; 1390 } 1391 1392 EDbVendor vendor = config != null ? config.getVendor() : EDbVendor.dbvmssql; 1393 BatchMetadataCollector collector = new BatchMetadataCollector(sqlStatements, vendor); 1394 TSQLEnv batchEnv = collector.collect(); 1395 1396 if (batchEnv != null) { 1397 setSqlEnv(batchEnv); 1398 logDebug("Collected batch-local DDL metadata into TSQLEnv"); 1399 } 1400 } 1401 1402 /** 1403 * Delta 4: Track database context from USE/SET statements. 1404 * 1405 * Scans the statement list for USE DATABASE, USE SCHEMA, SET SCHEMA, 1406 * and similar statements, and applies the context to TSQLEnv for 1407 * proper resolution of unqualified table names. 1408 */ 1409 private void trackDatabaseContext() { 1410 if (sqlStatements == null || sqlStatements.size() == 0) { 1411 return; 1412 } 1413 1414 DatabaseContextTracker tracker = new DatabaseContextTracker(); 1415 tracker.processStatements(sqlStatements); 1416 1417 // Apply context to TSQLEnv if any context was found 1418 if (tracker.hasContext()) { 1419 TSQLEnv env = getSqlEnv(); 1420 if (env != null) { 1421 tracker.applyDefaults(env); 1422 logDebug("Applied database context: " + tracker); 1423 } else { 1424 // Create a minimal TSQLEnv if none exists 1425 EDbVendor vendor = config != null ? config.getVendor() : EDbVendor.dbvmssql; 1426 try { 1427 env = new TSQLEnv(vendor) { 1428 @Override 1429 public void initSQLEnv() { 1430 // Minimal initialization 1431 } 1432 }; 1433 tracker.applyDefaults(env); 1434 setSqlEnv(env); 1435 logDebug("Created minimal TSQLEnv with database context: " + tracker); 1436 } catch (Exception e) { 1437 // TSQLEnv creation failed - context will not be applied 1438 logDebug("Failed to create TSQLEnv for database context: " + e.getMessage()); 1439 } 1440 } 1441 } 1442 } 1443 1444 /** 1445 * Build the global scope 1446 */ 1447 private void buildGlobalScope() { 1448 logDebug("Building global scope"); 1449 1450 // Get SQLEnv and vendor for qualified name resolution 1451 TSQLEnv sqlEnv = globalContext != null ? globalContext.getSqlEnv() : null; 1452 EDbVendor vendor = EDbVendor.dbvoracle; // Default 1453 1454 // Try to get vendor from statements 1455 if (sqlStatements != null && sqlStatements.size() > 0) { 1456 vendor = sqlStatements.get(0).dbvendor; 1457 } 1458 1459 // Create global scope with sqlEnv and vendor for proper qualified name resolution 1460 globalScope = new GlobalScope(globalContext, config.getNameMatcher(), sqlEnv, vendor); 1461 1462 logDebug("GlobalScope created with defaults: catalog=" + 1463 globalScope.getDefaultCatalog() + ", schema=" + globalScope.getDefaultSchema()); 1464 } 1465 1466 /** 1467 * Process a single statement 1468 */ 1469 private void processStatement(Object statement) { 1470 if (statement instanceof TSelectSqlStatement) { 1471 processSelectStatement((TSelectSqlStatement) statement); 1472 } 1473 // TODO: Add support for INSERT, UPDATE, DELETE, etc. 1474 } 1475 1476 /** 1477 * Process a SELECT statement 1478 */ 1479 private void processSelectStatement(TSelectSqlStatement select) { 1480 processSelectStatement(select, globalScope); 1481 } 1482 1483 /** 1484 * Process a SELECT statement with a specific parent scope. 1485 * This is used for recursive processing of CTE subqueries. 1486 */ 1487 private void processSelectStatement(TSelectSqlStatement select, IScope givenParentScope) { 1488 logDebug("Processing SELECT statement"); 1489 1490 // Create SELECT scope (will be child of CTE scope if CTEs exist, otherwise child of given parent scope) 1491 IScope parentScope = givenParentScope; 1492 1493 // Process CTEs (WITH clause) if present 1494 CTEScope cteScope = null; 1495 if (select.getCteList() != null && select.getCteList().size() > 0) { 1496 cteScope = processCTEs(select.getCteList(), givenParentScope); 1497 parentScope = cteScope; // CTEs become parent of SELECT 1498 } 1499 1500 SelectScope selectScope = new SelectScope(parentScope, select); 1501 1502 // Process FROM clause 1503 if (select.tables != null && select.tables.size() > 0) { 1504 FromScope fromScope = processFromClause(select, selectScope); 1505 selectScope.setFromScope(fromScope); 1506 } 1507 1508 // Process column references in SELECT list 1509 if (select.getResultColumnList() != null) { 1510 List<TObjectName> selectListColumns = collectObjectNamesFromResultColumns(select.getResultColumnList()); 1511 processColumnReferences(selectListColumns, selectScope); 1512 } 1513 1514 // Process WHERE clause 1515 if (select.getWhereClause() != null && 1516 select.getWhereClause().getCondition() != null) { 1517 List<TObjectName> whereColumns = select.getWhereClause().getCondition().getColumnsInsideExpression(); 1518 processColumnReferences(whereColumns, selectScope); 1519 } 1520 1521 // Process GROUP BY clause 1522 GroupByScope groupByScope = null; 1523 if (select.getGroupByClause() != null) { 1524 groupByScope = processGroupBy(select, selectScope); 1525 } 1526 1527 // Process HAVING clause 1528 if (select.getGroupByClause() != null && 1529 select.getGroupByClause().getHavingClause() != null) { 1530 processHaving(select, selectScope, groupByScope); 1531 } 1532 1533 // Process ORDER BY clause 1534 if (select.getOrderbyClause() != null) { 1535 processOrderBy(select, selectScope); 1536 } 1537 } 1538 1539 /** 1540 * Process FROM clause and build FROM scope 1541 */ 1542 private FromScope processFromClause(TSelectSqlStatement select, IScope parentScope) { 1543 FromScope fromScope = new FromScope(parentScope, select.tables); 1544 1545 // Process each relation (table or join) 1546 ArrayList<TTable> relations = select.getRelations(); 1547 if (relations != null) { 1548 for (TTable table : relations) { 1549 processTableOrJoin(table, fromScope); 1550 } 1551 } 1552 1553 return fromScope; 1554 } 1555 1556 /** 1557 * Recursively process a table or join expression and add to FROM scope 1558 */ 1559 private void processTableOrJoin(TTable table, FromScope fromScope) { 1560 if (table.getTableType() == ETableSource.join) { 1561 // This is a JOIN - recursively process left and right tables 1562 TJoinExpr joinExpr = table.getJoinExpr(); 1563 if (joinExpr != null) { 1564 logDebug("Processing JOIN: " + joinExpr.getJointype()); 1565 1566 // Recursively process left table 1567 TTable leftTable = joinExpr.getLeftTable(); 1568 if (leftTable != null) { 1569 processTableOrJoin(leftTable, fromScope); 1570 } 1571 1572 // Recursively process right table 1573 TTable rightTable = joinExpr.getRightTable(); 1574 if (rightTable != null) { 1575 processTableOrJoin(rightTable, fromScope); 1576 } 1577 1578 // TODO: Create JoinScope to handle nullable semantics 1579 // For now, we just add the base tables to FROM scope 1580 } 1581 } else { 1582 // This is a base table (objectname, subquery, etc.) 1583 INamespace namespace = createNamespaceForTable(table); 1584 1585 // Validate namespace (load metadata) 1586 namespace.validate(); 1587 1588 // Determine alias 1589 String alias = table.getAliasName() != null 1590 ? table.getAliasName() 1591 : table.getName(); 1592 1593 // Add to FROM scope 1594 fromScope.addChild(namespace, alias, false); 1595 1596 logDebug("Added table to FROM scope: " + alias); 1597 } 1598 } 1599 1600 /** 1601 * Process CTEs (WITH clause) and build CTE scope 1602 */ 1603 private CTEScope processCTEs(TCTEList cteList, IScope parentScope) { 1604 CTEScope cteScope = new CTEScope(parentScope, cteList); 1605 logDebug("Processing WITH clause with " + cteList.size() + " CTE(s)"); 1606 1607 // Process each CTE in order (later CTEs can reference earlier ones) 1608 for (int i = 0; i < cteList.size(); i++) { 1609 TCTE cte = cteList.getCTE(i); 1610 1611 // Get CTE name 1612 String cteName = cte.getTableName() != null ? cte.getTableName().toString() : null; 1613 if (cteName == null) { 1614 logDebug("Skipping CTE with null name"); 1615 continue; 1616 } 1617 1618 // Get CTE subquery 1619 TSelectSqlStatement cteSubquery = cte.getSubquery(); 1620 if (cteSubquery == null) { 1621 logDebug("Skipping CTE '" + cteName + "' with null subquery"); 1622 continue; 1623 } 1624 1625 // Create CTENamespace 1626 CTENamespace cteNamespace = new CTENamespace( 1627 cte, 1628 cteName, 1629 cteSubquery, 1630 config.getNameMatcher() 1631 ); 1632 1633 // Validate namespace (load column metadata from subquery) 1634 cteNamespace.validate(); 1635 1636 // Add to CTE scope (makes it visible to later CTEs and main query) 1637 cteScope.addCTE(cteName, cteNamespace); 1638 1639 logDebug("Added CTE to scope: " + cteName + 1640 " (columns=" + cteNamespace.getExplicitColumns().size() + 1641 ", recursive=" + cteNamespace.isRecursive() + ")"); 1642 1643 // Recursively process CTE subquery 1644 // This ensures that: 1645 // 1. Columns within the CTE are properly resolved 1646 // 2. Nested CTEs within this CTE are handled 1647 // 3. Later CTEs can reference this CTE's columns 1648 logDebug("Recursively processing CTE subquery: " + cteName); 1649 processSelectStatement(cteSubquery, cteScope); 1650 } 1651 1652 return cteScope; 1653 } 1654 1655 /** 1656 * Process GROUP BY clause and build GROUP BY scope 1657 */ 1658 private GroupByScope processGroupBy(TSelectSqlStatement select, SelectScope selectScope) { 1659 GroupByScope groupByScope = new GroupByScope(selectScope, select.getGroupByClause()); 1660 logDebug("Processing GROUP BY clause"); 1661 1662 // Set the FROM scope for column resolution 1663 if (selectScope.getFromScope() != null) { 1664 groupByScope.setFromScope(selectScope.getFromScope()); 1665 } 1666 1667 // Process column references in GROUP BY items 1668 if (select.getGroupByClause().getItems() != null) { 1669 for (int i = 0; i < select.getGroupByClause().getItems().size(); i++) { 1670 gudusoft.gsqlparser.nodes.TGroupByItem item = select.getGroupByClause().getItems().getGroupByItem(i); 1671 if (item.getExpr() != null) { 1672 List<TObjectName> groupByColumns = item.getExpr().getColumnsInsideExpression(); 1673 processColumnReferences(groupByColumns, groupByScope); 1674 } 1675 } 1676 } 1677 1678 return groupByScope; 1679 } 1680 1681 /** 1682 * Process HAVING clause and build HAVING scope 1683 */ 1684 private void processHaving(TSelectSqlStatement select, SelectScope selectScope, GroupByScope groupByScope) { 1685 logDebug("Processing HAVING clause"); 1686 1687 HavingScope havingScope = new HavingScope( 1688 selectScope, 1689 select.getGroupByClause().getHavingClause() 1690 ); 1691 1692 // Set GROUP BY scope for grouped column resolution 1693 if (groupByScope != null) { 1694 havingScope.setGroupByScope(groupByScope); 1695 } 1696 1697 // Set SELECT scope for alias resolution 1698 havingScope.setSelectScope(selectScope); 1699 1700 // Process column references in HAVING condition 1701 List<TObjectName> havingColumns = select.getGroupByClause().getHavingClause().getColumnsInsideExpression(); 1702 processColumnReferences(havingColumns, havingScope); 1703 } 1704 1705 /** 1706 * Process ORDER BY clause and build ORDER BY scope 1707 */ 1708 private void processOrderBy(TSelectSqlStatement select, SelectScope selectScope) { 1709 logDebug("Processing ORDER BY clause"); 1710 1711 OrderByScope orderByScope = new OrderByScope(selectScope, select.getOrderbyClause()); 1712 1713 // Set SELECT scope for alias resolution 1714 orderByScope.setSelectScope(selectScope); 1715 1716 // Set FROM scope for direct column resolution (database-dependent) 1717 if (selectScope.getFromScope() != null) { 1718 orderByScope.setFromScope(selectScope.getFromScope()); 1719 } 1720 1721 // Process column references in ORDER BY items 1722 if (select.getOrderbyClause().getItems() != null) { 1723 for (int i = 0; i < select.getOrderbyClause().getItems().size(); i++) { 1724 gudusoft.gsqlparser.nodes.TOrderByItem item = select.getOrderbyClause().getItems().getOrderByItem(i); 1725 if (item.getSortKey() != null) { 1726 List<TObjectName> orderByColumns = item.getSortKey().getColumnsInsideExpression(); 1727 processColumnReferences(orderByColumns, orderByScope); 1728 } 1729 } 1730 } 1731 } 1732 1733 /** 1734 * Create appropriate namespace for a table 1735 */ 1736 private INamespace createNamespaceForTable(TTable table) { 1737 // Check if it's a subquery 1738 if (table.getSubquery() != null) { 1739 return new SubqueryNamespace( 1740 table.getSubquery(), 1741 table.getAliasName(), 1742 config.getNameMatcher() 1743 ); 1744 } 1745 1746 // Regular table - pass sqlEnv and vendor for qualified name resolution 1747 TSQLEnv sqlEnv = globalContext != null ? globalContext.getSqlEnv() : null; 1748 EDbVendor vendor = table.dbvendor != null ? table.dbvendor : EDbVendor.dbvoracle; 1749 return new TableNamespace(table, config.getNameMatcher(), sqlEnv, vendor); 1750 } 1751 1752 /** 1753 * Collect all TObjectName from TResultColumnList 1754 */ 1755 private List<TObjectName> collectObjectNamesFromResultColumns( 1756 gudusoft.gsqlparser.nodes.TResultColumnList resultColumns) { 1757 List<TObjectName> objNames = new ArrayList<>(); 1758 1759 for (int i = 0; i < resultColumns.size(); i++) { 1760 gudusoft.gsqlparser.nodes.TResultColumn rc = resultColumns.getResultColumn(i); 1761 if (rc.getExpr() != null) { 1762 // Get all column references from the expression 1763 List<TObjectName> exprColumns = rc.getExpr().getColumnsInsideExpression(); 1764 if (exprColumns != null) { 1765 objNames.addAll(exprColumns); 1766 } 1767 } 1768 } 1769 1770 return objNames; 1771 } 1772 1773 /** 1774 * Process column references (TObjectName list) 1775 */ 1776 private void processColumnReferences(List<TObjectName> objectNames, IScope scope) { 1777 if (objectNames == null) return; 1778 1779 for (TObjectName objName : objectNames) { 1780 // Record column-to-scope mapping for iterative resolution (Principle 1) 1781 columnToScopeMap.put(objName, scope); 1782 allColumnReferences.add(objName); 1783 1784 // Resolve the column reference 1785 nameResolver.resolve(objName, scope); 1786 1787 // Handle USING column priority for JOIN...USING syntax 1788 handleUsingColumnResolution(objName); 1789 1790 // Handle Teradata NAMED alias resolution 1791 handleTeradataNamedAliasResolution(objName); 1792 handleQualifyClauseAliasResolution(objName); 1793 } 1794 } 1795 1796 // Detailed legacy sync timing (for profiling) 1797 private static long globalTimeClearLinked = 0; 1798 private static long globalTimeFillAttributes = 0; 1799 private static long globalTimeSyncColumns = 0; 1800 private static long globalTimePopulateOrphans = 0; 1801 private static long globalTimeClearHints = 0; 1802 1803 /** 1804 * Get detailed legacy sync timing breakdown. 1805 */ 1806 public static String getLegacySyncTimings() { 1807 long total = globalTimeClearLinked + globalTimeFillAttributes + globalTimeSyncColumns + globalTimePopulateOrphans + globalTimeClearHints; 1808 return String.format( 1809 "LegacySync Breakdown:\n" + 1810 " ClearLinkedColumns: %d ms (%.1f%%)\n" + 1811 " FillTableAttributes: %d ms (%.1f%%)\n" + 1812 " SyncColumnToLegacy: %d ms (%.1f%%)\n" + 1813 " PopulateOrphanColumns: %d ms (%.1f%%)\n" + 1814 " ClearSyntaxHints: %d ms (%.1f%%)\n" + 1815 " Total: %d ms", 1816 globalTimeClearLinked, total > 0 ? 100.0 * globalTimeClearLinked / total : 0, 1817 globalTimeFillAttributes, total > 0 ? 100.0 * globalTimeFillAttributes / total : 0, 1818 globalTimeSyncColumns, total > 0 ? 100.0 * globalTimeSyncColumns / total : 0, 1819 globalTimePopulateOrphans, total > 0 ? 100.0 * globalTimePopulateOrphans / total : 0, 1820 globalTimeClearHints, total > 0 ? 100.0 * globalTimeClearHints / total : 0, 1821 total); 1822 } 1823 1824 /** 1825 * Create cloned columns for star column tracing. 1826 * 1827 * <p>This is a CORE part of TSQLResolver2's name resolution. When a column traces 1828 * through a CTE or subquery with SELECT * to a physical table, we create a cloned 1829 * TObjectName with sourceTable pointing to the traced physical table. 1830 * 1831 * <p>Example: 1832 * <pre> 1833 * WITH cte AS (SELECT * FROM physical_table) 1834 * SELECT a FROM cte 1835 * </pre> 1836 * 1837 * <p>For column 'a' in the outer SELECT: 1838 * <ul> 1839 * <li>Original column: sourceTable = cte (immediate source)</li> 1840 * <li>Cloned column: sourceTable = physical_table (traced through star)</li> 1841 * </ul> 1842 * 1843 * <p>Both columns are added to allColumnReferences for complete lineage tracking. 1844 * This ensures the formatter can output both the immediate source and the traced 1845 * physical table when needed. 1846 */ 1847 private void createTracedColumnClones() { 1848 // Collect clones to add (avoid ConcurrentModificationException) 1849 java.util.List<TObjectName> clonesToAdd = new java.util.ArrayList<>(); 1850 1851 for (TObjectName column : allColumnReferences) { 1852 // Skip star columns - they represent all columns from a table and shouldn't be cloned 1853 String colName = column.getColumnNameOnly(); 1854 if (colName != null && colName.equals("*")) { 1855 continue; 1856 } 1857 1858 // Skip columns without resolution 1859 gudusoft.gsqlparser.resolver2.model.ResolutionResult resolution = column.getResolution(); 1860 if (resolution == null || !resolution.isExactMatch()) { 1861 continue; 1862 } 1863 1864 gudusoft.gsqlparser.resolver2.model.ColumnSource source = resolution.getColumnSource(); 1865 if (source == null) { 1866 continue; 1867 } 1868 1869 TTable sourceTable = column.getSourceTable(); 1870 if (sourceTable == null) { 1871 continue; 1872 } 1873 1874 // Only process CTE or subquery columns 1875 if (!sourceTable.isCTEName() && sourceTable.getTableType() != ETableSource.subquery) { 1876 continue; 1877 } 1878 1879 // Get the traced physical table 1880 TTable finalTable = source.getFinalTable(); 1881 if (finalTable == null || finalTable == sourceTable) { 1882 continue; 1883 } 1884 1885 // Skip if finalTable is also a CTE or subquery 1886 if (finalTable.isCTEName() || finalTable.getTableType() == ETableSource.subquery) { 1887 continue; 1888 } 1889 1890 // Skip subquery columns when the column matches an explicit column in the subquery's 1891 // SELECT list. Cloning is only needed when tracing through star columns. 1892 // For example, in "SELECT al1.COL1, al1.COL3 FROM (SELECT t1.COL1, t2.* FROM t1, t2) al1": 1893 // - al1.COL1 matches explicit "t1.COL1" -> don't clone (stays at subquery level) 1894 // - al1.COL3 doesn't match explicit column, must come from t2.* -> clone to t2 1895 if (sourceTable.getTableType() == ETableSource.subquery) { 1896 TSelectSqlStatement subquery = sourceTable.getSubquery(); 1897 if (subquery != null && subqueryHasExplicitColumn(subquery, colName)) { 1898 continue; 1899 } 1900 } 1901 1902 // Skip UNION scenarios - syncToLegacyStructures already handles linking to all 1903 // UNION branch tables via getAllFinalTables(). Creating clones would cause duplicates. 1904 java.util.List<TTable> allFinalTables = source.getAllFinalTables(); 1905 if (allFinalTables != null && allFinalTables.size() > 1) { 1906 continue; 1907 } 1908 1909 // Skip UNQUALIFIED join condition columns - they should not be traced to the source 1910 // subquery's underlying table via star column expansion. 1911 // This is particularly important for MERGE ON clause columns which may 1912 // belong to the target table rather than the source subquery. 1913 // QUALIFIED columns (like S.id) should still be traced as they explicitly reference 1914 // the source subquery. 1915 // Note: We check location only because ownStmt may be null for unresolved columns. 1916 if (column.getLocation() == ESqlClause.joinCondition 1917 && (column.getTableString() == null || column.getTableString().isEmpty())) { 1918 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1919 logInfo("createTracedColumnClones: Skipping unqualified join condition column " + column.toString() + 1920 " - should not be traced to subquery's underlying table"); 1921 } 1922 continue; 1923 } 1924 1925 // Check if a column with same name and same finalTable already exists 1926 // (colName was already extracted above when checking for star columns) 1927 boolean alreadyExists = false; 1928 for (TObjectName existing : allColumnReferences) { 1929 if (existing.getSourceTable() == finalTable && 1930 colName != null && colName.equalsIgnoreCase(existing.getColumnNameOnly())) { 1931 alreadyExists = true; 1932 break; 1933 } 1934 } 1935 // Also check in clonesToAdd 1936 if (!alreadyExists) { 1937 for (TObjectName clone : clonesToAdd) { 1938 if (clone.getSourceTable() == finalTable && 1939 colName != null && colName.equalsIgnoreCase(clone.getColumnNameOnly())) { 1940 alreadyExists = true; 1941 break; 1942 } 1943 } 1944 } 1945 1946 if (!alreadyExists) { 1947 // Clone the column and set sourceTable to the traced physical table 1948 TObjectName clonedColumn = column.clone(); 1949 clonedColumn.setSourceTable(finalTable); 1950 clonesToAdd.add(clonedColumn); 1951 1952 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 1953 logInfo("createTracedColumnClones: Cloned column " + column.toString() + 1954 " with sourceTable traced from " + sourceTable.getTableName() + 1955 " to physical table " + finalTable.getTableName()); 1956 } 1957 } 1958 } 1959 1960 // Add all clones to allColumnReferences (local copy in TSQLResolver2) 1961 allColumnReferences.addAll(clonesToAdd); 1962 1963 // Also add to scopeBuildResult so consumers using scopeBuildResult.getAllColumnReferences() 1964 // (like TestGetTableColumn2 for star column expansion tests) can see the clones 1965 if (scopeBuildResult != null && !clonesToAdd.isEmpty()) { 1966 scopeBuildResult.addColumnReferences(clonesToAdd); 1967 } 1968 1969 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE && !clonesToAdd.isEmpty()) { 1970 logInfo("createTracedColumnClones: Created " + clonesToAdd.size() + " traced column clones"); 1971 } 1972 } 1973 1974 /** 1975 * Sync results to legacy structures for backward compatibility. 1976 * This populates: 1977 * - TTable.linkedColumns: columns resolved to this table 1978 * - TObjectName.sourceTable: already set in setResolution() 1979 * - TObjectName.linkedColumnDef: from ColumnSource.definitionNode 1980 * - TObjectName.sourceColumn: from ColumnSource.definitionNode (if TResultColumn) 1981 */ 1982 private void syncToLegacyStructures() { 1983 if (!config.isLegacyCompatibilityEnabled()) { 1984 logInfo("Legacy compatibility disabled, skipping sync"); 1985 return; 1986 } 1987 1988 for (int i = 0; i < sqlStatements.size(); i++) { 1989 } 1990 1991 logInfo("Syncing to legacy structures..."); 1992 1993 long phaseStart; 1994 1995 // Clear existing linkedColumns on all tables 1996 phaseStart = System.currentTimeMillis(); 1997 clearAllLinkedColumns(); 1998 1999 // Clear existing orphanColumns on all statements 2000 // These will be repopulated in Phase 4b based on TSQLResolver2 resolution 2001 for (int i = 0; i < sqlStatements.size(); i++) { 2002 clearOrphanColumnsRecursive(sqlStatements.get(i)); 2003 } 2004 globalTimeClearLinked += System.currentTimeMillis() - phaseStart; 2005 2006 // Phase 1: Fill TTable.getAttributes() for all tables 2007 // This uses the namespace data already collected during resolution 2008 phaseStart = System.currentTimeMillis(); 2009 Set<TTable> processedTables = new HashSet<>(); 2010 for (int i = 0; i < sqlStatements.size(); i++) { 2011 fillTableAttributesRecursive(sqlStatements.get(i), processedTables); 2012 } 2013 globalTimeFillAttributes += System.currentTimeMillis() - phaseStart; 2014 logInfo("Filled attributes for " + processedTables.size() + " tables"); 2015 2016 // Phase 2: Iterate through all column references and sync to legacy structures 2017 phaseStart = System.currentTimeMillis(); 2018 int syncCount = 0; 2019 for (TObjectName column : allColumnReferences) { 2020 if (syncColumnToLegacy(column)) { 2021 syncCount++; 2022 } 2023 } 2024 globalTimeSyncColumns += System.currentTimeMillis() - phaseStart; 2025 2026 // Phase 3: Link CTAS target table columns 2027 // For CREATE TABLE AS SELECT, the SELECT list columns should be linked to the target table 2028 for (int i = 0; i < sqlStatements.size(); i++) { 2029 linkCTASTargetTableColumns(sqlStatements.get(i)); 2030 } 2031 2032 // Phase 4: Sync implicit database/schema from USE DATABASE/USE SCHEMA to AST 2033 // This enables TObjectName.getAnsiSchemaName() and getAnsiCatalogName() to work correctly 2034 syncImplicitDbSchemaToAST(); 2035 2036 // Phase 4b: Populate orphan columns 2037 // Columns with sourceTable=null (unresolved or ambiguous) should be added to 2038 // their containing statement's orphanColumns list. This enables TGetTableColumn 2039 // to report them as orphan columns (with linkOrphanColumnToFirstTable option). 2040 phaseStart = System.currentTimeMillis(); 2041 populateOrphanColumns(); 2042 globalTimePopulateOrphans += System.currentTimeMillis() - phaseStart; 2043 2044 // Phase 4c: Expand star columns using push-down inferred columns 2045 // For SELECT * and SELECT table.*, expand to individual columns based on: 2046 // 1. Inferred columns from the namespace (via push-down algorithm) 2047 // 2. This enables star column expansion without TSQLEnv metadata 2048 phaseStart = System.currentTimeMillis(); 2049 expandStarColumnsUsingPushDown(); 2050 long expandTime = System.currentTimeMillis() - phaseStart; 2051 logInfo("Star column expansion took " + expandTime + "ms"); 2052 2053 // Phase 5: Clear orphan column syntax hints for resolved columns 2054 // The old resolver adds "sphint" (syntax hint) warnings for columns that can't be resolved. 2055 // TSQLResolver2 resolves these columns but doesn't clear the syntax hints. 2056 // This phase cleans up those hints to maintain compatibility with tests expecting no hints. 2057 phaseStart = System.currentTimeMillis(); 2058 clearOrphanColumnSyntaxHints(); 2059 globalTimeClearHints += System.currentTimeMillis() - phaseStart; 2060 2061 logInfo("Legacy sync complete: " + syncCount + "/" + allColumnReferences.size() + " columns synced"); 2062 } 2063 2064 /** 2065 * Link SELECT list columns to CTAS target table. 2066 * For CREATE TABLE AS SELECT statements, the output column names (aliases) 2067 * should be linked to the target table. The source column references 2068 * remain linked to their source tables. 2069 * 2070 * NOTE: For CTAS, the parser (TCreateTableSqlStatement.doParseStatement) already 2071 * correctly creates and links alias columns to the target table. The source columns 2072 * that were incorrectly added are filtered out in clearLinkedColumnsRecursive(). 2073 * This method now only handles cases where the parser didn't create alias columns. 2074 */ 2075 private void linkCTASTargetTableColumns(TCustomSqlStatement stmt) { 2076 if (stmt == null) return; 2077 2078 // CTAS columns are already handled by the parser (TCreateTableSqlStatement.doParseStatement) 2079 // and incorrectly added source columns are filtered in clearLinkedColumnsRecursive(). 2080 // No additional processing needed here for CTAS. 2081 2082 // Process nested statements (for other statement types that might need CTAS handling) 2083 for (int i = 0; i < stmt.getStatements().size(); i++) { 2084 linkCTASTargetTableColumns(stmt.getStatements().get(i)); 2085 } 2086 } 2087 2088 /** 2089 * Populate orphanColumns for unresolved columns. 2090 * Columns with sourceTable=null should be added to their containing statement's orphanColumns. 2091 * This enables TGetTableColumn to report these as "missed" columns. 2092 */ 2093 private void populateOrphanColumns() { 2094 int addedCount = 0; 2095 for (TObjectName column : allColumnReferences) { 2096 if (column == null) continue; 2097 2098 // Skip non-column types that should not be in orphan columns 2099 EDbObjectType dbObjectType = column.getDbObjectType(); 2100 if (dbObjectType == EDbObjectType.column_alias // alias clause column definitions (e.g., AS x (numbers, animals)) 2101 || dbObjectType == EDbObjectType.variable // stored procedure variables 2102 || dbObjectType == EDbObjectType.parameter // stored procedure parameters 2103 || dbObjectType == EDbObjectType.cursor // cursors 2104 || dbObjectType == EDbObjectType.constant // constants 2105 || dbObjectType == EDbObjectType.label // labels 2106 ) { 2107 continue; 2108 } 2109 2110 // Check resolution status directly - ambiguous columns should be added to orphanColumns 2111 // Note: column.getColumnSource() returns the first candidate for ambiguous columns, 2112 // which would cause them to be incorrectly skipped. We need to check the resolution status first. 2113 // IMPORTANT: This check must come BEFORE the sourceTable check because Phase 1 (linkColumnToTable) 2114 // might have already set sourceTable during parsing, but TSQLResolver2 correctly marked it as ambiguous. 2115 // NOTE: Skip star columns (*) since they are handled specially via sourceTableList 2116 ResolutionResult resolution = column.getResolution(); 2117 String columnName = column.getColumnNameOnly(); 2118 boolean isStarColumn = columnName != null && columnName.equals("*"); 2119 2120 if (resolution != null && resolution.getStatus() == ResolutionStatus.AMBIGUOUS && !isStarColumn) { 2121 // Ambiguous columns should be added to orphanColumns so they appear as "missed" 2122 // Clear sourceTable if it was set by Phase 1 (linkColumnToTable) so the column 2123 // doesn't also appear as resolved in the output 2124 if (column.getSourceTable() != null) { 2125 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 2126 logInfo("populateOrphanColumns: Clearing sourceTable for AMBIGUOUS column: " + column.toString() 2127 + " at (" + column.getLineNo() + "," + column.getColumnNo() + ")" 2128 + " was linked to " + column.getSourceTable().getTableName() 2129 + " with " + (resolution.getAmbiguousSource() != null ? 2130 resolution.getAmbiguousSource().getCandidateCount() : 0) + " candidates"); 2131 } 2132 column.setSourceTable(null); 2133 } 2134 // Fall through to add to orphanColumns 2135 } else { 2136 // Star columns (*) should NEVER be orphan columns - they represent all columns 2137 // from all tables and are handled specially via sourceTableList and linked 2138 // to tables in syncColumnToLegacy() which runs after this phase. 2139 if (isStarColumn) { 2140 continue; 2141 } 2142 2143 // For non-ambiguous columns, skip if they have a sourceTable 2144 if (column.getSourceTable() != null) { 2145 continue; 2146 } 2147 2148 // Also skip columns that have a ColumnSource with a valid table 2149 ColumnSource source = column.getColumnSource(); 2150 if (source != null) { 2151 TTable finalTable = source.getFinalTable(); 2152 if (finalTable != null) { 2153 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 2154 logInfo("populateOrphanColumns: Skipping column with ColumnSource: " + column.toString() 2155 + " at (" + column.getLineNo() + "," + column.getColumnNo() + ")" 2156 + " -> resolved to " + finalTable.getTableName()); 2157 } 2158 continue; 2159 } 2160 // Also check overrideTable for derived table columns 2161 TTable overrideTable = source.getOverrideTable(); 2162 if (overrideTable != null) { 2163 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 2164 logInfo("populateOrphanColumns: Skipping column with ColumnSource (override): " + column.toString() 2165 + " at (" + column.getLineNo() + "," + column.getColumnNo() + ")" 2166 + " -> resolved to " + overrideTable.getTableName()); 2167 } 2168 continue; 2169 } 2170 } 2171 } 2172 2173 // Debug: log columns being added to orphan 2174 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 2175 ColumnSource debugSource = column.getColumnSource(); 2176 logInfo("populateOrphanColumns: Adding orphan column: " + column.toString() 2177 + " at (" + column.getLineNo() + "," + column.getColumnNo() + ")" 2178 + ", hasColumnSource=" + (debugSource != null) 2179 + (debugSource != null ? ", namespace=" + (debugSource.getSourceNamespace() != null ? 2180 debugSource.getSourceNamespace().getClass().getSimpleName() : "null") : "")); 2181 } 2182 2183 // Find the containing statement for this column 2184 TCustomSqlStatement containingStmt = findContainingStatement(column); 2185 if (containingStmt != null) { 2186 // Set ownStmt so TSQLResolver2ResultFormatter can use getOwnStmt().getFirstPhysicalTable() 2187 // to link orphan columns to the first physical table (matching TGetTableColumn behavior) 2188 column.setOwnStmt(containingStmt); 2189 2190 TObjectNameList orphanColumns = containingStmt.getOrphanColumns(); 2191 if (orphanColumns != null && !containsColumn(orphanColumns, column)) { 2192 orphanColumns.addObjectName(column); 2193 addedCount++; 2194 } 2195 } else { 2196 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 2197 logInfo("Could not find containing statement for orphan column: " + column.toString()); 2198 } 2199 } 2200 } 2201 logInfo("Populated " + addedCount + " orphan columns"); 2202 } 2203 2204 /** 2205 * Find the statement that contains a column reference. 2206 * First tries to use the scope information (more reliable), then falls back to AST traversal. 2207 * For PL/SQL blocks, searches for the innermost DML statement that contains the column. 2208 */ 2209 private TCustomSqlStatement findContainingStatement(TObjectName column) { 2210 // First, try to use the scope information from columnToScopeMap 2211 // The scope's node is typically the containing statement 2212 IScope scope = columnToScopeMap.get(column); 2213 if (scope != null) { 2214 TParseTreeNode scopeNode = scope.getNode(); 2215 if (scopeNode instanceof TCustomSqlStatement) { 2216 TCustomSqlStatement stmt = (TCustomSqlStatement) scopeNode; 2217 // If the scope is a PL/SQL block or procedure, search for DML statements within it 2218 // that actually contain the column (by line number) 2219 if (isPLSQLBlockStatement(stmt)) { 2220 TCustomSqlStatement dmlStmt = findDMLStatementContaining(stmt, column); 2221 if (dmlStmt != null) { 2222 return dmlStmt; 2223 } 2224 } 2225 return stmt; 2226 } 2227 } 2228 2229 // Fallback: traverse up the AST to find the nearest TCustomSqlStatement parent 2230 TParseTreeNode node = column; 2231 while (node != null) { 2232 if (node instanceof TCustomSqlStatement) { 2233 return (TCustomSqlStatement) node; 2234 } 2235 node = node.getParentObjectName(); 2236 } 2237 2238 // Last resort: search all statements for a DML statement containing the column 2239 TCustomSqlStatement result = null; 2240 if (sqlStatements.size() > 0) { 2241 for (int i = 0; i < sqlStatements.size(); i++) { 2242 TCustomSqlStatement stmt = sqlStatements.get(i); 2243 TCustomSqlStatement dmlStmt = findDMLStatementContaining(stmt, column); 2244 if (dmlStmt != null) { 2245 result = dmlStmt; 2246 break; 2247 } 2248 } 2249 if (result == null) { 2250 result = sqlStatements.get(0); 2251 } 2252 } 2253 return result; 2254 } 2255 2256 /** 2257 * Check if a statement is a PL/SQL block type statement. 2258 */ 2259 private boolean isPLSQLBlockStatement(TCustomSqlStatement stmt) { 2260 if (stmt == null) return false; 2261 String className = stmt.getClass().getSimpleName(); 2262 return className.startsWith("TPlsql") || className.startsWith("TPLSql") || 2263 className.contains("Block") || className.contains("Procedure") || 2264 className.contains("Function") || className.contains("Package"); 2265 } 2266 2267 /** 2268 * DML Statement Range for efficient line-based lookup. 2269 * Used by the DML index cache (Performance Optimization A). 2270 */ 2271 private static class DmlRange implements Comparable<DmlRange> { 2272 final long startLine; 2273 final long endLine; 2274 final TCustomSqlStatement stmt; 2275 2276 DmlRange(TCustomSqlStatement stmt) { 2277 this.stmt = stmt; 2278 this.startLine = stmt.getStartToken() != null ? stmt.getStartToken().lineNo : -1; 2279 this.endLine = stmt.getEndToken() != null ? stmt.getEndToken().lineNo : -1; 2280 } 2281 2282 boolean contains(long line) { 2283 return startLine >= 0 && startLine <= line && line <= endLine; 2284 } 2285 2286 // Sort by startLine for binary search 2287 @Override 2288 public int compareTo(DmlRange other) { 2289 return Long.compare(this.startLine, other.startLine); 2290 } 2291 } 2292 2293 /** 2294 * Cache for DML statement ranges per parent statement (Performance Optimization A). 2295 * Built lazily on first access, cleared at start of each resolve() call. 2296 * Uses IdentityHashMap because we need object identity, not equals(). 2297 */ 2298 private final Map<TCustomSqlStatement, List<DmlRange>> dmlIndexCache = new IdentityHashMap<>(); 2299 2300 /** 2301 * Build DML index for a parent statement. 2302 */ 2303 private List<DmlRange> buildDmlIndex(TCustomSqlStatement parent) { 2304 final List<DmlRange> ranges = new ArrayList<>(); 2305 parent.acceptChildren(new TParseTreeVisitor() { 2306 @Override 2307 public void preVisit(TInsertSqlStatement stmt) { 2308 ranges.add(new DmlRange(stmt)); 2309 } 2310 @Override 2311 public void preVisit(TUpdateSqlStatement stmt) { 2312 ranges.add(new DmlRange(stmt)); 2313 } 2314 @Override 2315 public void preVisit(TDeleteSqlStatement stmt) { 2316 ranges.add(new DmlRange(stmt)); 2317 } 2318 @Override 2319 public void preVisit(TSelectSqlStatement stmt) { 2320 ranges.add(new DmlRange(stmt)); 2321 } 2322 }); 2323 // Sort by startLine for efficient lookup 2324 java.util.Collections.sort(ranges); 2325 return ranges; 2326 } 2327 2328 /** 2329 * Get or build the DML index for a parent statement (Performance Optimization A). 2330 */ 2331 private List<DmlRange> getDmlIndex(TCustomSqlStatement parent) { 2332 return dmlIndexCache.computeIfAbsent(parent, this::buildDmlIndex); 2333 } 2334 2335 /** 2336 * Find the innermost DML statement (INSERT/UPDATE/DELETE/SELECT) within a parent statement 2337 * that contains the given column reference (by line number range). 2338 * Uses cached DML index for O(log N) lookup instead of O(N) traversal. 2339 */ 2340 private TCustomSqlStatement findDMLStatementContaining(TCustomSqlStatement parent, TObjectName column) { 2341 if (parent == null || column == null) return null; 2342 2343 long columnLine = column.getLineNo(); 2344 TCustomSqlStatement result = null; 2345 2346 // Use cached DML index (Performance Optimization A) 2347 List<DmlRange> ranges = getDmlIndex(parent); 2348 2349 // Find all DML statements that contain the column by line number 2350 // Need to check all ranges that could contain the column (can't use pure binary search 2351 // because ranges can overlap and we want the innermost one) 2352 for (DmlRange range : ranges) { 2353 // Optimization: if startLine > columnLine, no more ranges can contain it 2354 if (range.startLine > columnLine) { 2355 break; 2356 } 2357 if (range.contains(columnLine)) { 2358 // Found a matching DML statement - prefer the innermost one (later startLine) 2359 if (result == null || 2360 (range.startLine >= result.getStartToken().lineNo)) { 2361 result = range.stmt; 2362 } 2363 } 2364 } 2365 2366 return result; 2367 } 2368 2369 /** 2370 * Sync implicit database/schema from USE DATABASE/USE SCHEMA statements to AST. 2371 * This enables TObjectName.getAnsiSchemaName() and getAnsiCatalogName() to work correctly 2372 * for unqualified object names. 2373 * 2374 * This is similar to what TDatabaseObjectResolver does in the legacy resolver: 2375 * it visits all TObjectName nodes and sets implicitDatabaseName/implicitSchemaName 2376 * based on the current database/schema context. 2377 */ 2378 private void syncImplicitDbSchemaToAST() { 2379 // Get the tracked database context 2380 TSQLEnv env = getSqlEnv(); 2381 if (env == null) { 2382 return; 2383 } 2384 2385 String defaultCatalog = env.getDefaultCatalogName(); 2386 String defaultSchema = env.getDefaultSchemaName(); 2387 2388 // If no defaults are set, nothing to sync 2389 if ((defaultCatalog == null || defaultCatalog.isEmpty()) && 2390 (defaultSchema == null || defaultSchema.isEmpty())) { 2391 return; 2392 } 2393 2394 logDebug("Syncing implicit DB/schema to AST: catalog=" + defaultCatalog + ", schema=" + defaultSchema); 2395 2396 // Visit all statements and set implicit names on TObjectName nodes 2397 for (int i = 0; i < sqlStatements.size(); i++) { 2398 TCustomSqlStatement stmt = sqlStatements.get(i); 2399 if (stmt != null) { 2400 stmt.acceptChildren(new ImplicitDbSchemaVisitor(defaultCatalog, defaultSchema)); 2401 } 2402 } 2403 } 2404 2405 /** 2406 * Visitor to set implicit database/schema on TObjectName nodes. 2407 */ 2408 private static class ImplicitDbSchemaVisitor extends TParseTreeVisitor { 2409 private final String defaultCatalog; 2410 private final String defaultSchema; 2411 2412 public ImplicitDbSchemaVisitor(String defaultCatalog, String defaultSchema) { 2413 this.defaultCatalog = defaultCatalog; 2414 this.defaultSchema = defaultSchema; 2415 } 2416 2417 @Override 2418 public void preVisit(TObjectName node) { 2419 if (node == null) return; 2420 2421 // Skip column objects - they don't need implicit DB/schema 2422 if (node.getDbObjectType() == EDbObjectType.column) return; 2423 2424 // Set default database name if not qualified 2425 if (defaultCatalog != null && !defaultCatalog.isEmpty() && node.getDatabaseToken() == null) { 2426 node.setImplictDatabaseName(defaultCatalog); 2427 } 2428 2429 // Set default schema name if not qualified 2430 if (defaultSchema != null && !defaultSchema.isEmpty() && node.getSchemaToken() == null) { 2431 node.setImplictSchemaName(defaultSchema); 2432 } 2433 } 2434 } 2435 2436 /** 2437 * Selectively clear orphan column syntax hints (sphint) based on TSQLResolver2 resolution. 2438 * 2439 * Phase 1 (linkColumnToTable during parsing) adds sphint hints for columns it can't resolve. 2440 * TSQLResolver2 should: 2441 * 1. KEEP sphint hints for columns that are in allColumnReferences with NOT_FOUND/AMBIGUOUS status 2442 * (these are genuinely orphan/ambiguous columns) 2443 * 2. CLEAR sphint hints for all other columns: 2444 * - Columns successfully resolved (EXACT_MATCH) 2445 * - Columns filtered out by ScopeBuilder (package constants, function keywords, etc.) 2446 * - Columns in contexts TSQLResolver2 doesn't collect (MERGE VALUES, etc.) 2447 */ 2448 private void clearOrphanColumnSyntaxHints() { 2449 // Build a set of positions for columns that should KEEP their sphint hints 2450 // These are columns in allColumnReferences with NOT_FOUND or AMBIGUOUS status 2451 Set<String> orphanPositions = new HashSet<>(); 2452 2453 for (TObjectName col : allColumnReferences) { 2454 if (col == null) continue; 2455 gudusoft.gsqlparser.resolver2.model.ResolutionResult resolution = col.getResolution(); 2456 if (resolution != null) { 2457 ResolutionStatus status = resolution.getStatus(); 2458 // Only keep sphint for genuinely AMBIGUOUS columns 2459 // NOT_FOUND columns might be due to TSQLResolver2 scope issues (e.g., MERGE WHEN clause) 2460 // so we clear their sphint to match old resolver behavior 2461 if (status == ResolutionStatus.AMBIGUOUS) { 2462 TSourceToken startToken = col.getStartToken(); 2463 if (startToken != null) { 2464 String key = startToken.lineNo + ":" + startToken.columnNo; 2465 orphanPositions.add(key); 2466 } 2467 } 2468 } 2469 } 2470 2471 // Clear sphint hints for positions NOT in orphanPositions 2472 for (int i = 0; i < sqlStatements.size(); i++) { 2473 TCustomSqlStatement stmt = sqlStatements.get(i); 2474 if (stmt == null) continue; 2475 clearNonOrphanSphintHintsRecursive(stmt, orphanPositions); 2476 } 2477 } 2478 2479 /** 2480 * Recursively clear sphint hints except for genuinely orphan columns. 2481 */ 2482 private void clearNonOrphanSphintHintsRecursive(TCustomSqlStatement stmt, Set<String> orphanPositions) { 2483 if (stmt == null) return; 2484 2485 // Clear sphint hints that are NOT for genuinely orphan columns 2486 if (stmt.getSyntaxHints() != null && stmt.getSyntaxHints().size() > 0) { 2487 for (int j = stmt.getSyntaxHints().size() - 1; j >= 0; j--) { 2488 TSyntaxError syntaxError = stmt.getSyntaxHints().get(j); 2489 if (syntaxError.errortype == EErrorType.sphint) { 2490 String key = syntaxError.lineNo + ":" + syntaxError.columnNo; 2491 if (!orphanPositions.contains(key)) { 2492 // This sphint is NOT for a genuinely orphan column - clear it 2493 stmt.getSyntaxHints().remove(j); 2494 logDebug("Cleared sphint at line " + syntaxError.lineNo); 2495 } 2496 // Keep sphint hints for genuinely orphan columns (in orphanPositions) 2497 } 2498 } 2499 } 2500 2501 // Note: orphanColumns is populated by populateOrphanColumns() in Phase 4b 2502 // DO NOT clear it here - TGetTableColumn relies on orphanColumns for 2503 // linkOrphanColumnToFirstTable functionality 2504 2505 // Process nested statements 2506 for (int k = 0; k < stmt.getStatements().size(); k++) { 2507 clearNonOrphanSphintHintsRecursive(stmt.getStatements().get(k), orphanPositions); 2508 } 2509 } 2510 2511 2512 2513 /** 2514 * Filter UNNEST table's linkedColumns to keep only legitimate columns. 2515 * Phase 1 (linkColumnToTable) may incorrectly link external variables to UNNEST 2516 * when UNNEST is the only table in scope. This method removes such incorrect links. 2517 * 2518 * Legitimate columns for UNNEST: 2519 * - Implicit column: the alias (e.g., "arry_pair" from "UNNEST(...) AS arry_pair") 2520 * - WITH OFFSET column (e.g., "pos" from "WITH OFFSET AS pos") 2521 * - Derived struct field columns (from UNNEST of STRUCT arrays) 2522 */ 2523 private void filterUnnestLinkedColumns(TTable unnestTable) { 2524 if (unnestTable == null || unnestTable.getTableType() != ETableSource.unnest) { 2525 return; 2526 } 2527 2528 TObjectNameList linkedColumns = unnestTable.getLinkedColumns(); 2529 if (linkedColumns == null || linkedColumns.size() == 0) { 2530 return; 2531 } 2532 2533 // Build set of legitimate column names 2534 java.util.Set<String> legitimateNames = new java.util.HashSet<>(); 2535 2536 // 1. Implicit column (alias name) 2537 String aliasName = unnestTable.getAliasName(); 2538 if (aliasName != null && !aliasName.isEmpty()) { 2539 legitimateNames.add(aliasName.toUpperCase()); 2540 } 2541 2542 // 2. WITH OFFSET column 2543 TUnnestClause unnestClause = unnestTable.getUnnestClause(); 2544 if (unnestClause != null && unnestClause.getWithOffset() != null) { 2545 if (unnestClause.getWithOffsetAlais() != null && 2546 unnestClause.getWithOffsetAlais().getAliasName() != null) { 2547 legitimateNames.add(unnestClause.getWithOffsetAlais().getAliasName().toString().toUpperCase()); 2548 } else { 2549 legitimateNames.add("OFFSET"); 2550 } 2551 } 2552 2553 // 3. Derived struct field columns 2554 if (unnestClause != null && unnestClause.getDerivedColumnList() != null) { 2555 for (int i = 0; i < unnestClause.getDerivedColumnList().size(); i++) { 2556 TObjectName derivedCol = unnestClause.getDerivedColumnList().getObjectName(i); 2557 if (derivedCol != null) { 2558 legitimateNames.add(derivedCol.toString().toUpperCase()); 2559 } 2560 } 2561 } 2562 2563 // 4. Explicit alias columns (Presto/Trino syntax: UNNEST(...) AS t(col1, col2)) 2564 if (unnestTable.getAliasClause() != null && 2565 unnestTable.getAliasClause().getColumns() != null) { 2566 for (int i = 0; i < unnestTable.getAliasClause().getColumns().size(); i++) { 2567 TObjectName colName = unnestTable.getAliasClause().getColumns().getObjectName(i); 2568 if (colName != null) { 2569 legitimateNames.add(colName.toString().toUpperCase()); 2570 } 2571 } 2572 } 2573 2574 // Collect columns to keep 2575 java.util.List<TObjectName> toKeep = new java.util.ArrayList<>(); 2576 for (int i = 0; i < linkedColumns.size(); i++) { 2577 TObjectName col = linkedColumns.getObjectName(i); 2578 if (col != null) { 2579 String colName = col.getColumnNameOnly(); 2580 if (colName != null && legitimateNames.contains(colName.toUpperCase())) { 2581 toKeep.add(col); 2582 } 2583 } 2584 } 2585 2586 // Clear and re-add only legitimate columns 2587 linkedColumns.clear(); 2588 for (TObjectName col : toKeep) { 2589 linkedColumns.addObjectName(col); 2590 } 2591 } 2592 2593 /** 2594 * Clear linkedColumns on all tables in all statements. 2595 */ 2596 private void clearAllLinkedColumns() { 2597 // Use a set to track processed statements and avoid processing duplicates 2598 // This is important when processing subqueries within tables, as the same 2599 // subquery might be reachable from multiple paths 2600 java.util.Set<TCustomSqlStatement> processed = new java.util.HashSet<>(); 2601 for (int i = 0; i < sqlStatements.size(); i++) { 2602 clearLinkedColumnsRecursive(sqlStatements.get(i), processed); 2603 } 2604 } 2605 2606 /** 2607 * Recursively clear orphanColumns on statements. 2608 * These will be repopulated with genuinely unresolved columns in Phase 4b. 2609 */ 2610 private void clearOrphanColumnsRecursive(TCustomSqlStatement stmt) { 2611 if (stmt == null) return; 2612 2613 if (stmt.getOrphanColumns() != null) { 2614 stmt.getOrphanColumns().clear(); 2615 } 2616 2617 // Process nested statements 2618 for (int i = 0; i < stmt.getStatements().size(); i++) { 2619 clearOrphanColumnsRecursive(stmt.getStatements().get(i)); 2620 } 2621 2622 // Also handle stored procedure/function body statements 2623 if (stmt instanceof gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) { 2624 gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement sp = 2625 (gudusoft.gsqlparser.stmt.TStoredProcedureSqlStatement) stmt; 2626 for (int i = 0; i < sp.getBodyStatements().size(); i++) { 2627 clearOrphanColumnsRecursive(sp.getBodyStatements().get(i)); 2628 } 2629 } 2630 } 2631 2632 private void clearLinkedColumnsRecursive(TCustomSqlStatement stmt, java.util.Set<TCustomSqlStatement> processed) { 2633 if (stmt == null) return; 2634 2635 // Skip if already processed to avoid redundant work and potential infinite loops 2636 if (processed.contains(stmt)) { 2637 return; 2638 } 2639 processed.add(stmt); 2640 2641 // Skip DAX statements - they populate their own linkedColumns during parsing 2642 // via TDaxFunction.doParse() which calls psql.linkColumnToTable() directly. 2643 // TSQLResolver2's ScopeBuilder doesn't traverse DAX expressions, so we must 2644 // preserve the linkedColumns that DAX parsing already established. 2645 if (stmt instanceof TDaxStmt) { 2646 return; 2647 } 2648 2649 // Skip ALTER TABLE statements - they populate linkedColumns during parsing 2650 // via TAlterTableOption.doParse() which directly adds columns to the target table's 2651 // linkedColumns. TSQLResolver2's ScopeBuilder doesn't traverse these option nodes, 2652 // so we must preserve the linkedColumns that parsing already established. 2653 if (stmt instanceof TAlterTableStatement) { 2654 return; 2655 } 2656 2657 // For CREATE TABLE statements, we need special handling: 2658 // - Regular CREATE TABLE (with column definitions): Preserve constraint columns 2659 // populated during TConstraint.doParse() 2660 // - CTAS (CREATE TABLE AS SELECT): Filter out source columns incorrectly added 2661 // to target table, but preserve the correctly created alias columns 2662 boolean isCreateTable = (stmt instanceof TCreateTableSqlStatement); 2663 if (isCreateTable) { 2664 TCreateTableSqlStatement ctas = (TCreateTableSqlStatement) stmt; 2665 boolean isCTAS = (ctas.getSubQuery() != null); 2666 // For CTAS, filter out source columns from target table's linkedColumns 2667 // The old resolver incorrectly adds source columns (from the SELECT) to the target table 2668 // Keep only columns whose sourceTable is the target table itself 2669 if (isCTAS && ctas.getTargetTable() != null) { 2670 TTable targetTable = ctas.getTargetTable(); 2671 TObjectNameList linkedColumns = targetTable.getLinkedColumns(); 2672 if (linkedColumns != null && linkedColumns.size() > 0) { 2673 // Collect columns to keep (those belonging to target table) 2674 java.util.List<TObjectName> toKeep = new java.util.ArrayList<>(); 2675 for (int i = 0; i < linkedColumns.size(); i++) { 2676 TObjectName col = linkedColumns.getObjectName(i); 2677 if (col != null && col.getSourceTable() == targetTable) { 2678 toKeep.add(col); 2679 } 2680 } 2681 // Clear and re-add only the columns to keep 2682 linkedColumns.clear(); 2683 for (TObjectName col : toKeep) { 2684 linkedColumns.addObjectName(col); 2685 } 2686 } 2687 } 2688 } 2689 2690 if (!isCreateTable && stmt.tables != null) { 2691 // Check if this statement contains a TD_UNPIVOT table 2692 // TD_UNPIVOT populates linkedColumns on its inner table during TTDUnpivot.doParse() 2693 // If we clear linkedColumns here, we lose those column references 2694 boolean hasTDUnpivot = false; 2695 for (int i = 0; i < stmt.tables.size(); i++) { 2696 TTable table = stmt.tables.getTable(i); 2697 if (table != null && table.getTableType() == ETableSource.td_unpivot) { 2698 hasTDUnpivot = true; 2699 break; 2700 } 2701 } 2702 2703 for (int i = 0; i < stmt.tables.size(); i++) { 2704 TTable table = stmt.tables.getTable(i); 2705 if (table != null && table.getLinkedColumns() != null) { 2706 // For UNNEST tables, filter out incorrectly linked columns from Phase 1. 2707 // Phase 1 (linkColumnToTable) may have linked external variables to UNNEST 2708 // when it's the only table in scope. Keep only legitimate columns: 2709 // - Implicit column (the UNNEST alias, e.g., "arry_pair" from "UNNEST(...) AS arry_pair") 2710 // - WITH OFFSET column (e.g., "pos" from "WITH OFFSET AS pos") 2711 if (table.getTableType() == ETableSource.unnest) { 2712 filterUnnestLinkedColumns(table); 2713 continue; 2714 } 2715 // Skip TD_UNPIVOT tables - they don't have their own columns but 2716 // TTDUnpivot.doParse() populates columns on the inner table 2717 if (table.getTableType() == ETableSource.td_unpivot) { 2718 continue; 2719 } 2720 // If this statement contains TD_UNPIVOT, skip clearing all tables 2721 // because TD_UNPIVOT populates linkedColumns on inner tables 2722 if (hasTDUnpivot) { 2723 continue; 2724 } 2725 table.getLinkedColumns().clear(); 2726 } 2727 } 2728 } 2729 2730 // Skip recursive processing if this statement contains TD_UNPIVOT 2731 // TD_UNPIVOT's inner table (in the ON clause) has columns populated during parsing 2732 // and those columns need to be preserved 2733 boolean hasTDUnpivot = false; 2734 if (stmt.tables != null) { 2735 for (int i = 0; i < stmt.tables.size(); i++) { 2736 TTable table = stmt.tables.getTable(i); 2737 if (table != null && table.getTableType() == ETableSource.td_unpivot) { 2738 hasTDUnpivot = true; 2739 break; 2740 } 2741 } 2742 } 2743 2744 if (!hasTDUnpivot) { 2745 for (int i = 0; i < stmt.getStatements().size(); i++) { 2746 clearLinkedColumnsRecursive(stmt.getStatements().get(i), processed); 2747 } 2748 2749 // Also process subqueries within tables - these are NOT in getStatements() 2750 // but are accessed via table.getSubquery() 2751 if (stmt.tables != null) { 2752 for (int i = 0; i < stmt.tables.size(); i++) { 2753 TTable table = stmt.tables.getTable(i); 2754 if (table != null && table.getSubquery() != null) { 2755 clearLinkedColumnsRecursive(table.getSubquery(), processed); 2756 } 2757 } 2758 } 2759 } 2760 } 2761 2762 /** 2763 * Recursively fill TTable.getAttributes() for all tables in a statement. 2764 * Uses namespace data already collected during name resolution. 2765 * 2766 * Processing order is important: 2767 * 1. Process CTEs first 2768 * 2. Process leaf tables (objectname, function, etc.) - not JOIN or subquery 2769 * 3. Process subqueries (recursively) 2770 * 4. Process JOIN tables last (they depend on child tables having attributes) 2771 */ 2772 private void fillTableAttributesRecursive(TCustomSqlStatement stmt, Set<TTable> processedTables) { 2773 if (stmt == null) return; 2774 2775 // Skip DAX statements - they use their own attribute/linkedColumn mechanism 2776 // established during TDaxFunction.doParse() parsing phase. 2777 if (stmt instanceof TDaxStmt) { 2778 return; 2779 } 2780 2781 // Skip ALTER TABLE statements - they use their own linkedColumn mechanism 2782 // established during TAlterTableOption.doParse() parsing phase. 2783 if (stmt instanceof TAlterTableStatement) { 2784 return; 2785 } 2786 2787 // Skip CREATE TABLE statements - they use their own linkedColumn mechanism 2788 // established during TConstraint.doParse() parsing phase. 2789 if (stmt instanceof TCreateTableSqlStatement) { 2790 return; 2791 } 2792 2793 // Phase 1: Process CTE tables first 2794 if (stmt instanceof TSelectSqlStatement) { 2795 TSelectSqlStatement selectStmt = (TSelectSqlStatement) stmt; 2796 TCTEList cteList = selectStmt.getCteList(); 2797 if (cteList != null) { 2798 for (int i = 0; i < cteList.size(); i++) { 2799 TCTE cte = cteList.getCTE(i); 2800 if (cte != null && cte.getSubquery() != null) { 2801 fillTableAttributesRecursive(cte.getSubquery(), processedTables); 2802 } 2803 } 2804 } 2805 } 2806 2807 // Collect tables by type for proper processing order 2808 List<TTable> leafTables = new ArrayList<>(); 2809 List<TTable> subqueryTables = new ArrayList<>(); 2810 List<TTable> joinTables = new ArrayList<>(); 2811 2812 // First, collect from stmt.tables 2813 if (stmt.tables != null) { 2814 for (int i = 0; i < stmt.tables.size(); i++) { 2815 TTable table = stmt.tables.getTable(i); 2816 if (table == null || processedTables.contains(table)) continue; 2817 2818 switch (table.getTableType()) { 2819 case join: 2820 joinTables.add(table); 2821 // Also collect nested tables within the join 2822 collectNestedJoinTables(table, leafTables, subqueryTables, joinTables, processedTables); 2823 break; 2824 case subquery: 2825 subqueryTables.add(table); 2826 break; 2827 default: 2828 leafTables.add(table); 2829 break; 2830 } 2831 } 2832 } 2833 2834 // Also collect from getRelations() - JOIN tables are often stored there 2835 if (stmt.getRelations() != null) { 2836 for (int i = 0; i < stmt.getRelations().size(); i++) { 2837 IRelation rel = stmt.getRelations().get(i); 2838 if (!(rel instanceof TTable)) continue; 2839 TTable table = (TTable) rel; 2840 if (processedTables.contains(table)) continue; 2841 2842 if (table.getTableType() == ETableSource.join) { 2843 if (!joinTables.contains(table)) { 2844 joinTables.add(table); 2845 // Also collect nested tables within the join 2846 collectNestedJoinTables(table, leafTables, subqueryTables, joinTables, processedTables); 2847 } 2848 } 2849 } 2850 } 2851 2852 // Phase 2: Process leaf tables first (objectname, function, xml, etc.) 2853 for (TTable table : leafTables) { 2854 if (!processedTables.contains(table)) { 2855 fillTableAttributes(table, processedTables, stmt); 2856 processedTables.add(table); 2857 } 2858 } 2859 2860 // Phase 3: Process subqueries (recursively process their contents first) 2861 for (TTable table : subqueryTables) { 2862 if (!processedTables.contains(table)) { 2863 if (table.getSubquery() != null) { 2864 fillTableAttributesRecursive(table.getSubquery(), processedTables); 2865 } 2866 fillTableAttributes(table, processedTables, stmt); 2867 processedTables.add(table); 2868 } 2869 } 2870 2871 // Phase 4: Process JOIN tables last (they need child tables to have attributes) 2872 for (TTable table : joinTables) { 2873 if (!processedTables.contains(table)) { 2874 fillTableAttributes(table, processedTables, stmt); 2875 processedTables.add(table); 2876 } 2877 } 2878 2879 // Process nested statements 2880 for (int i = 0; i < stmt.getStatements().size(); i++) { 2881 fillTableAttributesRecursive(stmt.getStatements().get(i), processedTables); 2882 } 2883 } 2884 2885 /** 2886 * Collect nested tables within a JOIN expression. 2887 * This ensures all component tables are processed before the JOIN itself. 2888 */ 2889 private void collectNestedJoinTables(TTable joinTable, 2890 List<TTable> leafTables, 2891 List<TTable> subqueryTables, 2892 List<TTable> joinTables, 2893 Set<TTable> processedTables) { 2894 if (joinTable == null || joinTable.getJoinExpr() == null) return; 2895 2896 TJoinExpr joinExpr = joinTable.getJoinExpr(); 2897 2898 // Process left table 2899 TTable leftTable = joinExpr.getLeftTable(); 2900 if (leftTable != null && !processedTables.contains(leftTable)) { 2901 switch (leftTable.getTableType()) { 2902 case join: 2903 joinTables.add(leftTable); 2904 collectNestedJoinTables(leftTable, leafTables, subqueryTables, joinTables, processedTables); 2905 break; 2906 case subquery: 2907 subqueryTables.add(leftTable); 2908 break; 2909 default: 2910 leafTables.add(leftTable); 2911 break; 2912 } 2913 } 2914 2915 // Process right table 2916 TTable rightTable = joinExpr.getRightTable(); 2917 if (rightTable != null && !processedTables.contains(rightTable)) { 2918 switch (rightTable.getTableType()) { 2919 case join: 2920 joinTables.add(rightTable); 2921 collectNestedJoinTables(rightTable, leafTables, subqueryTables, joinTables, processedTables); 2922 break; 2923 case subquery: 2924 subqueryTables.add(rightTable); 2925 break; 2926 default: 2927 leafTables.add(rightTable); 2928 break; 2929 } 2930 } 2931 } 2932 2933 /** 2934 * Fill TTable.getAttributes() for a single table using namespace data. 2935 * This converts the namespace's columnSources to TAttributeNode objects. 2936 * 2937 * @param table The table to fill attributes for 2938 * @param processedTables Set of already processed tables to avoid duplicates 2939 * @param stmt The statement context (used for UNNEST to get the SELECT statement) 2940 */ 2941 private void fillTableAttributes(TTable table, Set<TTable> processedTables, TCustomSqlStatement stmt) { 2942 if (table == null) return; 2943 2944 // Clear existing attributes 2945 table.getAttributes().clear(); 2946 2947 String displayName = table.getDisplayName(true); 2948 if (displayName == null || displayName.isEmpty()) { 2949 displayName = table.getAliasName(); 2950 if (displayName == null || displayName.isEmpty()) { 2951 displayName = table.getName(); 2952 } 2953 } 2954 2955 // First, try to use existing namespace from ScopeBuildResult 2956 // Skip namespace lookup for UNNEST tables - they need special handling via initAttributesForUnnest 2957 INamespace existingNamespace = null; 2958 if (table.getTableType() != ETableSource.unnest) { 2959 existingNamespace = scopeBuildResult != null 2960 ? scopeBuildResult.getNamespaceForTable(table) 2961 : null; 2962 } 2963 2964 if (existingNamespace != null) { 2965 // Use existing namespace's column sources 2966 // Returns false if namespace has no real metadata (only inferred columns) 2967 if (fillAttributesFromNamespace(table, existingNamespace, displayName)) { 2968 return; 2969 } 2970 // Fall through to legacy logic if no real metadata 2971 } 2972 2973 // Fall back to type-specific handling if no namespace found 2974 switch (table.getTableType()) { 2975 case objectname: 2976 if (table.isCTEName()) { 2977 // CTE reference - use initAttributesFromCTE 2978 TCTE cte = table.getCTE(); 2979 if (cte != null) { 2980 table.initAttributesFromCTE(cte); 2981 } 2982 } else { 2983 // Physical table - create TableNamespace and extract columns 2984 fillPhysicalTableAttributes(table, displayName); 2985 } 2986 break; 2987 2988 case subquery: 2989 // Subquery - use initAttributesFromSubquery 2990 if (table.getSubquery() != null) { 2991 String prefix = ""; 2992 if (table.getAliasClause() != null) { 2993 prefix = table.getAliasClause().toString() + "."; 2994 } 2995 table.initAttributesFromSubquery(table.getSubquery(), prefix); 2996 } 2997 break; 2998 2999 case join: 3000 // JOIN - combine attributes from left and right tables 3001 // First, add USING columns to the left and right tables (if present) 3002 if (table.getJoinExpr() != null) { 3003 addUsingColumnsToTables(table.getJoinExpr()); 3004 // Then initialize the join expression's attributes (which pulls from left/right tables) 3005 table.getJoinExpr().initAttributes(0); 3006 } 3007 table.initAttributesForJoin(); 3008 break; 3009 3010 case function: 3011 // Table function 3012 table.initAttributeForTableFunction(); 3013 break; 3014 3015 case xmltable: 3016 // XML table 3017 table.initAttributeForXMLTable(); 3018 break; 3019 3020 case tableExpr: 3021 // Table expression 3022 TAttributeNode.addNodeToList( 3023 new TAttributeNode(displayName + ".*", table), 3024 table.getAttributes() 3025 ); 3026 break; 3027 3028 case rowList: 3029 // Row list 3030 table.initAttributeForRowList(); 3031 break; 3032 3033 case unnest: 3034 // UNNEST - initialize attributes using the SELECT statement context 3035 if (stmt instanceof TSelectSqlStatement) { 3036 TSelectSqlStatement select = (TSelectSqlStatement) stmt; 3037 table.initAttributesForUnnest(getSqlEnv(), select); 3038 } 3039 break; 3040 3041 case pivoted_table: 3042 // PIVOT table 3043 table.initAttributesForPivotTable(); 3044 break; 3045 } 3046 } 3047 3048 /** 3049 * Fill table attributes from an existing namespace's column sources. 3050 * This uses the namespace data that was collected during ScopeBuilder traversal. 3051 * 3052 * @return true if attributes were successfully filled, false if should fall back to legacy logic 3053 */ 3054 private boolean fillAttributesFromNamespace(TTable table, INamespace namespace, String displayName) { 3055 // Ensure namespace is validated 3056 if (!namespace.isValidated()) { 3057 namespace.validate(); 3058 } 3059 3060 // For TableNamespace without actual metadata (only inferred columns), 3061 // return false to fall back to legacy logic which uses wildcards 3062 if (namespace instanceof TableNamespace) { 3063 TableNamespace tableNs = (TableNamespace) namespace; 3064 // Check if the namespace has actual metadata by seeing if there are any columns 3065 // with high confidence from metadata sources (not inferred) 3066 Map<String, ColumnSource> columnSources = namespace.getAllColumnSources(); 3067 boolean hasRealMetadata = false; 3068 for (ColumnSource source : columnSources.values()) { 3069 if (source.getConfidence() >= 1.0 && 3070 !("inferred_from_usage".equals(source.getEvidence()))) { 3071 hasRealMetadata = true; 3072 break; 3073 } 3074 } 3075 if (!hasRealMetadata) { 3076 // No real metadata, fall back to legacy logic with wildcards 3077 return false; 3078 } 3079 3080 // Has metadata - use namespace columns 3081 for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) { 3082 String colName = entry.getKey(); 3083 ColumnSource source = entry.getValue(); 3084 // Only include columns with real metadata, not inferred ones 3085 if (source.getConfidence() >= 1.0 && 3086 !("inferred_from_usage".equals(source.getEvidence()))) { 3087 TAttributeNode.addNodeToList( 3088 new TAttributeNode(displayName + "." + colName, table), 3089 table.getAttributes() 3090 ); 3091 } 3092 } 3093 3094 // If no columns after filtering, add wildcard 3095 if (table.getAttributes().isEmpty()) { 3096 TAttributeNode.addNodeToList( 3097 new TAttributeNode(displayName + ".*", table), 3098 table.getAttributes() 3099 ); 3100 } 3101 return true; 3102 } 3103 3104 // For other namespace types (SubqueryNamespace, CTENamespace, etc.), 3105 // use all column sources 3106 Map<String, ColumnSource> columnSources = namespace.getAllColumnSources(); 3107 if (columnSources != null && !columnSources.isEmpty()) { 3108 for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) { 3109 String colName = entry.getKey(); 3110 TAttributeNode.addNodeToList( 3111 new TAttributeNode(displayName + "." + colName, table), 3112 table.getAttributes() 3113 ); 3114 } 3115 } 3116 3117 // If no columns found, add wildcard attribute 3118 if (table.getAttributes().isEmpty()) { 3119 TAttributeNode.addNodeToList( 3120 new TAttributeNode(displayName + ".*", table), 3121 table.getAttributes() 3122 ); 3123 } 3124 return true; 3125 } 3126 3127 /** 3128 * Fill attributes for a physical table using TableNamespace. 3129 */ 3130 private void fillPhysicalTableAttributes(TTable table, String displayName) { 3131 // Create namespace for this table with sqlEnv and vendor for qualified name resolution 3132 TSQLEnv sqlEnv = globalContext != null ? globalContext.getSqlEnv() : null; 3133 EDbVendor vendor = table.dbvendor != null ? table.dbvendor : EDbVendor.dbvoracle; 3134 TableNamespace namespace = new TableNamespace(table, config.getNameMatcher(), sqlEnv, vendor); 3135 3136 // Validate to populate columnSources 3137 namespace.validate(); 3138 3139 // Convert columnSources to TAttributeNode 3140 Map<String, ColumnSource> columnSources = namespace.getAllColumnSources(); 3141 if (columnSources != null && !columnSources.isEmpty()) { 3142 for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) { 3143 String colName = entry.getKey(); 3144 TAttributeNode.addNodeToList( 3145 new TAttributeNode(displayName + "." + colName, table), 3146 table.getAttributes() 3147 ); 3148 } 3149 } 3150 3151 // If no columns found from metadata, add wildcard attribute 3152 // (this allows any column to potentially match) 3153 if (table.getAttributes().isEmpty()) { 3154 // Add columns from linkedColumns if available 3155 if (table.getLinkedColumns() != null && table.getLinkedColumns().size() > 0) { 3156 for (TObjectName col : table.getLinkedColumns()) { 3157 if (col.getCandidateTables() != null && col.getCandidateTables().size() > 1) { 3158 continue; // Skip ambiguous columns 3159 } 3160 TAttributeNode.addNodeToList( 3161 new TAttributeNode(displayName + "." + col.getColumnNameOnly(), table), 3162 table.getAttributes() 3163 ); 3164 } 3165 } 3166 // Add wildcard attribute 3167 TAttributeNode.addNodeToList( 3168 new TAttributeNode(displayName + ".*", table), 3169 table.getAttributes() 3170 ); 3171 } 3172 } 3173 3174 /** 3175 * Add USING columns to the left and right tables in a JOIN expression. 3176 * USING columns should appear in both tables' attribute lists before the wildcard. 3177 * This method recursively handles nested JOINs. 3178 */ 3179 private void addUsingColumnsToTables(TJoinExpr joinExpr) { 3180 if (joinExpr == null) return; 3181 3182 // Recursively handle nested joins 3183 TTable leftTable = joinExpr.getLeftTable(); 3184 TTable rightTable = joinExpr.getRightTable(); 3185 3186 if (leftTable != null && leftTable.getTableType() == ETableSource.join && leftTable.getJoinExpr() != null) { 3187 addUsingColumnsToTables(leftTable.getJoinExpr()); 3188 } 3189 if (rightTable != null && rightTable.getTableType() == ETableSource.join && rightTable.getJoinExpr() != null) { 3190 addUsingColumnsToTables(rightTable.getJoinExpr()); 3191 } 3192 3193 // Handle USING columns in this join 3194 gudusoft.gsqlparser.nodes.TObjectNameList usingColumns = joinExpr.getUsingColumns(); 3195 if (usingColumns == null || usingColumns.size() == 0) return; 3196 3197 // Add USING columns to both tables 3198 for (int i = 0; i < usingColumns.size(); i++) { 3199 TObjectName usingCol = usingColumns.getObjectName(i); 3200 if (usingCol == null) continue; 3201 String colName = usingCol.getColumnNameOnly(); 3202 3203 // Add to left table (insert before wildcard if possible) 3204 if (leftTable != null && leftTable.getTableType() != ETableSource.join) { 3205 addColumnAttributeBeforeWildcard(leftTable, colName); 3206 } 3207 3208 // Add to right table (insert before wildcard if possible) 3209 if (rightTable != null && rightTable.getTableType() != ETableSource.join) { 3210 addColumnAttributeBeforeWildcard(rightTable, colName); 3211 } 3212 } 3213 } 3214 3215 /** 3216 * Add a column attribute to a table, inserting before the wildcard (*) if present. 3217 * This ensures USING columns appear before the wildcard in the attribute list. 3218 */ 3219 private void addColumnAttributeBeforeWildcard(TTable table, String columnName) { 3220 if (table == null || columnName == null) return; 3221 3222 String displayName = table.getDisplayName(true); 3223 if (displayName == null || displayName.isEmpty()) { 3224 displayName = table.getAliasName(); 3225 if (displayName == null || displayName.isEmpty()) { 3226 displayName = table.getName(); 3227 } 3228 } 3229 3230 String attrName = displayName + "." + columnName; 3231 3232 // Check if attribute already exists 3233 ArrayList<TAttributeNode> attrs = table.getAttributes(); 3234 for (TAttributeNode attr : attrs) { 3235 if (attr.getName().equalsIgnoreCase(attrName)) { 3236 return; // Already exists 3237 } 3238 } 3239 3240 // Find the wildcard position 3241 int wildcardIndex = -1; 3242 for (int i = 0; i < attrs.size(); i++) { 3243 if (attrs.get(i).getName().endsWith(".*")) { 3244 wildcardIndex = i; 3245 break; 3246 } 3247 } 3248 3249 // Insert before wildcard or add to end 3250 TAttributeNode newAttr = new TAttributeNode(attrName, table); 3251 if (wildcardIndex >= 0) { 3252 attrs.add(wildcardIndex, newAttr); 3253 } else { 3254 TAttributeNode.addNodeToList(newAttr, attrs); 3255 } 3256 } 3257 3258 /** 3259 * Sync a single column to legacy structures. 3260 * @return true if column was synced (had a sourceTable) 3261 */ 3262 private boolean syncColumnToLegacy(TObjectName column) { 3263 if (column == null) return false; 3264 3265 // Special handling for star columns (SELECT *) 3266 // Star columns represent ALL tables in the FROM clause and should be synced to ALL tables 3267 // in their sourceTableList, not just the first one. 3268 String columnName = column.getColumnNameOnly(); 3269 if (columnName != null && columnName.equals("*")) { 3270 java.util.ArrayList<TTable> sourceTableList = column.getSourceTableList(); 3271 if (sourceTableList != null && sourceTableList.size() > 0) { 3272 boolean synced = false; 3273 for (TTable starTable : sourceTableList) { 3274 if (starTable == null) continue; 3275 // Skip subquery types - the star should be linked to physical tables 3276 if (starTable.getTableType() == ETableSource.subquery) continue; 3277 gudusoft.gsqlparser.nodes.TObjectNameList starLinkedColumns = starTable.getLinkedColumns(); 3278 if (starLinkedColumns != null && !containsColumn(starLinkedColumns, column)) { 3279 starLinkedColumns.addObjectName(column); 3280 synced = true; 3281 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3282 logInfo("syncColumnToLegacy: Synced star column to sourceTableList table: " 3283 + starTable.getTableName()); 3284 } 3285 } 3286 } 3287 return synced; 3288 } 3289 } 3290 3291 // Check if column is AMBIGUOUS - don't sync to legacy if it's ambiguous 3292 // Ambiguous columns should be added to orphanColumns, not linkedColumns 3293 // NOTE: Skip this check for star columns (*) since they are handled specially 3294 // via sourceTableList and should be linked to all tables in the FROM clause 3295 ResolutionResult resolution = column.getResolution(); 3296 if (resolution != null && resolution.getStatus() == ResolutionStatus.AMBIGUOUS) { 3297 // Don't treat star columns as ambiguous - they're supposed to match all tables 3298 if (columnName != null && columnName.equals("*")) { 3299 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3300 logInfo("syncColumnToLegacy: Star column has AMBIGUOUS status, proceeding with normal sync"); 3301 } 3302 // Fall through to normal processing 3303 } else { 3304 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3305 logInfo("syncColumnToLegacy: Skipping AMBIGUOUS column: " + column.toString() 3306 + " with " + (resolution.getAmbiguousSource() != null ? 3307 resolution.getAmbiguousSource().getCandidateCount() : 0) + " candidates"); 3308 } 3309 // Clear sourceTable if it was set by Phase 1 (linkColumnToTable) 3310 // This ensures the column will be treated as orphan by TGetTableColumn 3311 if (column.getSourceTable() != null) { 3312 column.setSourceTable(null); 3313 } 3314 return false; 3315 } 3316 } 3317 3318 TTable sourceTable = column.getSourceTable(); 3319 ColumnSource source = column.getColumnSource(); 3320 3321 // Handle columns resolved through PlsqlVariableNamespace 3322 // These are stored procedure variables/parameters - mark them as variables 3323 // so they won't be added to orphan columns 3324 if (source != null && source.getSourceNamespace() instanceof gudusoft.gsqlparser.resolver2.namespace.PlsqlVariableNamespace) { 3325 column.setDbObjectTypeDirectly(EDbObjectType.variable); 3326 // Variables don't need to be linked to tables 3327 return false; 3328 } 3329 3330 // Fix for subquery columns: When a column is EXPLICITLY QUALIFIED with a subquery alias 3331 // (e.g., mm.material_id), the old resolver Phase 1 may have incorrectly set sourceTable 3332 // to the physical table inside the subquery. TSQLResolver2 should correct this to point 3333 // to the subquery TTable itself. This preserves the intermediate layer for data lineage: 3334 // mm.material_id -> subquery mm -> physical table 3335 // 3336 // IMPORTANT: Only apply this correction for QUALIFIED columns. Unqualified columns 3337 // (like those inferred from star column expansion) should keep their physical table 3338 // sourceTable for proper data lineage tracing. 3339 if (source != null && column.isQualified()) { 3340 INamespace ns = source.getSourceNamespace(); 3341 if (ns instanceof SubqueryNamespace) { 3342 TTable subqueryTable = ns.getSourceTable(); 3343 // If the subquery's TTable is different from the current sourceTable, 3344 // use the subquery's TTable to maintain proper semantic layering 3345 if (subqueryTable != null && subqueryTable != sourceTable) { 3346 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3347 logInfo("syncColumnToLegacy: Correcting sourceTable from " + 3348 (sourceTable != null ? sourceTable.getTableName() : "null") + 3349 " to subquery " + subqueryTable.getTableName() + " for qualified column " + column.toString()); 3350 } 3351 sourceTable = subqueryTable; 3352 column.setSourceTable(sourceTable); 3353 } 3354 } 3355 } 3356 3357 // If sourceTable is null, try to get it from ColumnSource 3358 // This handles columns resolved to derived tables (subqueries with aliases) 3359 // where TSQLResolver2 resolved via ColumnSource but didn't set sourceTable on TObjectName 3360 if (sourceTable == null && source != null) { 3361 TTable finalTable = source.getFinalTable(); 3362 if (finalTable != null) { 3363 sourceTable = finalTable; 3364 column.setSourceTable(sourceTable); 3365 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3366 logInfo("syncColumnToLegacy: Set sourceTable from ColumnSource.getFinalTable() for " 3367 + column.toString() + " -> " + finalTable.getTableName()); 3368 } 3369 } else { 3370 // Try getAllFinalTables() - this may succeed when getFinalTable() returns null 3371 // For example, columns inferred through star push-down may have overrideTable set 3372 // which getAllFinalTables() will return as a single-element list 3373 java.util.List<TTable> allFinalTables = source.getAllFinalTables(); 3374 if (allFinalTables != null && !allFinalTables.isEmpty()) { 3375 // Use the first non-subquery table from allFinalTables 3376 for (TTable candidateTable : allFinalTables) { 3377 if (candidateTable != null && candidateTable.getTableType() != ETableSource.subquery) { 3378 sourceTable = candidateTable; 3379 column.setSourceTable(sourceTable); 3380 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3381 logInfo("syncColumnToLegacy: Set sourceTable from ColumnSource.getAllFinalTables() for " 3382 + column.toString() + " -> " + candidateTable.getTableName()); 3383 } 3384 break; 3385 } 3386 } 3387 } 3388 3389 // Fallback: try overrideTable for cases like derived tables in JOIN ON clauses 3390 if (sourceTable == null) { 3391 TTable overrideTable = source.getOverrideTable(); 3392 if (overrideTable != null) { 3393 sourceTable = overrideTable; 3394 column.setSourceTable(sourceTable); 3395 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3396 logInfo("syncColumnToLegacy: Set sourceTable from ColumnSource.getOverrideTable() for " 3397 + column.toString() + " -> " + overrideTable.getTableName()); 3398 } 3399 } 3400 } 3401 } 3402 } 3403 3404 if (sourceTable == null) { 3405 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE && source != null) { 3406 logInfo("syncColumnToLegacy: Column " + column.toString() 3407 + " has ColumnSource but no table. Namespace: " 3408 + (source.getSourceNamespace() != null ? source.getSourceNamespace().getClass().getSimpleName() : "null") 3409 + ", evidence: " + source.getEvidence()); 3410 } 3411 return false; 3412 } 3413 3414 // For struct-field access (e.g., customer.customer_id in BigQuery), 3415 // create a synthetic column representing the base column (e.g., "customer") 3416 // instead of using the original column which has the field name (e.g., "customer_id") 3417 if (source != null && "struct_field_access".equals(source.getEvidence())) { 3418 String baseColumnName = source.getExposedName(); 3419 if (baseColumnName != null && !baseColumnName.isEmpty()) { 3420 // Create synthetic TObjectName for the base column 3421 EDbVendor vendor = config != null ? config.getVendor() : EDbVendor.dbvbigquery; 3422 TObjectName baseColumn = TObjectName.createObjectName( 3423 vendor, EDbObjectType.column, baseColumnName); 3424 baseColumn.setSourceTable(sourceTable); 3425 3426 // Add the base column to linkedColumns (avoid duplicates by name) 3427 gudusoft.gsqlparser.nodes.TObjectNameList linkedColumns = sourceTable.getLinkedColumns(); 3428 if (linkedColumns != null && !containsColumnByName(linkedColumns, baseColumnName)) { 3429 linkedColumns.addObjectName(baseColumn); 3430 } 3431 return true; // Skip adding the original column 3432 } 3433 } 3434 3435 // 1. Add to TTable.linkedColumns (avoid duplicates) 3436 gudusoft.gsqlparser.nodes.TObjectNameList linkedColumns = sourceTable.getLinkedColumns(); 3437 if (linkedColumns != null && !containsColumn(linkedColumns, column)) { 3438 linkedColumns.addObjectName(column); 3439 } 3440 3441 // 2. For UNION scenarios, also add to all final tables from UNION branches 3442 // This is critical for star column push-down tests that expect columns to be 3443 // linked to ALL tables in a UNION, not just the first one. 3444 if (source != null) { 3445 java.util.List<TTable> allFinalTables = source.getAllFinalTables(); 3446 if (allFinalTables != null && allFinalTables.size() > 1) { 3447 for (TTable unionTable : allFinalTables) { 3448 if (unionTable == null || unionTable == sourceTable) continue; 3449 // Skip subquery types - only link to physical tables 3450 if (unionTable.getTableType() == ETableSource.subquery) continue; 3451 gudusoft.gsqlparser.nodes.TObjectNameList unionLinkedColumns = unionTable.getLinkedColumns(); 3452 if (unionLinkedColumns != null && !containsColumn(unionLinkedColumns, column)) { 3453 unionLinkedColumns.addObjectName(column); 3454 } 3455 } 3456 } 3457 3458 // 2b. For CTE columns, also link to the CTE reference table 3459 // When a column is resolved through a CTE, it should be linked to both: 3460 // - The CTE reference table (immediate source) 3461 // - The underlying physical tables (final source) 3462 INamespace ns = source.getSourceNamespace(); 3463 if (ns instanceof gudusoft.gsqlparser.resolver2.namespace.CTENamespace) { 3464 gudusoft.gsqlparser.resolver2.namespace.CTENamespace cteNs = 3465 (gudusoft.gsqlparser.resolver2.namespace.CTENamespace) ns; 3466 TTable cteTable = cteNs.getReferencingTable(); 3467 if (cteTable != null && cteTable != sourceTable) { 3468 gudusoft.gsqlparser.nodes.TObjectNameList cteLinkedColumns = cteTable.getLinkedColumns(); 3469 if (cteLinkedColumns != null && !containsColumn(cteLinkedColumns, column)) { 3470 cteLinkedColumns.addObjectName(column); 3471 } 3472 } 3473 } 3474 3475 // 2c. For subquery columns, also link to the underlying physical tables 3476 // When sourceTable is a subquery (e.g., qualified column S.id from MERGE USING subquery), 3477 // TGetTableColumn needs the column to be linked to physical tables for output. 3478 // Use getFinalTable() to trace through to the ultimate physical table. 3479 // IMPORTANT: Only link if a column with the same name doesn't already exist - 3480 // this avoids duplicates when both outer and inner queries reference the same column. 3481 // EXCEPTION: Skip MERGE ON clause columns - they should not be linked to the source 3482 // subquery's underlying table because they may belong to the target table instead. 3483 if (sourceTable.getTableType() == ETableSource.subquery) { 3484 // Skip UNQUALIFIED join condition columns - they should not be traced to the source 3485 // subquery's underlying table via star column expansion. 3486 // This is particularly important for MERGE ON clause columns which may 3487 // belong to the target table rather than the source subquery. 3488 // QUALIFIED columns (like S.id) should still be traced as they explicitly reference 3489 // the source subquery. 3490 // Note: We check location only because ownStmt may be null for unresolved columns. 3491 boolean isUnqualifiedJoinConditionColumn = (column.getLocation() == ESqlClause.joinCondition) 3492 && (column.getTableString() == null || column.getTableString().isEmpty()); 3493 if (isUnqualifiedJoinConditionColumn && TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3494 logInfo("syncColumnToLegacy: Skipping unqualified join condition column " + column.toString() + 3495 " - should not be traced to subquery's underlying table"); 3496 } 3497 3498 if (!isUnqualifiedJoinConditionColumn) { 3499 TTable finalTable = source.getFinalTable(); 3500 if (finalTable != null && finalTable != sourceTable && 3501 finalTable.getTableType() != ETableSource.subquery) { 3502 gudusoft.gsqlparser.nodes.TObjectNameList finalLinkedColumns = finalTable.getLinkedColumns(); 3503 if (finalLinkedColumns != null && !containsColumn(finalLinkedColumns, column) 3504 && !containsColumnByName(finalLinkedColumns, column.getColumnNameOnly())) { 3505 finalLinkedColumns.addObjectName(column); 3506 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3507 logInfo("syncColumnToLegacy: Also linked " + column.toString() + 3508 " to underlying physical table " + finalTable.getTableName()); 3509 } 3510 } 3511 } 3512 } 3513 } 3514 3515 } 3516 3517 // 3. Sync linkedColumnDef and sourceColumn from ColumnSource 3518 if (source != null) { 3519 Object defNode = source.getDefinitionNode(); 3520 3521 // Set linkedColumnDef if definition is a TColumnDefinition 3522 if (defNode instanceof gudusoft.gsqlparser.nodes.TColumnDefinition) { 3523 column.setLinkedColumnDef((gudusoft.gsqlparser.nodes.TColumnDefinition) defNode); 3524 } 3525 3526 // Set sourceColumn if definition is a TResultColumn 3527 // BUT skip for CTE explicit columns - these reference the CTE column name (e.g., "mgr_dept") 3528 // not the underlying SELECT column (e.g., "grp"). The CTE column is a TObjectName, 3529 // not a TResultColumn, so we cannot set it as sourceColumn. 3530 if (defNode instanceof TResultColumn) { 3531 String evidence = source.getEvidence(); 3532 boolean isCTEExplicitColumn = evidence != null && evidence.startsWith("cte_explicit_column"); 3533 if (!isCTEExplicitColumn) { 3534 column.setSourceColumn((TResultColumn) defNode); 3535 } 3536 } 3537 // Special case: for star-inferred columns, set sourceColumn to the star column 3538 // The definitionNode is intentionally null to avoid affecting formatter output, 3539 // but we still need to set sourceColumn for legacy API compatibility. 3540 // Use setSourceColumnOnly to avoid changing dbObjectType which affects filtering. 3541 else if (defNode == null && source.getEvidence() != null 3542 && source.getEvidence().contains("auto_inferred")) { 3543 // This is a star-inferred column - get the star column from the namespace 3544 INamespace namespace = source.getSourceNamespace(); 3545 if (namespace != null) { 3546 TResultColumn starColumn = namespace.getStarColumn(); 3547 if (starColumn != null) { 3548 column.setSourceColumnOnly(starColumn); 3549 } 3550 } 3551 } 3552 } 3553 3554 return true; 3555 } 3556 3557 /** 3558 * Check if a column already exists in the list (by identity). 3559 */ 3560 private boolean containsColumn(gudusoft.gsqlparser.nodes.TObjectNameList list, TObjectName column) { 3561 for (int i = 0; i < list.size(); i++) { 3562 if (list.getObjectName(i) == column) { 3563 return true; 3564 } 3565 } 3566 return false; 3567 } 3568 3569 /** 3570 * Check if a column with the given name already exists in the list. 3571 * Used for struct-field access where we create synthetic columns. 3572 */ 3573 private boolean containsColumnByName(gudusoft.gsqlparser.nodes.TObjectNameList list, String columnName) { 3574 if (columnName == null) return false; 3575 // Normalize by stripping quotes for comparison 3576 String normalizedName = stripQuotes(columnName); 3577 for (int i = 0; i < list.size(); i++) { 3578 TObjectName col = list.getObjectName(i); 3579 if (col != null) { 3580 String existingName = stripQuotes(col.getColumnNameOnly()); 3581 if (normalizedName.equalsIgnoreCase(existingName)) { 3582 return true; 3583 } 3584 } 3585 } 3586 return false; 3587 } 3588 3589 /** 3590 * Strip leading/trailing quote characters from a string. 3591 */ 3592 private String stripQuotes(String s) { 3593 if (s == null) return null; 3594 if (s.length() >= 2) { 3595 char first = s.charAt(0); 3596 char last = s.charAt(s.length() - 1); 3597 if ((first == '"' && last == '"') || 3598 (first == '\'' && last == '\'') || 3599 (first == '`' && last == '`') || 3600 (first == '[' && last == ']')) { 3601 return s.substring(1, s.length() - 1); 3602 } 3603 } 3604 return s; 3605 } 3606 3607 /** 3608 * Check if a subquery SELECT statement has an explicit (non-star) column with the given name. 3609 * This is used to determine whether to create traced column clones: 3610 * - If the column matches an explicit column in the subquery, don't clone (stays at subquery level) 3611 * - If the column doesn't match explicit columns (must come from star), clone to physical table 3612 * 3613 * @param subquery the SELECT statement to check 3614 * @param columnName the column name to look for (may have quotes) 3615 * @return true if the subquery has an explicit column matching the name 3616 */ 3617 private boolean subqueryHasExplicitColumn(TSelectSqlStatement subquery, String columnName) { 3618 if (subquery == null || columnName == null) { 3619 return false; 3620 } 3621 3622 // For combined queries (UNION/INTERSECT/EXCEPT), check the left branch 3623 if (subquery.isCombinedQuery()) { 3624 return subqueryHasExplicitColumn(subquery.getLeftStmt(), columnName); 3625 } 3626 3627 TResultColumnList resultColumns = subquery.getResultColumnList(); 3628 if (resultColumns == null) { 3629 return false; 3630 } 3631 3632 // Normalize the column name for comparison (strip quotes) 3633 String normalizedName = stripQuotes(columnName); 3634 3635 for (int i = 0; i < resultColumns.size(); i++) { 3636 TResultColumn rc = resultColumns.getResultColumn(i); 3637 if (rc == null) { 3638 continue; 3639 } 3640 3641 String colStr = rc.toString(); 3642 // Skip star columns - they're not explicit columns 3643 if (colStr != null && (colStr.equals("*") || colStr.endsWith(".*"))) { 3644 continue; 3645 } 3646 3647 // Get the effective column name (alias if present, otherwise the column name) 3648 String effectiveName = null; 3649 if (rc.getAliasClause() != null && rc.getAliasClause().getAliasName() != null) { 3650 effectiveName = rc.getAliasClause().getAliasName().toString(); 3651 } else if (rc.getExpr() != null && rc.getExpr().getObjectOperand() != null) { 3652 // For simple column references like "t1.COL1", get the column name 3653 effectiveName = rc.getExpr().getObjectOperand().getColumnNameOnly(); 3654 } 3655 3656 if (effectiveName != null) { 3657 String normalizedEffective = stripQuotes(effectiveName); 3658 if (normalizedName.equalsIgnoreCase(normalizedEffective)) { 3659 return true; 3660 } 3661 } 3662 } 3663 3664 return false; 3665 } 3666 3667 /** 3668 * Expand star columns using push-down inferred columns from namespaces. 3669 * 3670 * This is the core of the star column push-down algorithm: 3671 * 1. Find all star columns in SELECT lists 3672 * 2. For each star column, find its source namespace(s) 3673 * 3. Get inferred columns from the namespace (collected during resolution) 3674 * 4. Expand the star column by populating attributeNodesDerivedFromFromClause 3675 * 3676 * This enables star column expansion without TSQLEnv metadata by using 3677 * columns referenced in outer queries to infer what the star expands to. 3678 */ 3679 private void expandStarColumnsUsingPushDown() { 3680 int expandedCount = 0; 3681 Set<TCustomSqlStatement> processedStmts = new HashSet<>(); 3682 3683 // Track expanded star columns by their string representation for syncing 3684 Map<String, ArrayList<TAttributeNode>> expandedStarCols = new HashMap<>(); 3685 3686 // Process all statements recursively 3687 for (int i = 0; i < sqlStatements.size(); i++) { 3688 expandedCount += expandStarColumnsInStatement(sqlStatements.get(i), processedStmts, expandedStarCols); 3689 } 3690 3691 // Sync expanded attributes to column references in getAllColumnReferences() 3692 // The result column TObjectNames might be different instances than those collected 3693 // during scope building, so we need to copy the expanded attrs 3694 if (scopeBuildResult != null && !expandedStarCols.isEmpty()) { 3695 for (TObjectName colRef : scopeBuildResult.getAllColumnReferences()) { 3696 if (colRef == null) continue; 3697 String colStr = colRef.toString(); 3698 if (colStr == null || !colStr.endsWith("*")) continue; 3699 3700 // Skip if already has expanded attrs 3701 ArrayList<TAttributeNode> existingAttrs = colRef.getAttributeNodesDerivedFromFromClause(); 3702 if (existingAttrs != null && !existingAttrs.isEmpty()) continue; 3703 3704 // Find matching expanded star column 3705 ArrayList<TAttributeNode> expandedAttrs = expandedStarCols.get(colStr); 3706 if (expandedAttrs != null && !expandedAttrs.isEmpty()) { 3707 // Copy the expanded attrs to this column reference 3708 for (TAttributeNode attr : expandedAttrs) { 3709 TAttributeNode.addNodeToList(attr, existingAttrs); 3710 } 3711 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3712 logInfo("Synced " + expandedAttrs.size() + " expanded attrs to column reference: " + colStr); 3713 } 3714 } 3715 } 3716 } 3717 3718 logInfo("Expanded star columns using push-down: " + expandedCount + " columns added"); 3719 } 3720 3721 /** 3722 * Recursively expand star columns in a statement and its nested statements. 3723 * Uses processedStmts to track ALL statements (not just SELECTs) to prevent infinite loops. 3724 */ 3725 private int expandStarColumnsInStatement(TCustomSqlStatement stmt, Set<TCustomSqlStatement> processedStmts, 3726 Map<String, ArrayList<TAttributeNode>> expandedStarCols) { 3727 if (stmt == null) return 0; 3728 3729 // Cycle detection: skip if already processed this statement 3730 if (processedStmts.contains(stmt)) { 3731 return 0; 3732 } 3733 processedStmts.add(stmt); 3734 3735 int count = 0; 3736 3737 // Handle SELECT statements 3738 if (stmt instanceof TSelectSqlStatement) { 3739 TSelectSqlStatement select = (TSelectSqlStatement) stmt; 3740 count += expandStarColumnsInSelect(select, expandedStarCols); 3741 3742 // Handle UNION/INTERSECT/EXCEPT 3743 if (select.isCombinedQuery()) { 3744 if (select.getLeftStmt() != null) { 3745 count += expandStarColumnsInStatement(select.getLeftStmt(), processedStmts, expandedStarCols); 3746 } 3747 if (select.getRightStmt() != null) { 3748 count += expandStarColumnsInStatement(select.getRightStmt(), processedStmts, expandedStarCols); 3749 } 3750 } 3751 } 3752 3753 // Handle MERGE statements specially - process the USING clause 3754 if (stmt instanceof gudusoft.gsqlparser.stmt.TMergeSqlStatement) { 3755 gudusoft.gsqlparser.stmt.TMergeSqlStatement merge = (gudusoft.gsqlparser.stmt.TMergeSqlStatement) stmt; 3756 TTable usingTable = merge.getUsingTable(); 3757 if (usingTable != null && usingTable.getSubquery() != null) { 3758 count += expandStarColumnsInStatement(usingTable.getSubquery(), processedStmts, expandedStarCols); 3759 } 3760 } 3761 3762 // Process nested statements 3763 if (stmt.getStatements() != null) { 3764 for (int i = 0; i < stmt.getStatements().size(); i++) { 3765 Object nested = stmt.getStatements().get(i); 3766 if (nested instanceof TCustomSqlStatement) { 3767 count += expandStarColumnsInStatement((TCustomSqlStatement) nested, processedStmts, expandedStarCols); 3768 } 3769 } 3770 } 3771 3772 // Process tables with subqueries 3773 if (stmt.tables != null) { 3774 for (int i = 0; i < stmt.tables.size(); i++) { 3775 TTable table = stmt.tables.getTable(i); 3776 if (table != null && table.getSubquery() != null) { 3777 count += expandStarColumnsInStatement(table.getSubquery(), processedStmts, expandedStarCols); 3778 } 3779 } 3780 } 3781 3782 // Process CTEs 3783 if (stmt.getCteList() != null) { 3784 for (int i = 0; i < stmt.getCteList().size(); i++) { 3785 TCTE cte = stmt.getCteList().getCTE(i); 3786 if (cte != null && cte.getSubquery() != null) { 3787 count += expandStarColumnsInStatement(cte.getSubquery(), processedStmts, expandedStarCols); 3788 } 3789 } 3790 } 3791 3792 return count; 3793 } 3794 3795 /** 3796 * Expand star columns in a SELECT statement's result column list. 3797 */ 3798 private int expandStarColumnsInSelect(TSelectSqlStatement select, Map<String, ArrayList<TAttributeNode>> expandedStarCols) { 3799 if (select == null || select.getResultColumnList() == null) return 0; 3800 3801 int count = 0; 3802 TResultColumnList resultCols = select.getResultColumnList(); 3803 3804 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3805 logInfo("expandStarColumnsInSelect: Processing SELECT with " + resultCols.size() + " result columns"); 3806 } 3807 3808 for (int i = 0; i < resultCols.size(); i++) { 3809 TResultColumn rc = resultCols.getResultColumn(i); 3810 if (rc == null || rc.getExpr() == null) continue; 3811 3812 TObjectName objName = rc.getExpr().getObjectOperand(); 3813 if (objName == null) continue; 3814 3815 String colStr = objName.toString(); 3816 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE && colStr != null) { 3817 logInfo("expandStarColumnsInSelect: Column " + i + ": " + colStr); 3818 } 3819 if (colStr == null || !colStr.endsWith("*")) continue; 3820 3821 // This is a star column - expand it 3822 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3823 logInfo("expandStarColumnsInSelect: Found star column: " + colStr); 3824 } 3825 count += expandSingleStarColumn(objName, select, colStr, rc); 3826 3827 // Track the expanded attrs for syncing to column references 3828 ArrayList<TAttributeNode> attrList = objName.getAttributeNodesDerivedFromFromClause(); 3829 if (attrList != null && !attrList.isEmpty()) { 3830 expandedStarCols.put(colStr, attrList); 3831 } 3832 } 3833 3834 return count; 3835 } 3836 3837 /** 3838 * Expand a single star column using push-down inferred columns. 3839 * 3840 * @param starColumn The star column TObjectName (e.g., "*" or "src.*") 3841 * @param select The containing SELECT statement 3842 * @param colStr The string representation of the star column 3843 * @param resultColumn The TResultColumn containing the star (for EXCEPT column list) 3844 * @return Number of columns added 3845 */ 3846 private int expandSingleStarColumn(TObjectName starColumn, TSelectSqlStatement select, String colStr, TResultColumn resultColumn) { 3847 ArrayList<TAttributeNode> attrList = starColumn.getAttributeNodesDerivedFromFromClause(); 3848 3849 // Skip if already expanded 3850 if (!attrList.isEmpty()) { 3851 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3852 logInfo("expandSingleStarColumn: " + colStr + " already expanded with " + attrList.size() + " attrs"); 3853 } 3854 return 0; 3855 } 3856 3857 // Collect EXCEPT column names to exclude from expansion 3858 // (BigQuery: SELECT * EXCEPT (col1, col2) FROM ...) 3859 Set<String> exceptColumns = new HashSet<>(); 3860 if (resultColumn != null) { 3861 TObjectNameList exceptList = resultColumn.getExceptColumnList(); 3862 if (exceptList != null && exceptList.size() > 0) { 3863 for (int i = 0; i < exceptList.size(); i++) { 3864 TObjectName exceptCol = exceptList.getObjectName(i); 3865 if (exceptCol != null) { 3866 String exceptName = exceptCol.getColumnNameOnly(); 3867 if (exceptName != null && !exceptName.isEmpty()) { 3868 exceptColumns.add(exceptName.toUpperCase()); 3869 } 3870 } 3871 } 3872 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3873 logInfo("expandSingleStarColumn: Found " + exceptColumns.size() + 3874 " EXCEPT columns: " + exceptColumns); 3875 } 3876 } 3877 } 3878 3879 int count = 0; 3880 boolean isQualified = colStr.contains(".") && !colStr.equals("*"); 3881 3882 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3883 logInfo("expandSingleStarColumn: " + colStr + " isQualified=" + isQualified); 3884 } 3885 3886 if (isQualified) { 3887 // Qualified star (e.g., "src.*") - find the specific table/namespace 3888 String tablePrefix = colStr.substring(0, colStr.lastIndexOf('.')); 3889 count += expandQualifiedStar(starColumn, select, tablePrefix, attrList, exceptColumns); 3890 } else { 3891 // Unqualified star (*) - expand from all tables in FROM clause 3892 count += expandUnqualifiedStar(starColumn, select, attrList, exceptColumns); 3893 } 3894 3895 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3896 logInfo("expandSingleStarColumn: " + colStr + " expanded to " + count + " columns"); 3897 } 3898 3899 return count; 3900 } 3901 3902 /** 3903 * Expand a qualified star column (e.g., "src.*") using namespace inferred columns. 3904 * 3905 * @param starColumn The star column TObjectName 3906 * @param select The containing SELECT statement 3907 * @param tablePrefix The table prefix (e.g., "src" from "src.*") 3908 * @param attrList The list to add expanded attributes to 3909 * @param exceptColumns Column names to exclude (from EXCEPT clause), uppercase 3910 */ 3911 private int expandQualifiedStar(TObjectName starColumn, TSelectSqlStatement select, 3912 String tablePrefix, ArrayList<TAttributeNode> attrList, 3913 Set<String> exceptColumns) { 3914 int count = 0; 3915 3916 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3917 logInfo("expandQualifiedStar: tablePrefix=" + tablePrefix + 3918 ", exceptColumns=" + (exceptColumns != null ? exceptColumns : "none")); 3919 } 3920 3921 // Find the source table by alias or name 3922 TTable sourceTable = findTableByPrefixInSelect(select, tablePrefix); 3923 if (sourceTable == null) { 3924 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3925 logInfo("expandQualifiedStar: No source table found for " + tablePrefix); 3926 } 3927 // Fall back to just adding the qualified star attribute 3928 TAttributeNode.addNodeToList( 3929 new TAttributeNode(tablePrefix + ".*", null), 3930 attrList 3931 ); 3932 return 0; 3933 } 3934 3935 // Collect inferred columns from multiple sources: 3936 // 1. The table's own namespace (TableNamespace) 3937 // 2. If the SELECT is a CTE definition, the CTE's namespace 3938 // 3. If the SELECT is a subquery, the containing scope's namespace 3939 Set<String> allInferredCols = new HashSet<>(); 3940 3941 // Source 1: Get namespace for this table 3942 INamespace tableNamespace = scopeBuildResult != null 3943 ? scopeBuildResult.getNamespaceForTable(sourceTable) 3944 : null; 3945 3946 if (tableNamespace != null) { 3947 Set<String> inferredCols = tableNamespace.getInferredColumns(); 3948 if (inferredCols != null) { 3949 allInferredCols.addAll(inferredCols); 3950 } 3951 } 3952 3953 // Source 2: Check if this SELECT is part of a CTE definition 3954 // If so, the CTE namespace may have inferred columns from outer queries 3955 Set<String> cteInferredCols = getInferredColumnsFromContainingCTE(select); 3956 if (cteInferredCols != null) { 3957 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3958 logInfo("expandQualifiedStar: Adding " + cteInferredCols.size() + 3959 " CTE inferred columns for " + tablePrefix); 3960 } 3961 allInferredCols.addAll(cteInferredCols); 3962 } 3963 3964 // Source 3: Check the SELECT's output scope for inferred columns 3965 // IMPORTANT: For qualified star columns (like ta.*), only use scope-level inferred columns 3966 // if they actually exist in this table's namespace. Otherwise we'd incorrectly add columns 3967 // from other tables in the FROM clause to this star's expanded attributes. 3968 IScope selectScope = scopeBuildResult != null 3969 ? scopeBuildResult.getScopeForStatement(select) 3970 : null; 3971 if (selectScope != null) { 3972 Set<String> scopeInferredCols = getInferredColumnsFromScope(selectScope); 3973 if (scopeInferredCols != null && tableNamespace != null) { 3974 // Only add scope-level inferred columns that actually exist in this table's namespace 3975 // This prevents columns from other tables being incorrectly associated with this star 3976 Map<String, ColumnSource> columnSources = tableNamespace.getAllColumnSources(); 3977 Set<String> tableInferredCols = tableNamespace.getInferredColumns(); 3978 for (String scopeCol : scopeInferredCols) { 3979 // Check if this column can be resolved within this table's namespace 3980 boolean hasInNamespace = (columnSources != null && columnSources.containsKey(scopeCol)) || 3981 (tableInferredCols != null && tableInferredCols.contains(scopeCol)); 3982 if (hasInNamespace) { 3983 allInferredCols.add(scopeCol); 3984 } 3985 } 3986 } else if (scopeInferredCols != null && tableNamespace == null) { 3987 // No table namespace - add all scope columns (fallback for edge cases) 3988 allInferredCols.addAll(scopeInferredCols); 3989 } 3990 } 3991 3992 if (!allInferredCols.isEmpty()) { 3993 // Expand using inferred columns, filtering out EXCEPT columns 3994 for (String colName : allInferredCols) { 3995 // Skip columns in EXCEPT clause 3996 if (exceptColumns != null && !exceptColumns.isEmpty() && 3997 exceptColumns.contains(colName.toUpperCase())) { 3998 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 3999 logInfo("expandQualifiedStar: Skipping EXCEPT column: " + colName); 4000 } 4001 continue; 4002 } 4003 String attrName = tablePrefix + "." + colName; 4004 TAttributeNode.addNodeToList( 4005 new TAttributeNode(attrName, sourceTable), 4006 attrList 4007 ); 4008 count++; 4009 } 4010 } else if (tableNamespace != null) { 4011 // No inferred columns - try to get from namespace's column sources 4012 Map<String, ColumnSource> columnSources = tableNamespace.getAllColumnSources(); 4013 if (columnSources != null && !columnSources.isEmpty()) { 4014 for (String colName : columnSources.keySet()) { 4015 // Skip columns in EXCEPT clause 4016 if (exceptColumns != null && !exceptColumns.isEmpty() && 4017 exceptColumns.contains(colName.toUpperCase())) { 4018 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 4019 logInfo("expandQualifiedStar: Skipping EXCEPT column from sources: " + colName); 4020 } 4021 continue; 4022 } 4023 String attrName = tablePrefix + "." + colName; 4024 TAttributeNode.addNodeToList( 4025 new TAttributeNode(attrName, sourceTable), 4026 attrList 4027 ); 4028 count++; 4029 } 4030 } 4031 } 4032 4033 // If no columns were added, add the star as fallback 4034 if (count == 0) { 4035 TAttributeNode.addNodeToList( 4036 new TAttributeNode(tablePrefix + ".*", sourceTable), 4037 attrList 4038 ); 4039 } 4040 4041 return count; 4042 } 4043 4044 /** 4045 * Get inferred columns from a CTE that contains the given SELECT statement. 4046 * Used for push-down: when outer queries reference columns from a CTE, 4047 * those columns are inferred in the CTE's namespace and should be used 4048 * to expand star columns in the CTE's SELECT. 4049 */ 4050 private Set<String> getInferredColumnsFromContainingCTE(TSelectSqlStatement select) { 4051 if (select == null || scopeBuildResult == null || namespaceEnhancer == null) { 4052 return null; 4053 } 4054 4055 // Find the CTE that defines this SELECT 4056 Set<INamespace> starNamespaces = namespaceEnhancer.getStarNamespaces(); 4057 if (starNamespaces == null) { 4058 return null; 4059 } 4060 4061 for (INamespace ns : starNamespaces) { 4062 if (ns instanceof CTENamespace) { 4063 CTENamespace cteNs = (CTENamespace) ns; 4064 TSelectSqlStatement cteSelect = cteNs.getSelectStatement(); 4065 // Check both by reference and by start token position 4066 if (cteSelect == select || 4067 (cteSelect != null && select != null && 4068 cteSelect.getStartToken() != null && select.getStartToken() != null && 4069 cteSelect.getStartToken().posinlist == select.getStartToken().posinlist)) { 4070 Set<String> inferredCols = cteNs.getInferredColumns(); 4071 if (inferredCols != null && !inferredCols.isEmpty()) { 4072 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 4073 logInfo("getInferredColumnsFromContainingCTE: Found CTE " + cteNs.getDisplayName() + 4074 " with " + inferredCols.size() + " inferred columns"); 4075 } 4076 return inferredCols; 4077 } 4078 } 4079 } else if (ns instanceof SubqueryNamespace) { 4080 SubqueryNamespace subNs = (SubqueryNamespace) ns; 4081 TSelectSqlStatement subSelect = subNs.getSelectStatement(); 4082 if (subSelect == select || 4083 (subSelect != null && select != null && 4084 subSelect.getStartToken() != null && select.getStartToken() != null && 4085 subSelect.getStartToken().posinlist == select.getStartToken().posinlist)) { 4086 Set<String> inferredCols = subNs.getInferredColumns(); 4087 if (inferredCols != null && !inferredCols.isEmpty()) { 4088 if (TBaseType.DUMP_RESOLVER_LOG_TO_CONSOLE) { 4089 logInfo("getInferredColumnsFromContainingCTE: Found Subquery with " + 4090 inferredCols.size() + " inferred columns"); 4091 } 4092 return inferredCols; 4093 } 4094 } 4095 } 4096 } 4097 4098 return null; 4099 } 4100 4101 /** 4102 * Get inferred columns from namespaces in a scope's FROM clause. 4103 */ 4104 private Set<String> getInferredColumnsFromScope(IScope scope) { 4105 if (scope == null) { 4106 return null; 4107 } 4108 4109 Set<String> result = new HashSet<>(); 4110 4111 // Check all namespaces in the scope's children 4112 for (gudusoft.gsqlparser.resolver2.model.ScopeChild child : scope.getChildren()) { 4113 INamespace ns = child.getNamespace(); 4114 if (ns != null) { 4115 Set<String> inferredCols = ns.getInferredColumns(); 4116 if (inferredCols != null) { 4117 result.addAll(inferredCols); 4118 } 4119 } 4120 } 4121 4122 return result.isEmpty() ? null : result; 4123 } 4124 4125 /** 4126 * Expand an unqualified star column (*) using all tables in FROM clause. 4127 * 4128 * @param starColumn The star column TObjectName 4129 * @param select The containing SELECT statement 4130 * @param attrList The list to add expanded attributes to 4131 * @param exceptColumns Column names to exclude (from EXCEPT clause), uppercase 4132 */ 4133 private int expandUnqualifiedStar(TObjectName starColumn, TSelectSqlStatement select, 4134 ArrayList<TAttributeNode> attrList, Set<String> exceptColumns) { 4135 int count = 0; 4136 4137 if (select.tables == null) return 0; 4138 4139 for (int i = 0; i < select.tables.size(); i++) { 4140 TTable table = select.tables.getTable(i); 4141 if (table == null) continue; 4142 4143 // Skip certain table types 4144 if (table.getTableType() == ETableSource.join) continue; 4145 4146 String tablePrefix = table.getAliasName(); 4147 if (tablePrefix == null || tablePrefix.isEmpty()) { 4148 tablePrefix = table.getName(); 4149 } 4150 if (tablePrefix == null) continue; 4151 4152 // Get namespace for this table 4153 INamespace namespace = scopeBuildResult != null 4154 ? scopeBuildResult.getNamespaceForTable(table) 4155 : null; 4156 4157 if (namespace != null) { 4158 Set<String> inferredCols = namespace.getInferredColumns(); 4159 4160 if (inferredCols != null && !inferredCols.isEmpty()) { 4161 for (String colName : inferredCols) { 4162 // Skip columns in EXCEPT clause 4163 if (exceptColumns != null && !exceptColumns.isEmpty() && 4164 exceptColumns.contains(colName.toUpperCase())) { 4165 continue; 4166 } 4167 String attrName = tablePrefix + "." + colName; 4168 TAttributeNode.addNodeToList( 4169 new TAttributeNode(attrName, table), 4170 attrList 4171 ); 4172 count++; 4173 } 4174 } else { 4175 Map<String, ColumnSource> columnSources = namespace.getAllColumnSources(); 4176 if (columnSources != null && !columnSources.isEmpty()) { 4177 for (String colName : columnSources.keySet()) { 4178 // Skip columns in EXCEPT clause 4179 if (exceptColumns != null && !exceptColumns.isEmpty() && 4180 exceptColumns.contains(colName.toUpperCase())) { 4181 continue; 4182 } 4183 String attrName = tablePrefix + "." + colName; 4184 TAttributeNode.addNodeToList( 4185 new TAttributeNode(attrName, table), 4186 attrList 4187 ); 4188 count++; 4189 } 4190 } 4191 } 4192 } 4193 4194 // If no columns for this table, add the star as fallback 4195 if (count == 0 || (namespace != null && namespace.getInferredColumns().isEmpty() 4196 && namespace.getAllColumnSources().isEmpty())) { 4197 TAttributeNode.addNodeToList( 4198 new TAttributeNode(tablePrefix + ".*", table), 4199 attrList 4200 ); 4201 } 4202 } 4203 4204 return count; 4205 } 4206 4207 /** 4208 * Find a table by its prefix (alias or name) in a SELECT statement. 4209 */ 4210 private TTable findTableByPrefixInSelect(TSelectSqlStatement select, String prefix) { 4211 if (select == null || select.tables == null || prefix == null) return null; 4212 4213 // Normalize prefix (remove backticks, quotes, schema prefix for comparison) 4214 String normalizedPrefix = normalizeTablePrefix(prefix); 4215 4216 for (int i = 0; i < select.tables.size(); i++) { 4217 TTable table = select.tables.getTable(i); 4218 if (table == null) continue; 4219 4220 // Check alias first 4221 String alias = table.getAliasName(); 4222 if (alias != null && normalizeTablePrefix(alias).equalsIgnoreCase(normalizedPrefix)) { 4223 return table; 4224 } 4225 4226 // Check table name 4227 String name = table.getName(); 4228 if (name != null && normalizeTablePrefix(name).equalsIgnoreCase(normalizedPrefix)) { 4229 return table; 4230 } 4231 4232 // Check full table name (with schema) 4233 if (table.getTableName() != null) { 4234 String fullName = table.getTableName().toString(); 4235 if (fullName != null && normalizeTablePrefix(fullName).equalsIgnoreCase(normalizedPrefix)) { 4236 return table; 4237 } 4238 } 4239 } 4240 4241 return null; 4242 } 4243 4244 /** 4245 * Normalize table prefix for comparison (remove quotes, backticks). 4246 */ 4247 private String normalizeTablePrefix(String prefix) { 4248 if (prefix == null) return ""; 4249 String result = prefix.trim(); 4250 // Remove backticks 4251 if (result.startsWith("`") && result.endsWith("`")) { 4252 result = result.substring(1, result.length() - 1); 4253 } 4254 // Remove double quotes 4255 if (result.startsWith("\"") && result.endsWith("\"")) { 4256 result = result.substring(1, result.length() - 1); 4257 } 4258 // Remove brackets 4259 if (result.startsWith("[") && result.endsWith("]")) { 4260 result = result.substring(1, result.length() - 1); 4261 } 4262 return result; 4263 } 4264 4265 /** 4266 * Get resolution statistics 4267 */ 4268 public ResolutionStatistics getStatistics() { 4269 return resolutionContext.getStatistics(); 4270 } 4271 4272 /** 4273 * Get the resolution context (for advanced queries) 4274 */ 4275 public ResolutionContext getContext() { 4276 return resolutionContext; 4277 } 4278 4279 /** 4280 * Get the global scope 4281 */ 4282 public GlobalScope getGlobalScope() { 4283 return globalScope; 4284 } 4285 4286 /** 4287 * Get the configuration 4288 */ 4289 public TSQLResolverConfig getConfig() { 4290 return config; 4291 } 4292 4293 /** 4294 * Get the pass history (for iterative resolution analysis) 4295 * 4296 * @return list of all resolution passes (empty if non-iterative or not yet resolved) 4297 */ 4298 public List<ResolutionPass> getPassHistory() { 4299 return new ArrayList<>(passHistory); 4300 } 4301 4302 /** 4303 * Get the convergence detector (for iterative resolution analysis) 4304 * 4305 * @return convergence detector (null if iterative resolution is disabled) 4306 */ 4307 public ConvergenceDetector getConvergenceDetector() { 4308 return convergenceDetector; 4309 } 4310 4311 /** 4312 * Get the scope build result (for testing and analysis) 4313 * 4314 * @return scope build result from ScopeBuilder (null if not yet resolved) 4315 */ 4316 public ScopeBuildResult getScopeBuildResult() { 4317 return scopeBuildResult; 4318 } 4319 4320 /** 4321 * Get the resolution result access interface. 4322 * This provides a clean, statement-centric API for accessing resolution results. 4323 * 4324 * <p>Usage example:</p> 4325 * <pre> 4326 * TSQLResolver2 resolver = new TSQLResolver2(null, parser.sqlstatements); 4327 * resolver.resolve(); 4328 * 4329 * IResolutionResult result = resolver.getResult(); 4330 * 4331 * for (TCustomSqlStatement stmt : parser.sqlstatements) { 4332 * for (TTable table : result.getTables(stmt)) { 4333 * System.out.println("Table: " + table.getFullName()); 4334 * for (TObjectName col : result.getColumnsForTable(stmt, table)) { 4335 * System.out.println(" Column: " + col.getColumnNameOnly()); 4336 * } 4337 * } 4338 * } 4339 * </pre> 4340 * 4341 * @return resolution result access interface 4342 * @throws IllegalStateException if resolve() has not been called 4343 */ 4344 public IResolutionResult getResult() { 4345 if (scopeBuildResult == null) { 4346 throw new IllegalStateException( 4347 "Must call resolve() before getResult()"); 4348 } 4349 return new ResolutionResultImpl(scopeBuildResult, sqlStatements); 4350 } 4351 4352 // ===== Star Column Reverse Inference Support (Principle 3) ===== 4353 4354 /** 4355 * Star Column push-down context for reverse inference. 4356 * Tracks which columns should be added to which Namespaces based on 4357 * outer layer references. 4358 */ 4359 private static class StarPushDownContext { 4360 /** Namespace -> (ColumnName -> Confidence) */ 4361 private final Map<INamespace, Map<String, Double>> pushDownMap = new HashMap<>(); 4362 4363 /** 4364 * Record that a column should be added to a namespace. 4365 * If the same column is pushed multiple times, keep the highest confidence. 4366 */ 4367 public void pushColumn(INamespace namespace, String columnName, double confidence) { 4368 Map<String, Double> columns = pushDownMap.computeIfAbsent(namespace, k -> new HashMap<>()); 4369 columns.put(columnName, Math.max(confidence, columns.getOrDefault(columnName, 0.0))); 4370 } 4371 4372 /** 4373 * Get all columns that should be pushed to each namespace. 4374 */ 4375 public Map<INamespace, java.util.Set<String>> getAllPushDownColumns() { 4376 Map<INamespace, java.util.Set<String>> result = new HashMap<>(); 4377 for (Map.Entry<INamespace, Map<String, Double>> entry : pushDownMap.entrySet()) { 4378 result.put(entry.getKey(), entry.getValue().keySet()); 4379 } 4380 return result; 4381 } 4382 4383 /** 4384 * Get the confidence score for a specific column in a namespace. 4385 */ 4386 public double getConfidence(INamespace namespace, String columnName) { 4387 return pushDownMap.getOrDefault(namespace, java.util.Collections.emptyMap()) 4388 .getOrDefault(columnName, 0.0); 4389 } 4390 4391 /** 4392 * Get the total number of columns to be pushed down across all namespaces. 4393 */ 4394 public int getTotalPushedColumns() { 4395 return pushDownMap.values().stream() 4396 .mapToInt(Map::size) 4397 .sum(); 4398 } 4399 } 4400 4401 /** 4402 * Represents a star column source (CTE or subquery with SELECT *). 4403 * Used for reverse inference to track which columns are required from the star. 4404 */ 4405 private static class StarColumnSource { 4406 private final String name; // CTE name or subquery alias 4407 private final INamespace namespace; // The namespace for this source 4408 private final INamespace underlyingTableNamespace; // Namespace of the table behind SELECT * 4409 private final java.util.Set<String> requiredColumns = new java.util.HashSet<>(); 4410 4411 public StarColumnSource(String name, INamespace namespace, INamespace underlyingTableNamespace) { 4412 this.name = name; 4413 this.namespace = namespace; 4414 this.underlyingTableNamespace = underlyingTableNamespace; 4415 } 4416 4417 public String getName() { 4418 return name; 4419 } 4420 4421 public INamespace getNamespace() { 4422 return namespace; 4423 } 4424 4425 public void addRequiredColumn(String columnName) { 4426 requiredColumns.add(columnName); 4427 } 4428 4429 public java.util.Set<String> getRequiredColumns() { 4430 return requiredColumns; 4431 } 4432 4433 public boolean hasUnderlyingTable() { 4434 return underlyingTableNamespace != null; 4435 } 4436 4437 public INamespace getUnderlyingTableNamespace() { 4438 return underlyingTableNamespace; 4439 } 4440 4441 @Override 4442 public String toString() { 4443 return String.format("StarColumnSource[%s, required=%d]", name, requiredColumns.size()); 4444 } 4445 } 4446 4447 /** 4448 * Collect all star column sources (CTEs and subqueries with SELECT *). 4449 * Traverses the scope tree to find CTENamespace and SubqueryNamespace 4450 * that use SELECT * in their subqueries. 4451 */ 4452 private List<StarColumnSource> collectAllStarColumnSources() { 4453 List<StarColumnSource> sources = new ArrayList<>(); 4454 4455 // Traverse global scope tree 4456 if (globalScope != null) { 4457 collectStarSourcesFromScope(globalScope, sources); 4458 } 4459 4460 // Also traverse UPDATE scopes (for Teradata UPDATE...FROM syntax) 4461 if (scopeBuilder != null) { 4462 for (UpdateScope updateScope : scopeBuilder.getUpdateScopeMap().values()) { 4463 collectStarSourcesFromScope(updateScope, sources); 4464 } 4465 for (DeleteScope deleteScope : scopeBuilder.getDeleteScopeMap().values()) { 4466 collectStarSourcesFromScope(deleteScope, sources); 4467 } 4468 } 4469 4470 logDebug("Collected " + sources.size() + " star column sources"); 4471 return sources; 4472 } 4473 4474 /** 4475 * Recursively collect star column sources from a scope and its children. 4476 */ 4477 private void collectStarSourcesFromScope(IScope scope, List<StarColumnSource> sources) { 4478 // Check all child namespaces in this scope 4479 for (gudusoft.gsqlparser.resolver2.model.ScopeChild child : scope.getChildren()) { 4480 INamespace namespace = child.getNamespace(); 4481 4482 // Use the new interface method to check for star columns 4483 if (namespace.hasStarColumn()) { 4484 TSelectSqlStatement selectStmt = namespace.getSelectStatement(); 4485 INamespace underlyingNs = selectStmt != null ? getFirstTableNamespace(selectStmt) : null; 4486 4487 StarColumnSource starSource = new StarColumnSource( 4488 namespace.getDisplayName(), 4489 namespace, 4490 underlyingNs 4491 ); 4492 sources.add(starSource); 4493 4494 logDebug("Found star source: " + namespace.getDisplayName()); 4495 } 4496 } 4497 4498 // Recursively traverse child scopes based on scope type 4499 if (scope instanceof SelectScope) { 4500 SelectScope selectScope = (SelectScope) scope; 4501 if (selectScope.getFromScope() != null) { 4502 collectStarSourcesFromScope(selectScope.getFromScope(), sources); 4503 } 4504 } else if (scope instanceof UpdateScope) { 4505 UpdateScope updateScope = (UpdateScope) scope; 4506 if (updateScope.getFromScope() != null) { 4507 collectStarSourcesFromScope(updateScope.getFromScope(), sources); 4508 } 4509 } else if (scope instanceof DeleteScope) { 4510 DeleteScope deleteScope = (DeleteScope) scope; 4511 if (deleteScope.getFromScope() != null) { 4512 collectStarSourcesFromScope(deleteScope.getFromScope(), sources); 4513 } 4514 } 4515 } 4516 4517 4518 /** 4519 * Get the first table namespace from a SELECT statement's FROM clause. 4520 * Returns the DynamicStarSource if available. 4521 */ 4522 private INamespace getFirstTableNamespace(TSelectSqlStatement select) { 4523 if (select == null || select.tables == null || select.tables.size() == 0) { 4524 return null; 4525 } 4526 4527 // Get first table 4528 TTable firstTable = select.tables.getTable(0); 4529 String tableName = firstTable.getAliasName() != null 4530 ? firstTable.getAliasName() 4531 : firstTable.getName(); 4532 4533 // Search for corresponding namespace in all dynamic namespaces 4534 List<INamespace> dynamicNamespaces = getAllDynamicNamespaces(); 4535 for (INamespace ns : dynamicNamespaces) { 4536 if (ns.getDisplayName().equals(tableName)) { 4537 return ns; 4538 } 4539 } 4540 4541 return null; 4542 } 4543 4544 /** 4545 * Collect all outer references to a star column source. 4546 * Searches through allColumnReferences for columns that reference this star source. 4547 */ 4548 private List<TObjectName> collectOuterReferencesToSource(StarColumnSource starSource) { 4549 List<TObjectName> references = new ArrayList<>(); 4550 4551 if (starSource == null || starSource.getName() == null) { 4552 return references; 4553 } 4554 4555 String sourceName = starSource.getName(); 4556 4557 // Search through all collected column references 4558 for (TObjectName objName : allColumnReferences) { 4559 if (objName == null) { 4560 continue; 4561 } 4562 4563 // Check if this column reference is from the star source 4564 // E.g., for CTE named "my_cte", check if objName is like "my_cte.col1" 4565 String tableQualifier = getTableQualifier(objName); 4566 4567 if (tableQualifier != null && tableQualifier.equalsIgnoreCase(sourceName)) { 4568 references.add(objName); 4569 logDebug("Found outer reference: " + objName + " -> " + sourceName); 4570 } 4571 } 4572 4573 logDebug("Collected " + references.size() + " outer references for: " + sourceName); 4574 return references; 4575 } 4576 4577 /** 4578 * Get the table qualifier from a TObjectName. 4579 * E.g., for "schema.table.column", returns "table" 4580 * E.g., for "table.column", returns "table" 4581 * E.g., for "column", returns null 4582 */ 4583 private String getTableQualifier(TObjectName objName) { 4584 if (objName == null) { 4585 return null; 4586 } 4587 4588 // TObjectName has parts like: [schema, table, column] 4589 // or [table, column] 4590 // or [column] 4591 4592 // If there are 3 or more parts, the second-to-last is the table 4593 // If there are 2 parts, the first is the table 4594 // If there is 1 part, there's no table qualifier 4595 4596 String fullName = objName.toString(); 4597 String[] parts = fullName.split("\\."); 4598 4599 if (parts.length >= 3) { 4600 // schema.table.column -> return table 4601 return parts[parts.length - 2]; 4602 } else if (parts.length == 2) { 4603 // table.column -> return table 4604 return parts[0]; 4605 } else { 4606 // Just column name, no qualifier 4607 return null; 4608 } 4609 } 4610 4611 /** 4612 * Get all DynamicStarSource namespaces from the scope tree. 4613 * This is used to apply inference results to namespaces that need enhancement. 4614 */ 4615 private List<INamespace> getAllDynamicNamespaces() { 4616 List<INamespace> result = new ArrayList<>(); 4617 4618 // Collect from global scope tree 4619 if (globalScope != null) { 4620 collectDynamicNamespacesFromScope(globalScope, result); 4621 } 4622 4623 return result; 4624 } 4625 4626 /** 4627 * Recursively collect DynamicStarSource namespaces from a scope and its children. 4628 */ 4629 private void collectDynamicNamespacesFromScope(IScope scope, List<INamespace> result) { 4630 if (scope == null) { 4631 return; 4632 } 4633 4634 // Get all child namespaces from this scope 4635 for (gudusoft.gsqlparser.resolver2.model.ScopeChild child : scope.getChildren()) { 4636 INamespace namespace = child.getNamespace(); 4637 if (namespace instanceof gudusoft.gsqlparser.resolver2.namespace.DynamicStarSource) { 4638 result.add(namespace); 4639 logDebug("Found DynamicStarSource: " + namespace.getDisplayName()); 4640 } 4641 } 4642 4643 // Recursively traverse child scopes based on scope type 4644 if (scope instanceof SelectScope) { 4645 SelectScope selectScope = (SelectScope) scope; 4646 4647 // Traverse FROM scope 4648 if (selectScope.getFromScope() != null) { 4649 collectDynamicNamespacesFromScope(selectScope.getFromScope(), result); 4650 } 4651 } else if (scope instanceof CTEScope) { 4652 CTEScope cteScope = (CTEScope) scope; 4653 4654 // CTEs are already included in the children check above 4655 // But we need to check their subqueries by traversing nested scopes 4656 // The CTE namespaces themselves contain references to subquery scopes 4657 } else if (scope instanceof FromScope) { 4658 FromScope fromScope = (FromScope) scope; 4659 4660 // FROM scope children are already checked above 4661 // No additional child scopes to traverse 4662 } else if (scope instanceof GroupByScope) { 4663 GroupByScope groupByScope = (GroupByScope) scope; 4664 4665 // GroupBy scope typically doesn't have child scopes 4666 } else if (scope instanceof HavingScope) { 4667 HavingScope havingScope = (HavingScope) scope; 4668 4669 // Having scope typically doesn't have child scopes 4670 } else if (scope instanceof OrderByScope) { 4671 OrderByScope orderByScope = (OrderByScope) scope; 4672 4673 // OrderBy scope typically doesn't have child scopes 4674 } 4675 4676 // Additionally, traverse parent-child scope relationships 4677 // by checking if any of the namespaces contain nested SELECT statements 4678 for (gudusoft.gsqlparser.resolver2.model.ScopeChild child : scope.getChildren()) { 4679 INamespace namespace = child.getNamespace(); 4680 4681 // If this is a SubqueryNamespace, it contains a SELECT with its own scope tree 4682 if (namespace instanceof gudusoft.gsqlparser.resolver2.namespace.SubqueryNamespace) { 4683 // Subquery scopes are processed during scope building 4684 // and would be in statementScopeCache if we tracked them 4685 } 4686 } 4687 } 4688 4689 // ===== Logging helpers ===== 4690 4691 private void logInfo(String message) { 4692 TBaseType.log("[TSQLResolver2] " + message, TLog.INFO); 4693 } 4694 4695 private void logDebug(String message) { 4696 TBaseType.log("[TSQLResolver2] " + message, TLog.DEBUG); 4697 } 4698 4699 private void logError(String message) { 4700 TBaseType.log("[TSQLResolver2] " + message, TLog.ERROR); 4701 } 4702}