001package gudusoft.gsqlparser.ir.semantic.diff; 002 003import gudusoft.gsqlparser.ir.semantic.ColumnRef; 004import gudusoft.gsqlparser.ir.semantic.LineageEdge; 005import gudusoft.gsqlparser.ir.semantic.LineageRef; 006import gudusoft.gsqlparser.ir.semantic.OutputColumn; 007import gudusoft.gsqlparser.ir.semantic.RelationKind; 008import gudusoft.gsqlparser.ir.semantic.RelationSource; 009import gudusoft.gsqlparser.ir.semantic.SemanticProgram; 010import gudusoft.gsqlparser.ir.semantic.StatementGraph; 011import gudusoft.gsqlparser.ir.semantic.builder.SemanticIRBuilder; 012 013import java.util.ArrayDeque; 014import java.util.ArrayList; 015import java.util.Deque; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.LinkedHashMap; 019import java.util.LinkedHashSet; 020import java.util.List; 021import java.util.Locale; 022import java.util.Map; 023import java.util.Set; 024 025/** 026 * Project a {@link SemanticProgram} into a {@link CanonicalLineageModel}. 027 * 028 * <p>SELECT lineage: for each outer-statement output, BFS the 029 * program-level lineage edges (target → sources) until we hit 030 * {@code TABLE_COLUMN} terminals. Each terminal becomes one canonical 031 * SELECT edge. 032 * 033 * <p>Row influence: for each statement reachable from the outer (via 034 * CTE/SUBQUERY relations), every column ref in {@code filterColumnRefs} 035 * and {@code joinColumnRefs} is resolved to a base table by walking the 036 * lineage chain that starts at the relation it references. 037 */ 038public final class SemanticIRProjector { 039 040 private SemanticIRProjector() {} 041 042 public static ProjectorResult project(SemanticProgram program) { 043 if (program == null) { 044 throw new IllegalArgumentException("program must not be null"); 045 } 046 if (program.getStatements().isEmpty()) { 047 return ProjectorResult.unsupported( 048 ProjectorResult.UnsupportedReason.NO_RELATIONSHIPS, 049 "program has no statements"); 050 } 051 // A degrade placeholder means some block was never analyzed. Its empty 052 // OutputColumns would contribute zero edges below and the resulting 053 // model would look complete while silently missing them, so refuse 054 // rather than project an incomplete canonical model. 055 for (StatementGraph s : program.getStatements()) { 056 if (s.isUnanalyzed()) { 057 return ProjectorResult.unsupported( 058 ProjectorResult.UnsupportedReason.UNANALYZED_BLOCK, 059 "block '" + s.getName() + "' was not analyzed: " 060 + s.getUnanalyzedReason().getMessage()); 061 } 062 } 063 // Outer statement: last unnamed statement. Named statements are CTE 064 // bodies, FROM-subquery bodies, and (slice 11) scalar-subquery 065 // bodies (synthetic angle-bracketed names — see 066 // SemanticIRBuilder.SCALAR_BODY_PREFIX). The outer is the one that 067 // reads from them and is always unnamed. 068 int outerIndex = -1; 069 for (int i = program.getStatements().size() - 1; i >= 0; i--) { 070 if (program.getStatements().get(i).getName() == null) { 071 outerIndex = i; 072 break; 073 } 074 } 075 if (outerIndex < 0) { 076 return ProjectorResult.unsupported( 077 ProjectorResult.UnsupportedReason.NO_RELATIONSHIPS, 078 "no outer (unnamed) statement found"); 079 } 080 081 // Index lineage edges by their from-key so the BFS is O(N) per query. 082 Map<String, List<LineageEdge>> outgoingByFrom = indexLineage(program.getLineage()); 083 084 // Kind-aware lookups for row-influence resolution. A `CTE`-bound 085 // relation refers to a body whose name is the CTE name; a 086 // `SUBQUERY`-bound relation refers to a body whose name is the 087 // FROM-clause alias. Keeping the maps separate avoids collision when 088 // a CTE name and a subquery alias happen to match (e.g. 089 // {@code WITH x AS (...) SELECT FROM (SELECT ...) x}). 090 BodyIndexes bodies = new BodyIndexes(program); 091 092 StatementGraph outer = program.getStatements().get(outerIndex); 093 094 Set<String> outputNames = new LinkedHashSet<>(); 095 Map<String, Boolean> aggregateByOutput = new LinkedHashMap<>(); 096 Set<CanonicalLineageEdge> edges = new LinkedHashSet<>(); 097 098 // 1) SELECT edges per outer output. 099 for (OutputColumn out : outer.getOutputColumns()) { 100 String outName = out.getName().toLowerCase(Locale.ROOT); 101 outputNames.add(outName); 102 // Last write wins if the SQL has duplicate output names; the 103 // builder doesn't reject them today and the canonical model 104 // can't either, so we treat it as the OutputColumn list does. 105 aggregateByOutput.put(outName, out.isAggregate()); 106 107 String startKey = stmtOutputKey(outerIndex, out.getName()); 108 for (TableColumn tc : bfsToBaseColumns(startKey, outgoingByFrom)) { 109 edges.add(new CanonicalLineageEdge( 110 EdgeRole.SELECT, outName, tc.table, tc.column)); 111 } 112 } 113 114 // 2) Reachable statements from the outer. Single fixpoint BFS that 115 // combines (a) CTE / SUBQUERY relation edges and (b) program-level 116 // STATEMENT_OUTPUT → STATEMENT_OUTPUT lineage edges. The lineage 117 // walk picks up scalar-subquery bodies (slice 11) — they are 118 // referenced only via lineage, never via relations — and any 119 // statement those scalar bodies reach via their own relations 120 // gets visited in the same pass. 121 Set<Integer> reachable = computeReachable(program, outerIndex, outgoingByFrom); 122 123 // 3) Row-influence edges from every reachable statement's filter/join refs. 124 for (int idx : reachable) { 125 StatementGraph s = program.getStatements().get(idx); 126 for (ColumnRef ref : s.getFilterColumnRefs()) { 127 addRowInfluenceEdges(EdgeRole.FILTER, idx, s, ref, 128 outgoingByFrom, bodies, edges); 129 } 130 for (ColumnRef ref : s.getJoinColumnRefs()) { 131 addRowInfluenceEdges(EdgeRole.JOIN, idx, s, ref, 132 outgoingByFrom, bodies, edges); 133 } 134 } 135 136 // 4) Slice 24: predicate-body JOIN edges. Emit one JOIN canonical 137 // edge per base-column terminal of each predicate body's 138 // OutputColumn(s). The pass is intentionally OUTSIDE the 139 // `reachable` BFS: 140 // 141 // (a) Predicate bodies are unreachable from outer by slice-23 142 // design — outer holds no relation pointing at them and no 143 // STATEMENT_OUTPUT lineage edge into them. Adding them to 144 // `reachable` would let `addRowInfluenceEdges` walk their 145 // filter/join refs, which would manufacture FILTER / JOIN 146 // edges from inner WHERE / inner JOIN refs — dlineage's BFS 147 // does not chain into RS-2's RelationRows from RS-1 148 // (slice-24 probe finding 3), so adding those edges would 149 // break the slice-7 zero-divergence guarantee. 150 // (b) Iterating ONLY OutputColumns (not filter/join refs) means 151 // inner WHERE / inner JOIN refs do NOT contribute to outer's 152 // canonical model. This matches dlineage's behaviour: the 153 // fdr clause="on" source `clauseType="selectList" 154 // parent_id=<inner-RS>` resolves to the inner-projected 155 // column's base-column terminal via fdd chains, but the 156 // non-system source attribute prevents BFS from chaining 157 // into the inner RS's RelationRows. 158 // (c) Slice-23 constant-only bodies have OutputColumns with 159 // empty `sources` → bfsToBaseColumns finds no terminals → 160 // zero JOIN edges added (preserves slice-23 zero-divergence 161 // on corpus 21). Slice-24 column-ref bodies have one 162 // ColumnRef source whose lineage chain leads to the inner 163 // base column → exactly one JOIN edge per terminal. 164 // (d) De-duplication via the `Set<CanonicalLineageEdge>` 165 // semantics: a `(JOIN, null, departments, id)` edge from 166 // outer's `d.id = e.id` and from a column-bearing EXISTS 167 // `(SELECT d.id FROM departments d)` collapse to one. 168 for (int idx = 0; idx < program.getStatements().size(); idx++) { 169 StatementGraph s = program.getStatements().get(idx); 170 if (s.getName() == null) continue; 171 if (!SemanticIRBuilder.isPredicateSubquerySyntheticName(s.getName())) continue; 172 for (OutputColumn out : s.getOutputColumns()) { 173 String startKey = stmtOutputKey(idx, out.getName()); 174 for (TableColumn tc : bfsToBaseColumns(startKey, outgoingByFrom)) { 175 edges.add(new CanonicalLineageEdge( 176 EdgeRole.JOIN, null, tc.table, tc.column)); 177 } 178 } 179 } 180 181 return ProjectorResult.ok( 182 new CanonicalLineageModel(edges, outputNames, aggregateByOutput)); 183 } 184 185 /** 186 * Resolve a {@code ColumnRef} that appears in {@code stmt}'s filter/join 187 * clause down to base-table columns and emit one row-influence edge per 188 * terminal. 189 */ 190 private static void addRowInfluenceEdges(EdgeRole role, 191 int consumerIdx, 192 StatementGraph stmt, 193 ColumnRef ref, 194 Map<String, List<LineageEdge>> outgoingByFrom, 195 BodyIndexes bodies, 196 Set<CanonicalLineageEdge> sink) { 197 RelationSource matched = null; 198 for (RelationSource r : stmt.getRelations()) { 199 if (r.getAlias().equals(ref.getRelationAlias())) { 200 matched = r; 201 break; 202 } 203 } 204 if (matched == null) { 205 // Builder guarantees an alias is in scope, but be defensive — a 206 // dropped row-influence edge would silently mask divergence. 207 return; 208 } 209 // Slice 15: resolved-kind dispatch. OUTER_REFERENCE bindings 210 // delegate to the underlying outerKind. TABLE emits a 211 // base-column edge; CTE / SUBQUERY BFS through the outer body 212 // to reach base columns (mirroring dlineage's behaviour). 213 RelationKind kind = matched.getBinding().getKind(); 214 RelationKind resolvedKind = (kind == RelationKind.OUTER_REFERENCE) 215 ? matched.getBinding().getOuterKind() 216 : kind; 217 if (resolvedKind == RelationKind.TABLE 218 || resolvedKind == RelationKind.FUNCTION) { 219 // FUNCTION (table-valued function) is an opaque terminal source, 220 // projected like a base table column. 221 sink.add(new CanonicalLineageEdge(role, null, 222 matched.getBinding().getQualifiedName().toLowerCase(Locale.ROOT), 223 ref.getColumnName().toLowerCase(Locale.ROOT))); 224 return; 225 } 226 if (resolvedKind == RelationKind.CTE || resolvedKind == RelationKind.SUBQUERY) { 227 Integer downstream = bodies.lookup(consumerIdx, resolvedKind, matched); 228 if (downstream == null) return; 229 String startKey = stmtOutputKey(downstream, ref.getColumnName()); 230 for (TableColumn tc : bfsToBaseColumns(startKey, outgoingByFrom)) { 231 sink.add(new CanonicalLineageEdge(role, null, tc.table, tc.column)); 232 } 233 return; 234 } 235 // UNION / UNKNOWN / null outerKind — should not appear in 236 // row-influence today. A future slice introducing them must 237 // extend this dispatch. 238 throw new IllegalStateException( 239 "unhandled RelationKind in addRowInfluenceEdges: " + kind 240 + (kind == RelationKind.OUTER_REFERENCE 241 ? " (outerKind=" + matched.getBinding().getOuterKind() + ")" 242 : "")); 243 } 244 245 /** 246 * Single-fixpoint BFS over both (a) CTE/SUBQUERY relation edges 247 * and (b) program-level STATEMENT_OUTPUT → STATEMENT_OUTPUT 248 * lineage edges (slice 11). Each pop visits both edge kinds in 249 * the same pass, so a scalar-subquery body reached via lineage 250 * has its own CTE/SUBQUERY relations traversed before the BFS 251 * terminates. The lineage edges are pre-indexed by the caller 252 * to avoid rebuilding the index inside this method. 253 */ 254 private static Set<Integer> computeReachable(SemanticProgram program, 255 int outerIndex, 256 Map<String, List<LineageEdge>> outgoingByFrom) { 257 BodyIndexes bodies = new BodyIndexes(program); 258 Set<Integer> reachable = new LinkedHashSet<>(); 259 Deque<Integer> q = new ArrayDeque<>(); 260 q.add(outerIndex); 261 reachable.add(outerIndex); 262 while (!q.isEmpty()) { 263 int idx = q.removeFirst(); 264 StatementGraph s = program.getStatements().get(idx); 265 // (a) CTE/SUBQUERY relations. 266 for (RelationSource r : s.getRelations()) { 267 RelationKind k = r.getBinding().getKind(); 268 if (k != RelationKind.CTE && k != RelationKind.SUBQUERY) continue; 269 Integer downstream = bodies.lookup(idx, k, r); 270 if (downstream != null && reachable.add(downstream)) { 271 q.add(downstream); 272 } 273 } 274 // (b) STATEMENT_OUTPUT → STATEMENT_OUTPUT lineage edges 275 // originating from this statement's outputs (slice 11 276 // scalar-subquery body reachability). 277 for (OutputColumn out : s.getOutputColumns()) { 278 String key = stmtOutputKey(idx, out.getName()); 279 List<LineageEdge> outgoing = outgoingByFrom.get(key); 280 if (outgoing == null) continue; 281 for (LineageEdge e : outgoing) { 282 LineageRef to = e.getTo(); 283 if (to.getKind() != LineageRef.Kind.STATEMENT_OUTPUT) continue; 284 int downstream = to.getStatementIndex(); 285 if (reachable.add(downstream)) { 286 q.add(downstream); 287 } 288 } 289 } 290 } 291 return reachable; 292 } 293 294 /** Pure helper: BFS from a statement-output key over lineage edges. */ 295 private static List<TableColumn> bfsToBaseColumns(String startKey, 296 Map<String, List<LineageEdge>> outgoingByFrom) { 297 List<TableColumn> out = new ArrayList<>(); 298 Set<String> visited = new HashSet<>(); 299 Deque<String> q = new ArrayDeque<>(); 300 q.add(startKey); 301 visited.add(startKey); 302 while (!q.isEmpty()) { 303 String cur = q.removeFirst(); 304 List<LineageEdge> outgoing = outgoingByFrom.get(cur); 305 if (outgoing == null) continue; 306 for (LineageEdge e : outgoing) { 307 LineageRef to = e.getTo(); 308 if (to.getKind() == LineageRef.Kind.TABLE_COLUMN) { 309 out.add(new TableColumn( 310 to.getQualifiedName().toLowerCase(Locale.ROOT), 311 to.getColumnName().toLowerCase(Locale.ROOT))); 312 } else { 313 String nextKey = stmtOutputKey(to.getStatementIndex(), to.getOutputName()); 314 if (visited.add(nextKey)) { 315 q.add(nextKey); 316 } 317 } 318 } 319 } 320 return out; 321 } 322 323 private static Map<String, List<LineageEdge>> indexLineage(List<LineageEdge> all) { 324 Map<String, List<LineageEdge>> out = new HashMap<>(); 325 for (LineageEdge e : all) { 326 LineageRef from = e.getFrom(); 327 if (from.getKind() != LineageRef.Kind.STATEMENT_OUTPUT) continue; 328 String key = stmtOutputKey(from.getStatementIndex(), from.getOutputName()); 329 out.computeIfAbsent(key, k -> new ArrayList<>()).add(e); 330 } 331 return out; 332 } 333 334 private static String stmtOutputKey(int idx, String name) { 335 return idx + "/" + name; 336 } 337 338 private static final class TableColumn { 339 final String table; 340 final String column; 341 TableColumn(String table, String column) { 342 this.table = table; 343 this.column = column; 344 } 345 } 346 347 /** 348 * Kind-aware lookup from a CTE name or subquery alias to the index of 349 * its body statement, scoped per consumer statement. Maps are keyed by 350 * {@code consumerStmtIdx + "|" + name_lower}. 351 * 352 * <p>Slice 18 makes the lookup consumer-scoped (was: global alias-keyed) 353 * to handle the new shape "two CTE bodies each containing a FROM-subquery 354 * aliased {@code s}". With consumer-scoping each consumer sees its own 355 * body, instead of the last-write-wins value of a global map. 356 * 357 * <p>Construction (two passes): 358 * <ol> 359 * <li>Walk consumers in statement order over their direct CTE / SUBQUERY 360 * relations. {@code SUBQUERY} consumers exclusively claim a body 361 * (FROM-subquery bodies are extracted by exactly one consumer); 362 * {@code CTE} consumers do <i>not</i> claim — CTEs are reusable, so 363 * multiple consumers can record their own per-consumer entry pointing 364 * at the same CTE body. Subquery-claimed bodies are skipped by CTE 365 * pickers so a CTE name colliding with a FROM alias does not 366 * mis-bind.</li> 367 * <li>Walk STATEMENT_OUTPUT → STATEMENT_OUTPUT lineage edges to derive 368 * each child statement's immediate parent. For each child with an 369 * {@code OUTER_REFERENCE-of-CTE} or {@code OUTER_REFERENCE-of-SUBQUERY} 370 * relation, walk the immediate-parent chain (innermost → outermost) 371 * until an ancestor's Pass-1 entry is found, and copy that entry 372 * under {@code (childIdx, name)}. This is what makes the consumer- 373 * keyed lookup self-sufficient for slice-14/15 OUTER_REFERENCE row- 374 * influence — without it, a scalar body's lookup at its own index 375 * would return {@code null} because Pass 1 only stores entries for 376 * direct CTE/SUBQUERY relations (slice-15 invariant: OUTER_REFERENCE 377 * does not claim). Slice 20 generalises one-step propagation to a 378 * transitive chain walk so doubly-nested (and deeper) correlated 379 * scalars resolve their grandparent body via the same lookup.</li> 380 * </ol> 381 * 382 * <p>Collision behaviour: when both a CTE named {@code x} and a subquery 383 * aliased {@code x} exist, the SUBQUERY consumer's exclusive claim takes 384 * the FROM-subquery body (latest unclaimed prior); the CTE consumer's 385 * non-claiming pick takes the CTE body (earliest non-subquery-claimed 386 * prior). Pinned by {@code subqueryAliasResolvesPastUnusedCteWithSameName} 387 * and {@code outerReferenceRelationDoesNotClaimBodies} in 388 * {@link gudusoft.gsqlparser.ir.semantic.slice7.SemanticIRProjectorBodyIndexesTest}. 389 * 390 * <p>Unused CTEs (declared but never referenced) have no entry in any 391 * map. Reachability BFS does not visit them, so this is correct — there 392 * is no caller that could ask for them. Slice 18 removed the legacy 393 * default-population pass for the same reason. 394 */ 395 private static final class BodyIndexes { 396 // Per-consumer claim. Key: "<consumerStmtIdx>|<name_lower>". 397 private final Map<String, Integer> cteByConsumerAndName = new HashMap<>(); 398 private final Map<String, Integer> subqueryByConsumerAndAlias = new HashMap<>(); 399 400 BodyIndexes(SemanticProgram program) { 401 // Build the candidate-bodies-by-name index. Synthetic-named 402 // bodies (slice-11 scalar bodies, slice-12 set-op branches) are 403 // skipped — they are not CTE/FROM-subquery candidates and are 404 // reached only via lineage edges in computeReachable. 405 Map<String, java.util.List<Integer>> bodyIndicesByName = new HashMap<>(); 406 for (int i = 0; i < program.getStatements().size(); i++) { 407 StatementGraph s = program.getStatements().get(i); 408 if (s.getName() == null) continue; 409 if (SemanticIRBuilder.isScalarSyntheticName(s.getName())) continue; 410 if (SemanticIRBuilder.isSetOpBranchSyntheticName(s.getName())) continue; 411 if (SemanticIRBuilder.isPredicateSubquerySyntheticName(s.getName())) continue; 412 bodyIndicesByName 413 .computeIfAbsent(s.getName().toLowerCase(Locale.ROOT), 414 k -> new java.util.ArrayList<>()) 415 .add(i); 416 } 417 418 // Pass 1: walk consumer relations in statement order. Body 419 // claiming is kind-aware: 420 // - CTE consumer: pick the EARLIEST candidate matching the 421 // name with index < ci that is not already subquery-claimed. 422 // Do NOT claim — CTE bodies are reusable across consumers. 423 // - SUBQUERY consumer: pick the LATEST unclaimed candidate 424 // matching the alias with index < ci. Exclusive claim. 425 // 426 // Slice-15 invariant: OUTER_REFERENCE relations do NOT claim 427 // bodies in this pass — the filter at line ~XXX excludes them. 428 // Pass 2 below derives their per-consumer entries from the 429 // parent's entries via lineage. 430 Set<Integer> subqueryClaimed = new HashSet<>(); 431 for (int ci = 0; ci < program.getStatements().size(); ci++) { 432 StatementGraph cs = program.getStatements().get(ci); 433 for (RelationSource r : cs.getRelations()) { 434 RelationKind k = r.getBinding().getKind(); 435 if (k != RelationKind.CTE && k != RelationKind.SUBQUERY) continue; 436 String name = (k == RelationKind.SUBQUERY) 437 ? r.getAlias().toLowerCase(Locale.ROOT) 438 : r.getBinding().getQualifiedName().toLowerCase(Locale.ROOT); 439 java.util.List<Integer> candidates = bodyIndicesByName.get(name); 440 if (candidates == null) continue; 441 Integer chosen = null; 442 if (k == RelationKind.CTE) { 443 for (Integer idx : candidates) { 444 if (idx >= ci) break; 445 if (subqueryClaimed.contains(idx)) continue; 446 chosen = idx; 447 break; 448 } 449 } else { // SUBQUERY 450 for (int j = candidates.size() - 1; j >= 0; j--) { 451 int idx = candidates.get(j); 452 if (idx >= ci) continue; 453 if (subqueryClaimed.contains(idx)) continue; 454 chosen = idx; 455 break; 456 } 457 if (chosen != null) subqueryClaimed.add(chosen); 458 } 459 if (chosen == null) continue; 460 String key = ci + "|" + name; 461 if (k == RelationKind.CTE) cteByConsumerAndName.put(key, chosen); 462 else subqueryByConsumerAndAlias.put(key, chosen); 463 } 464 } 465 466 // Pass 2: derive each child statement's immediate parent from 467 // STATEMENT_OUTPUT → STATEMENT_OUTPUT lineage. For each child 468 // with an OUTER_REFERENCE-of-CTE or OUTER_REFERENCE-of-SUBQUERY 469 // relation, walk the IMMEDIATE-PARENT CHAIN (innermost → 470 // outermost) until an ancestor's per-consumer Pass-1 entry is 471 // found, and copy that entry under the child's index. This 472 // makes the consumer-keyed lookup self-sufficient for 473 // OUTER_REFERENCE row-influence at any nesting depth (slice 18 474 // codex round-2 MUST 2; slice 20 generalises one-step → chain). 475 // 476 // Slice-14/15 carry only the immediate-parent scope and one 477 // STATEMENT_OUTPUT edge per child output is typical, so 478 // first-write-wins is correct. Slice-20 chain walks support 479 // doubly-nested (and deeper) correlated scalars where the 480 // inner-inner's OUTER_REFERENCE is anchored at the 481 // grandparent (or further-ancestor) statement. 482 // 483 // Cycle guard via `visited`: STATEMENT_OUTPUT → STATEMENT_OUTPUT 484 // is a DAG by construction (a child's output never flows back 485 // to an ancestor's output), but the guard makes the 486 // non-termination invariant explicit. 487 Map<Integer, Integer> immediateParent = new HashMap<>(); 488 for (LineageEdge e : program.getLineage()) { 489 if (e.getFrom().getKind() != LineageRef.Kind.STATEMENT_OUTPUT) continue; 490 if (e.getTo().getKind() != LineageRef.Kind.STATEMENT_OUTPUT) continue; 491 // Slice 24: predicate-body edges do NOT anchor immediate- 492 // parent chains. The predicate body is unreachable from 493 // outer (slice-23 invariant); recording it as the 494 // "immediate parent" of a CTE / SUBQUERY body would 495 // mis-route OUTER_REFERENCE chain walks below — a slice-15 496 // / slice-20 child looking for the CTE owner would land 497 // at the predicate body's index, find no Pass-1 entry, 498 // walk to the predicate's parent (none recorded), and 499 // give up. Slice-23 constant predicate bodies emitted no 500 // STATEMENT_OUTPUT → STATEMENT_OUTPUT edges (their 501 // synthetic OutputColumn had empty sources), so the side 502 // effect surfaces only when slice-24 column-bearing 503 // predicate bodies have CTE-bound inner relations. 504 int fromIdx = e.getFrom().getStatementIndex(); 505 StatementGraph fromStmt = program.getStatements().get(fromIdx); 506 if (fromStmt.getName() != null 507 && SemanticIRBuilder.isPredicateSubquerySyntheticName(fromStmt.getName())) { 508 continue; 509 } 510 immediateParent.putIfAbsent( 511 e.getTo().getStatementIndex(), 512 e.getFrom().getStatementIndex()); 513 } 514 for (Map.Entry<Integer, Integer> entry : immediateParent.entrySet()) { 515 int childIdx = entry.getKey(); 516 StatementGraph cs = program.getStatements().get(childIdx); 517 for (RelationSource r : cs.getRelations()) { 518 if (r.getBinding().getKind() != RelationKind.OUTER_REFERENCE) continue; 519 RelationKind ok = r.getBinding().getOuterKind(); 520 if (ok != RelationKind.CTE && ok != RelationKind.SUBQUERY) continue; 521 String name = (ok == RelationKind.SUBQUERY) 522 ? r.getAlias().toLowerCase(Locale.ROOT) 523 : r.getBinding().getQualifiedName().toLowerCase(Locale.ROOT); 524 Map<String, Integer> sourceMap = (ok == RelationKind.CTE) 525 ? cteByConsumerAndName : subqueryByConsumerAndAlias; 526 Integer body = null; 527 Integer cur = entry.getValue(); 528 Set<Integer> visited = new HashSet<>(); 529 visited.add(childIdx); // never claim self 530 while (cur != null && visited.add(cur)) { 531 body = sourceMap.get(cur + "|" + name); 532 if (body != null) break; 533 cur = immediateParent.get(cur); 534 } 535 if (body == null) continue; 536 String childKey = childIdx + "|" + name; 537 if (ok == RelationKind.CTE) 538 cteByConsumerAndName.putIfAbsent(childKey, body); 539 else 540 subqueryByConsumerAndAlias.putIfAbsent(childKey, body); 541 } 542 } 543 } 544 545 Integer lookup(int consumerIdx, RelationKind kind, RelationSource consumer) { 546 String name = (kind == RelationKind.CTE) 547 ? consumer.getBinding().getQualifiedName().toLowerCase(Locale.ROOT) 548 : consumer.getAlias().toLowerCase(Locale.ROOT); 549 String key = consumerIdx + "|" + name; 550 if (kind == RelationKind.CTE) return cteByConsumerAndName.get(key); 551 if (kind == RelationKind.SUBQUERY) return subqueryByConsumerAndAlias.get(key); 552 return null; 553 } 554 } 555}