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        for (int i = 0; i < stmts.size(); i++) {
058            StatementGraph sg = stmts.get(i);
059            JoinGraph jg = sg.getJoinGraph();
060            if (jg.isEmpty()) continue;
061            if (containsUnsupported(jg)) continue;   // explicit gap; no guess
062            RelNode root = buildRelTree(jg);
063            if (root == null) continue;
064            // Slice 176 (S15): wrap the join tree in a Filter carrying the
065            // WHERE predicates' Rex tree (when present).
066            List<Predicate> filters = sg.getJoinAnalysisFacts().getFilterPredicates();
067            if (!filters.isEmpty()) {
068                RexNode cond = conjunction(filters);
069                root = new Filter(root, cond, cond != null ? cond.getAnchor() : root.getAnchor());
070            }
071            SourceAnchor anchor = root.getAnchor();
072            program.addPlan(new LogicalProgram.StatementPlan(
073                    /*owningRoutineId=*/ null, i, root, anchor));
074        }
075        return program;
076    }
077
078    /** Public for slice 176 reuse: build the left-deep rel tree for one graph. */
079    public RelNode buildRelTree(JoinGraph jg) {
080        RelNode current = null;
081        for (JoinEntity e : jg.getJoins()) {
082            SourceAnchor anchor = anchorOf(e.getSourceSpan());
083            if (current == null) {
084                // order 0 — left input is the first relation as a TableScan.
085                current = tableScanOf(e.getLeftEndpoint().getQualifiedName(),
086                        e.getLeftEndpoint().getAlias());
087            }
088            RelNode right = tableScanOf(e.getRightEndpoint().getQualifiedName(),
089                    e.getRightEndpoint().getAlias());
090            // Slice 176 (S15): the ON condition Rex tree (null when the join
091            // carries no predicates, e.g. CROSS / IMPLICIT_CROSS).
092            RexNode condition = e.getConditions().isEmpty()
093                    ? null : conjunction(e.getConditions());
094            current = new Join(current, right, mapJoinType(e.getJoinType()),
095                    condition, anchor);
096        }
097        return current;
098    }
099
100    // -----------------------------------------------------------------
101    // Slice 176 (S15) — semantic Predicate/Operand -> Rex mapping. Complex
102    // shapes degrade to a RexCall placeholder, never dropped.
103    // -----------------------------------------------------------------
104
105    /** AND-combine a predicate list into one RexNode (single => itself). */
106    private static RexNode conjunction(List<Predicate> preds) {
107        List<RexNode> rex = new ArrayList<RexNode>();
108        for (Predicate p : preds) {
109            RexNode r = toRex(p);
110            if (r != null) rex.add(r);
111        }
112        if (rex.isEmpty()) return null;
113        if (rex.size() == 1) return rex.get(0);
114        return new RexCall("AND", rex, null);
115    }
116
117    private static RexNode toRex(Predicate p) {
118        SourceAnchor anchor = anchorOf(p.getSourceSpan());
119        List<RexNode> operands = new ArrayList<RexNode>();
120        operands.add(toRex(p.getLeftOperand()));
121        if (p.getRightOperand() != null) {
122            operands.add(toRex(p.getRightOperand()));
123        }
124        String op = p.getOperator() != null ? p.getOperator() : p.getKind().name();
125        return new RexCall(op, operands, anchor);
126    }
127
128    private static RexNode toRex(PredicateOperand op) {
129        SourceAnchor anchor = anchorOf(op.getSourceSpan());
130        switch (op.getKind()) {
131            case COLUMN: {
132                String table = op.getColumn().getResolution() != null
133                        && op.getColumn().getResolution().getResolvedTableQualifiedName() != null
134                        ? op.getColumn().getResolution().getResolvedTableQualifiedName()
135                        : op.getColumn().getRelationAlias();
136                return new RexColumnRef(table, op.getColumn().getColumnName(), anchor);
137            }
138            case LITERAL:
139                return new RexLiteral(op.getText(), RexLiteral.LiteralType.STRING, anchor);
140            case CALL:
141                // Sub-structure not preserved at the semantic layer; a
142                // placeholder RexCall keeps the shape without guessing args.
143                return new RexCall("CALL", Collections.<RexNode>emptyList(), anchor);
144            default:
145                // COMPLEX — preserved, never dropped.
146                return new RexCall("COMPLEX", Collections.<RexNode>emptyList(), anchor);
147        }
148    }
149
150    private static TableScan tableScanOf(String qualifiedName, String alias) {
151        String name = (qualifiedName != null && !qualifiedName.isEmpty())
152                ? qualifiedName : alias;
153        return new TableScan(name, Collections.<String>emptyList(), null);
154    }
155
156    private static boolean containsUnsupported(JoinGraph jg) {
157        for (JoinEntity e : jg.getJoins()) {
158            if (e.getJoinType() == SemanticJoinType.UNSUPPORTED) return true;
159        }
160        return false;
161    }
162
163    private static Join.JoinType mapJoinType(SemanticJoinType t) {
164        switch (t) {
165            case INNER: return Join.JoinType.INNER;
166            case LEFT: return Join.JoinType.LEFT;
167            case RIGHT: return Join.JoinType.RIGHT;
168            case FULL: return Join.JoinType.FULL;
169            case NATURAL: return Join.JoinType.NATURAL;
170            case CROSS:
171            case IMPLICIT_CROSS:
172                return Join.JoinType.CROSS;
173            default:
174                // Unreachable: UNSUPPORTED statements are filtered upstream.
175                return Join.JoinType.INNER;
176        }
177    }
178
179    /**
180     * Map a semantic {@link SourceSpan} (1-based line/col, no offsets) to a
181     * {@link SourceAnchor}. Offsets are unavailable at the semantic layer,
182     * so they are set to -1; line/col are preserved.
183     */
184    static SourceAnchor anchorOf(SourceSpan span) {
185        if (span == null) return null;
186        return new SourceAnchor("",
187                /*startOffset=*/ -1, /*endOffset=*/ -1,
188                (int) span.getStartLine(), (int) span.getStartColumn(),
189                (int) span.getEndLine(), (int) span.getEndColumn(),
190                /*statementKey=*/ null, /*snippet=*/ null);
191    }
192}