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