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}