001package gudusoft.gsqlparser.resolver2.expansion; 002 003import gudusoft.gsqlparser.nodes.TObjectName; 004import gudusoft.gsqlparser.nodes.TResultColumn; 005import gudusoft.gsqlparser.nodes.TResultColumnList; 006import gudusoft.gsqlparser.resolver2.ScopeBuildResult; 007import gudusoft.gsqlparser.resolver2.model.ScopeChild; 008import gudusoft.gsqlparser.resolver2.namespace.INamespace; 009import gudusoft.gsqlparser.resolver2.scope.IScope; 010import gudusoft.gsqlparser.resolver2.scope.SelectScope; 011import gudusoft.gsqlparser.resolver2.scope.FromScope; 012import gudusoft.gsqlparser.resolver2.scope.CTEScope; 013import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 014import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher; 015import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 016 017import java.util.*; 018 019/** 020 * Resolves star columns by pushing down column references from outer queries. 021 * 022 * <p>This is the GSP Java implementation of the star column push-down mechanism. 023 * When an outer query references columns from a CTE or subquery that uses SELECT *, 024 * we need to "push down" those column references to infer which columns are needed. 025 * 026 * <p>Example scenario: 027 * <pre> 028 * WITH my_cte AS (SELECT * FROM Employees) 029 * SELECT id, name FROM my_cte; 030 * </pre> 031 * 032 * <p>Push-down process: 033 * 1. my_cte has SELECT * (star column) 034 * 2. Outer query references id, name from my_cte 035 * 3. Push down: id and name are "required" columns from the star 036 * 4. These columns are inferred to come from Employees table 037 * 038 * <p>Key features: 039 * - Works with ScopeBuildResult from ScopeBuilder 040 * - Multi-level push-down (nested CTEs/subqueries) 041 * - Iterative resolution (may need multiple passes) 042 * - Confidence scoring based on reference type 043 */ 044public class StarPushDownResolver { 045 046 /** 047 * Represents a column reference that needs to be pushed down. 048 */ 049 public static class PushDownRequest { 050 /** The column name being referenced */ 051 public final String columnName; 052 053 /** The column reference node */ 054 public final TObjectName columnReference; 055 056 /** The namespace containing the star column */ 057 public final INamespace starNamespace; 058 059 /** The star column (TResultColumn with *) */ 060 public final TResultColumn starColumn; 061 062 /** Confidence that this push-down is correct */ 063 public final double confidence; 064 065 /** Evidence for this push-down */ 066 public final String evidence; 067 068 public PushDownRequest( 069 String columnName, 070 TObjectName columnReference, 071 INamespace starNamespace, 072 TResultColumn starColumn, 073 double confidence, 074 String evidence) { 075 this.columnName = columnName; 076 this.columnReference = columnReference; 077 this.starNamespace = starNamespace; 078 this.starColumn = starColumn; 079 this.confidence = confidence; 080 this.evidence = evidence; 081 } 082 083 @Override 084 public String toString() { 085 return String.format("PushDown[%s -> %s, conf=%.2f]", 086 columnName, starNamespace.getDisplayName(), confidence); 087 } 088 } 089 090 /** 091 * Represents the result of push-down resolution. 092 */ 093 public static class PushDownResult { 094 /** Namespace -> list of columns pushed down to it */ 095 private final Map<INamespace, Set<String>> pushedDownColumns = new LinkedHashMap<>(); 096 097 /** Namespace -> list of column references from outer queries */ 098 private final Map<INamespace, List<TObjectName>> upLevelReferences = new LinkedHashMap<>(); 099 100 /** All processed requests */ 101 private final List<PushDownRequest> processedRequests = new ArrayList<>(); 102 103 /** Number of iterations performed */ 104 private int iterations = 0; 105 106 public void addPushedDownColumn(INamespace namespace, String columnName) { 107 pushedDownColumns.computeIfAbsent(namespace, k -> new LinkedHashSet<>()).add(columnName); 108 } 109 110 public void addUpLevelReference(INamespace namespace, TObjectName reference) { 111 upLevelReferences.computeIfAbsent(namespace, k -> new ArrayList<>()).add(reference); 112 } 113 114 public void addProcessedRequest(PushDownRequest request) { 115 processedRequests.add(request); 116 } 117 118 public Set<String> getPushedDownColumns(INamespace namespace) { 119 return pushedDownColumns.getOrDefault(namespace, Collections.emptySet()); 120 } 121 122 public List<TObjectName> getUpLevelReferences(INamespace namespace) { 123 return upLevelReferences.getOrDefault(namespace, Collections.emptyList()); 124 } 125 126 public int getTotalPushedDownColumns() { 127 int count = 0; 128 for (Set<String> columns : pushedDownColumns.values()) { 129 count += columns.size(); 130 } 131 return count; 132 } 133 134 public int getStarNamespaceCount() { 135 return pushedDownColumns.size(); 136 } 137 138 public int getIterations() { 139 return iterations; 140 } 141 142 void setIterations(int iterations) { 143 this.iterations = iterations; 144 } 145 146 public List<PushDownRequest> getProcessedRequests() { 147 return Collections.unmodifiableList(processedRequests); 148 } 149 150 @Override 151 public String toString() { 152 return String.format( 153 "PushDownResult[namespaces=%d, columns=%d, iterations=%d]", 154 pushedDownColumns.size(), 155 getTotalPushedDownColumns(), 156 iterations 157 ); 158 } 159 } 160 161 /** Maximum number of iterations for push-down resolution */ 162 private final int maxIterations; 163 164 /** Whether to enable debug logging */ 165 private final boolean debugLogging; 166 167 /** Name matcher for column comparison */ 168 private final INameMatcher nameMatcher; 169 170 public StarPushDownResolver() { 171 this(10, false, new DefaultNameMatcher()); 172 } 173 174 public StarPushDownResolver(int maxIterations, boolean debugLogging) { 175 this(maxIterations, debugLogging, new DefaultNameMatcher()); 176 } 177 178 public StarPushDownResolver(int maxIterations, boolean debugLogging, INameMatcher nameMatcher) { 179 this.maxIterations = maxIterations; 180 this.debugLogging = debugLogging; 181 this.nameMatcher = nameMatcher != null ? nameMatcher : new DefaultNameMatcher(); 182 } 183 184 /** 185 * Resolve star columns by pushing down column references. 186 * 187 * @param scopeBuildResult the scope build result from ScopeBuilder 188 * @return push-down resolution result 189 */ 190 public PushDownResult resolve(ScopeBuildResult scopeBuildResult) { 191 PushDownResult result = new PushDownResult(); 192 193 if (scopeBuildResult == null || scopeBuildResult.getGlobalScope() == null) { 194 return result; 195 } 196 197 // Step 1: Collect all namespaces with star columns 198 List<StarNamespaceInfo> starNamespaces = collectStarNamespaces(scopeBuildResult); 199 log("Found " + starNamespaces.size() + " namespaces with star columns"); 200 201 if (starNamespaces.isEmpty()) { 202 return result; 203 } 204 205 // Step 2: Iteratively collect push-down requests and process them 206 int iteration = 0; 207 int newColumnsFound; 208 209 do { 210 newColumnsFound = 0; 211 iteration++; 212 213 // Collect all column references that target star namespaces 214 List<PushDownRequest> requests = collectPushDownRequests( 215 scopeBuildResult, starNamespaces, result); 216 217 log("Iteration " + iteration + ": Found " + requests.size() + " push-down requests"); 218 219 // Process each request 220 for (PushDownRequest request : requests) { 221 if (processPushDownRequest(request, result)) { 222 newColumnsFound++; 223 } 224 } 225 226 log("Iteration " + iteration + ": Added " + newColumnsFound + " new columns"); 227 228 } while (newColumnsFound > 0 && iteration < maxIterations); 229 230 result.setIterations(iteration); 231 232 // Step 3: Apply inferred columns to dynamic namespaces 233 applyInferredColumns(result); 234 235 log("Push-down complete: " + result); 236 return result; 237 } 238 239 /** 240 * Information about a namespace with star column. 241 */ 242 private static class StarNamespaceInfo { 243 final INamespace namespace; 244 final TResultColumn starColumn; 245 final IScope containingScope; 246 final String alias; 247 248 StarNamespaceInfo(INamespace namespace, TResultColumn starColumn, 249 IScope containingScope, String alias) { 250 this.namespace = namespace; 251 this.starColumn = starColumn; 252 this.containingScope = containingScope; 253 this.alias = alias; 254 } 255 } 256 257 /** 258 * Collect all namespaces that have star columns. 259 */ 260 private List<StarNamespaceInfo> collectStarNamespaces(ScopeBuildResult scopeBuildResult) { 261 List<StarNamespaceInfo> result = new ArrayList<>(); 262 263 // Traverse all scopes 264 collectStarNamespacesFromScope(scopeBuildResult.getGlobalScope(), result); 265 266 // Also check SelectScopes from statementScopeMap 267 for (SelectScope selectScope : scopeBuildResult.getStatementScopeMap().values()) { 268 if (selectScope.getFromScope() != null) { 269 collectStarNamespacesFromFromScope(selectScope.getFromScope(), selectScope, result); 270 } 271 } 272 273 return result; 274 } 275 276 /** 277 * Recursively collect star namespaces from a scope. 278 */ 279 private void collectStarNamespacesFromScope(IScope scope, List<StarNamespaceInfo> result) { 280 if (scope == null) { 281 return; 282 } 283 284 // Check children of this scope 285 for (ScopeChild child : scope.getChildren()) { 286 INamespace ns = child.getNamespace(); 287 if (ns != null && ns.hasStarColumn()) { 288 TResultColumn starCol = findStarColumn(ns); 289 result.add(new StarNamespaceInfo(ns, starCol, scope, child.getAlias())); 290 } 291 } 292 293 // Recurse into child scopes 294 if (scope instanceof SelectScope) { 295 SelectScope selectScope = (SelectScope) scope; 296 if (selectScope.getFromScope() != null) { 297 collectStarNamespacesFromScope(selectScope.getFromScope(), result); 298 } 299 } 300 } 301 302 /** 303 * Collect star namespaces from a FromScope. 304 */ 305 private void collectStarNamespacesFromFromScope(FromScope fromScope, 306 SelectScope parentSelect, 307 List<StarNamespaceInfo> result) { 308 for (ScopeChild child : fromScope.getChildren()) { 309 INamespace ns = child.getNamespace(); 310 if (ns != null && ns.hasStarColumn()) { 311 // Avoid duplicates 312 boolean exists = result.stream() 313 .anyMatch(info -> info.namespace == ns); 314 if (!exists) { 315 TResultColumn starCol = findStarColumn(ns); 316 result.add(new StarNamespaceInfo(ns, starCol, parentSelect, child.getAlias())); 317 } 318 } 319 } 320 } 321 322 /** 323 * Find the star column in a namespace's SELECT statement. 324 */ 325 private TResultColumn findStarColumn(INamespace namespace) { 326 TSelectSqlStatement select = namespace.getSelectStatement(); 327 if (select == null || select.getResultColumnList() == null) { 328 return null; 329 } 330 331 TResultColumnList resultList = select.getResultColumnList(); 332 for (int i = 0; i < resultList.size(); i++) { 333 TResultColumn rc = resultList.getResultColumn(i); 334 if (rc != null && rc.toString().endsWith("*")) { 335 return rc; 336 } 337 } 338 return null; 339 } 340 341 /** 342 * Collect push-down requests by finding column references that target star namespaces. 343 */ 344 private List<PushDownRequest> collectPushDownRequests( 345 ScopeBuildResult scopeBuildResult, 346 List<StarNamespaceInfo> starNamespaces, 347 PushDownResult currentResult) { 348 349 List<PushDownRequest> requests = new ArrayList<>(); 350 351 // For each column reference in the build result 352 for (TObjectName colRef : scopeBuildResult.getAllColumnReferences()) { 353 // Get the scope where this column appears 354 IScope colScope = scopeBuildResult.getScopeForColumn(colRef); 355 if (colScope == null) { 356 continue; 357 } 358 359 // Extract table prefix and column name 360 String prefix = getTablePrefix(colRef); 361 String columnName = colRef.getColumnNameOnly(); 362 363 if (columnName == null || columnName.isEmpty() || columnName.equals("*")) { 364 continue; // Skip star columns themselves 365 } 366 367 // Check if this column reference targets any star namespace 368 for (StarNamespaceInfo starInfo : starNamespaces) { 369 if (matchesStarNamespace(colRef, prefix, starInfo, colScope)) { 370 // Check if already processed 371 Set<String> existing = currentResult.getPushedDownColumns(starInfo.namespace); 372 boolean alreadyExists = existing.stream() 373 .anyMatch(c -> nameMatcher.matches(c, columnName)); 374 375 if (!alreadyExists) { 376 double confidence = calculateConfidence(colRef, prefix, starInfo); 377 String evidence = buildEvidence(colRef, starInfo); 378 379 requests.add(new PushDownRequest( 380 columnName, 381 colRef, 382 starInfo.namespace, 383 starInfo.starColumn, 384 confidence, 385 evidence 386 )); 387 } 388 } 389 } 390 } 391 392 return requests; 393 } 394 395 /** 396 * Get the table prefix from a column reference (e.g., "t" from "t.col"). 397 */ 398 private String getTablePrefix(TObjectName colRef) { 399 if (colRef == null) { 400 return null; 401 } 402 403 // Try to get the table/schema part 404 String fullName = colRef.toString(); 405 String columnOnly = colRef.getColumnNameOnly(); 406 407 if (fullName != null && columnOnly != null && fullName.contains(".")) { 408 int lastDot = fullName.lastIndexOf('.'); 409 if (lastDot > 0) { 410 return fullName.substring(0, lastDot); 411 } 412 } 413 414 return null; 415 } 416 417 /** 418 * Check if a column reference matches a star namespace. 419 */ 420 private boolean matchesStarNamespace(TObjectName colRef, String prefix, 421 StarNamespaceInfo starInfo, IScope colScope) { 422 // If column has a prefix, it must match the namespace alias/name 423 if (prefix != null && !prefix.isEmpty()) { 424 String nsName = starInfo.alias; 425 if (nsName == null) { 426 nsName = starInfo.namespace.getDisplayName(); 427 } 428 429 // Check if prefix matches namespace name (case-insensitive) 430 if (!nameMatcher.matches(prefix, nsName)) { 431 // Also check without schema prefix (e.g., "schema.table" vs "table") 432 if (prefix.contains(".")) { 433 String shortPrefix = prefix.substring(prefix.lastIndexOf('.') + 1); 434 if (!nameMatcher.matches(shortPrefix, nsName)) { 435 return false; 436 } 437 } else { 438 return false; 439 } 440 } 441 } 442 443 // Verify scope relationship: column's scope should be able to "see" the star namespace 444 return canAccessNamespace(colScope, starInfo); 445 } 446 447 /** 448 * Check if a scope can access a star namespace. 449 */ 450 private boolean canAccessNamespace(IScope colScope, StarNamespaceInfo starInfo) { 451 // Walk up the scope chain from colScope 452 IScope current = colScope; 453 while (current != null) { 454 // Check if this scope contains the star namespace 455 if (current == starInfo.containingScope) { 456 return true; 457 } 458 459 // Check if this is a SelectScope with FromScope containing the namespace 460 if (current instanceof SelectScope) { 461 SelectScope selectScope = (SelectScope) current; 462 if (selectScope.getFromScope() != null) { 463 for (ScopeChild child : selectScope.getFromScope().getChildren()) { 464 if (child.getNamespace() == starInfo.namespace) { 465 return true; 466 } 467 } 468 } 469 } 470 471 // Check CTE scopes 472 if (current instanceof CTEScope) { 473 CTEScope cteScope = (CTEScope) current; 474 // CTEs are accessible from this scope and below 475 for (ScopeChild child : cteScope.getChildren()) { 476 if (child.getNamespace() == starInfo.namespace) { 477 return true; 478 } 479 } 480 } 481 482 current = current.getParent(); 483 } 484 485 return false; 486 } 487 488 /** 489 * Calculate confidence score for a push-down request. 490 */ 491 private double calculateConfidence(TObjectName colRef, String prefix, StarNamespaceInfo starInfo) { 492 // Higher confidence for qualified references (t.col) 493 if (prefix != null && !prefix.isEmpty()) { 494 return 0.95; 495 } 496 497 // Lower confidence for unqualified references 498 return 0.7; 499 } 500 501 /** 502 * Build evidence string for a push-down request. 503 */ 504 private String buildEvidence(TObjectName colRef, StarNamespaceInfo starInfo) { 505 return String.format("Column '%s' referenced from outer query, targets '%s' with SELECT *", 506 colRef.toString(), starInfo.namespace.getDisplayName()); 507 } 508 509 /** 510 * Process a single push-down request. 511 * 512 * @param request the push-down request 513 * @param result the result to update 514 * @return true if a new column was added, false otherwise 515 */ 516 private boolean processPushDownRequest(PushDownRequest request, PushDownResult result) { 517 if (request == null || request.starNamespace == null) { 518 return false; 519 } 520 521 Set<String> existingColumns = result.getPushedDownColumns(request.starNamespace); 522 523 // Check if this column is already pushed down (case-insensitive) 524 for (String existing : existingColumns) { 525 if (nameMatcher.matches(existing, request.columnName)) { 526 return false; // Already have this column 527 } 528 } 529 530 // Add new pushed-down column 531 result.addPushedDownColumn(request.starNamespace, request.columnName); 532 result.addUpLevelReference(request.starNamespace, request.columnReference); 533 result.addProcessedRequest(request); 534 535 log("Pushed down: " + request.columnName + " -> " + request.starNamespace.getDisplayName()); 536 537 return true; 538 } 539 540 /** 541 * Apply inferred columns to dynamic namespaces. 542 */ 543 private void applyInferredColumns(PushDownResult result) { 544 for (Map.Entry<INamespace, Set<String>> entry : result.pushedDownColumns.entrySet()) { 545 INamespace namespace = entry.getKey(); 546 Set<String> columns = entry.getValue(); 547 548 // If namespace supports dynamic inference, add inferred columns 549 if (namespace.supportsDynamicInference()) { 550 for (String columnName : columns) { 551 namespace.addInferredColumn(columnName, 0.9, "star_push_down"); 552 } 553 log("Applied " + columns.size() + " inferred columns to " + namespace.getDisplayName()); 554 } 555 } 556 } 557 558 /** 559 * Get statistics about push-down resolution. 560 */ 561 public Map<String, Object> getStatistics(PushDownResult result) { 562 Map<String, Object> stats = new LinkedHashMap<>(); 563 stats.put("star_namespaces", result.getStarNamespaceCount()); 564 stats.put("total_pushed_down", result.getTotalPushedDownColumns()); 565 stats.put("iterations", result.getIterations()); 566 stats.put("requests_processed", result.getProcessedRequests().size()); 567 return stats; 568 } 569 570 private void log(String message) { 571 if (debugLogging) { 572 System.out.println("[StarPushDownResolver] " + message); 573 } 574 } 575}