001package gudusoft.gsqlparser.ir.builder;
002
003import gudusoft.gsqlparser.ir.common.SourceAnchor;
004import gudusoft.gsqlparser.ir.logical.LogicalProgram;
005import gudusoft.gsqlparser.ir.logical.RelNode;
006import gudusoft.gsqlparser.ir.logical.RexNode;
007import gudusoft.gsqlparser.ir.logical.rel.Filter;
008import gudusoft.gsqlparser.ir.logical.rel.Join;
009import gudusoft.gsqlparser.ir.logical.rel.TableScan;
010import gudusoft.gsqlparser.ir.logical.rex.RexCall;
011import gudusoft.gsqlparser.ir.logical.rex.RexColumnRef;
012import gudusoft.gsqlparser.ir.logical.rex.RexLiteral;
013import gudusoft.gsqlparser.ir.semantic.SemanticProgram;
014import gudusoft.gsqlparser.ir.semantic.SourceSpan;
015import gudusoft.gsqlparser.ir.semantic.StatementGraph;
016import gudusoft.gsqlparser.ir.semantic.joinanalysis.JoinEntity;
017import gudusoft.gsqlparser.ir.semantic.joinanalysis.JoinGraph;
018import gudusoft.gsqlparser.ir.semantic.joinanalysis.Predicate;
019import gudusoft.gsqlparser.ir.semantic.joinanalysis.PredicateOperand;
020import gudusoft.gsqlparser.ir.semantic.joinanalysis.SemanticJoinType;
021
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025
026/**
027 * Join-analysis slice 175 (S14) — a scoped Logical IR builder for the
028 * SELECT-with-joins subset. It <strong>consumes the semantic
029 * {@link JoinGraph}</strong> (slices 167/168) rather than re-deriving a
030 * relational plan from {@code BoundProgram}, which lacks join order,
031 * predicate trees, and exact spans.
032 *
033 * <p>For each statement with a non-empty join graph it builds a left-deep
034 * {@link TableScan}/{@link Join} tree: the right input of join {@code n}
035 * is the newly added relation; the left input is the {@code Join} produced
036 * by join {@code n-1} (or the first {@code TableScan} for order 0).
037 * Semantic spans map to {@link SourceAnchor}s (line/col preserved; char
038 * offsets unavailable from the semantic layer, set to -1).
039 *
040 * <p>V1 scope: join <em>conditions</em> are left null here — the Rex
041 * predicate trees are attached in slice 176 (S15). A statement containing
042 * an {@link SemanticJoinType#UNSUPPORTED} join is surfaced explicitly by
043 * skipping its plan (never guessed); callers can detect the gap by the
044 * absent plan for that statement index.
045 */
046public final class SelectJoinLogicalIRBuilder {
047
048    /**
049     * Build a {@link LogicalProgram} from a semantic program: one
050     * {@code StatementPlan} per statement whose join graph is non-empty
051     * and fully supported.
052     */
053    public LogicalProgram build(SemanticProgram semantic) {
054        LogicalProgram program = new LogicalProgram();
055        if (semantic == null) return program;
056        List<StatementGraph> stmts = semantic.getStatements();
057        // A degrade placeholder (SemanticIRBuildOptions
058        // .withDegradeUnsupportedNestedBlocks) has an empty JoinGraph, which the
059        // loop below would skip exactly like an ordinary statement that has no
060        // joins — while still emitting plans for every other statement. But
061        // LogicalProgram carries neither diagnostics nor the placeholder marker,
062        // so unlike a SemanticProgram consumer its caller has no way to check
063        // isUnanalyzed() and would read plausible partial output as complete.
064        // Refuse the whole program instead of publishing that.
065        for (int i = 0; i < stmts.size(); i++) {
066            if (stmts.get(i).isUnanalyzed()) {
067                throw new IllegalArgumentException(
068                        "cannot build a logical program from a semantic program "
069                                + "containing an unanalyzed block: '"
070                                + stmts.get(i).getName() + "' ("
071                                + stmts.get(i).getUnanalyzedReason().getMessage()
072                                + "). Rebuild without "
073                                + "SemanticIRBuildOptions.withDegradeUnsupportedNestedBlocks(true).");
074            }
075        }
076        for (int i = 0; i < stmts.size(); i++) {
077            StatementGraph sg = stmts.get(i);
078            JoinGraph jg = sg.getJoinGraph();
079            if (jg.isEmpty()) continue;
080            if (containsUnsupported(jg)) continue;   // explicit gap; no guess
081            RelNode root = buildRelTree(jg);
082            if (root == null) continue;
083            // Slice 176 (S15): wrap the join tree in a Filter carrying the
084            // WHERE predicates' Rex tree (when present).
085            List<Predicate> filters = sg.getJoinAnalysisFacts().getFilterPredicates();
086            if (!filters.isEmpty()) {
087                RexNode cond = conjunction(filters);
088                root = new Filter(root, cond, cond != null ? cond.getAnchor() : root.getAnchor());
089            }
090            SourceAnchor anchor = root.getAnchor();
091            program.addPlan(new LogicalProgram.StatementPlan(
092                    /*owningRoutineId=*/ null, i, root, anchor));
093        }
094        return program;
095    }
096
097    /** Public for slice 176 reuse: build the left-deep rel tree for one graph. */
098    public RelNode buildRelTree(JoinGraph jg) {
099        RelNode current = null;
100        for (JoinEntity e : jg.getJoins()) {
101            SourceAnchor anchor = anchorOf(e.getSourceSpan());
102            if (current == null) {
103                // order 0 — left input is the first relation as a TableScan.
104                current = tableScanOf(e.getLeftEndpoint().getQualifiedName(),
105                        e.getLeftEndpoint().getAlias());
106            }
107            RelNode right = tableScanOf(e.getRightEndpoint().getQualifiedName(),
108                    e.getRightEndpoint().getAlias());
109            // Slice 176 (S15): the ON condition Rex tree (null when the join
110            // carries no predicates, e.g. CROSS / IMPLICIT_CROSS).
111            RexNode condition = e.getConditions().isEmpty()
112                    ? null : conjunction(e.getConditions());
113            current = new Join(current, right, mapJoinType(e.getJoinType()),
114                    condition, anchor);
115        }
116        return current;
117    }
118
119    // -----------------------------------------------------------------
120    // Slice 176 (S15) — semantic Predicate/Operand -> Rex mapping. Complex
121    // shapes degrade to a RexCall placeholder, never dropped.
122    // -----------------------------------------------------------------
123
124    /** AND-combine a predicate list into one RexNode (single => itself). */
125    private static RexNode conjunction(List<Predicate> preds) {
126        List<RexNode> rex = new ArrayList<RexNode>();
127        for (Predicate p : preds) {
128            RexNode r = toRex(p);
129            if (r != null) rex.add(r);
130        }
131        if (rex.isEmpty()) return null;
132        if (rex.size() == 1) return rex.get(0);
133        return new RexCall("AND", rex, null);
134    }
135
136    private static RexNode toRex(Predicate p) {
137        SourceAnchor anchor = anchorOf(p.getSourceSpan());
138        List<RexNode> operands = new ArrayList<RexNode>();
139        operands.add(toRex(p.getLeftOperand()));
140        if (p.getRightOperand() != null) {
141            operands.add(toRex(p.getRightOperand()));
142        }
143        String op = p.getOperator() != null ? p.getOperator() : p.getKind().name();
144        return new RexCall(op, operands, anchor);
145    }
146
147    private static RexNode toRex(PredicateOperand op) {
148        SourceAnchor anchor = anchorOf(op.getSourceSpan());
149        switch (op.getKind()) {
150            case COLUMN: {
151                String table = op.getColumn().getResolution() != null
152                        && op.getColumn().getResolution().getResolvedTableQualifiedName() != null
153                        ? op.getColumn().getResolution().getResolvedTableQualifiedName()
154                        : op.getColumn().getRelationAlias();
155                return new RexColumnRef(table, op.getColumn().getColumnName(), anchor);
156            }
157            case LITERAL:
158                return new RexLiteral(op.getText(), RexLiteral.LiteralType.STRING, anchor);
159            case CALL:
160                // Sub-structure not preserved at the semantic layer; a
161                // placeholder RexCall keeps the shape without guessing args.
162                return new RexCall("CALL", Collections.<RexNode>emptyList(), anchor);
163            default:
164                // COMPLEX — preserved, never dropped.
165                return new RexCall("COMPLEX", Collections.<RexNode>emptyList(), anchor);
166        }
167    }
168
169    private static TableScan tableScanOf(String qualifiedName, String alias) {
170        String name = (qualifiedName != null && !qualifiedName.isEmpty())
171                ? qualifiedName : alias;
172        return new TableScan(name, Collections.<String>emptyList(), null);
173    }
174
175    private static boolean containsUnsupported(JoinGraph jg) {
176        for (JoinEntity e : jg.getJoins()) {
177            if (e.getJoinType() == SemanticJoinType.UNSUPPORTED) return true;
178        }
179        return false;
180    }
181
182    private static Join.JoinType mapJoinType(SemanticJoinType t) {
183        switch (t) {
184            case INNER: return Join.JoinType.INNER;
185            case LEFT: return Join.JoinType.LEFT;
186            case RIGHT: return Join.JoinType.RIGHT;
187            case FULL: return Join.JoinType.FULL;
188            case NATURAL: return Join.JoinType.NATURAL;
189            case CROSS:
190            case IMPLICIT_CROSS:
191                return Join.JoinType.CROSS;
192            default:
193                // Unreachable: UNSUPPORTED statements are filtered upstream.
194                return Join.JoinType.INNER;
195        }
196    }
197
198    /**
199     * Map a semantic {@link SourceSpan} (1-based line/col, no offsets) to a
200     * {@link SourceAnchor}. Offsets are unavailable at the semantic layer,
201     * so they are set to -1; line/col are preserved.
202     */
203    static SourceAnchor anchorOf(SourceSpan span) {
204        if (span == null) return null;
205        return new SourceAnchor("",
206                /*startOffset=*/ -1, /*endOffset=*/ -1,
207                (int) span.getStartLine(), (int) span.getStartColumn(),
208                (int) span.getEndLine(), (int) span.getEndColumn(),
209                /*statementKey=*/ null, /*snippet=*/ null);
210    }
211}