001package gudusoft.gsqlparser.ir.semantic.joinanalysis; 002 003import gudusoft.gsqlparser.ir.semantic.SourceSpan; 004 005import java.util.ArrayList; 006import java.util.Collections; 007import java.util.List; 008import java.util.Objects; 009 010/** 011 * A structured per-join entity (GAP 1): the type, the two input 012 * endpoints, the written order, optional USING / NATURAL metadata, how 013 * it was written, an optional span, the decomposed ON predicate list 014 * (GAP 2, attached in slice 168), and an optional verbatim 015 * {@code conditionText} (the SQL substring derived from {@code span}, 016 * never reformatted). 017 * 018 * <p>Endpoints are {@link JoinEndpoint}s, never bare aliases, so a 019 * left-deep chain is modelled correctly: {@code rightEndpoint} is the 020 * newly added relation; {@code leftEndpoint} is the accumulated 021 * {@link JoinEndpointKind#JOIN_RESULT} of prior joins (or the first 022 * {@link JoinEndpointKind#RELATION}). 023 * 024 * <p>Immutable. Introduced by join-analysis slice 162 (S1); built in 025 * slice 167 (S6); predicates attached in slice 168 (S7). 026 */ 027public final class JoinEntity { 028 029 private final SemanticJoinType joinType; 030 private final JoinEndpoint leftEndpoint; 031 private final JoinEndpoint rightEndpoint; 032 private final int order; 033 private final JoinSourceSyntax sourceSyntax; 034 private final boolean naturalFlag; 035 private final boolean lateralFlag; 036 private final List<String> usingColumns; 037 private final List<Predicate> conditions; 038 private final SourceSpan sourceSpan; 039 private final String conditionText; 040 041 /** 042 * Original 10-arg constructor (non-lateral). Retained so existing 043 * callers and tests compile unchanged; delegates with 044 * {@code lateralFlag = false}. 045 */ 046 public JoinEntity(SemanticJoinType joinType, 047 JoinEndpoint leftEndpoint, JoinEndpoint rightEndpoint, 048 int order, JoinSourceSyntax sourceSyntax, 049 boolean naturalFlag, List<String> usingColumns, 050 List<Predicate> conditions, SourceSpan sourceSpan, 051 String conditionText) { 052 this(joinType, leftEndpoint, rightEndpoint, order, sourceSyntax, 053 naturalFlag, usingColumns, conditions, sourceSpan, conditionText, false); 054 } 055 056 /** 057 * Full constructor. {@code lateralFlag} marks a lateral join 058 * (SQL Server {@code CROSS APPLY} / {@code OUTER APPLY}); such a join 059 * carries no ON/USING and empty {@code conditions} — the correlation 060 * lives inside the right operand's own statement graph. The flag lets 061 * consumers distinguish a lateral join from a buggy cartesian INNER. 062 */ 063 public JoinEntity(SemanticJoinType joinType, 064 JoinEndpoint leftEndpoint, JoinEndpoint rightEndpoint, 065 int order, JoinSourceSyntax sourceSyntax, 066 boolean naturalFlag, List<String> usingColumns, 067 List<Predicate> conditions, SourceSpan sourceSpan, 068 String conditionText, boolean lateralFlag) { 069 if (joinType == null) { 070 throw new IllegalArgumentException("joinType must be non-null"); 071 } 072 if (leftEndpoint == null || rightEndpoint == null) { 073 throw new IllegalArgumentException("both endpoints must be non-null"); 074 } 075 if (sourceSyntax == null) { 076 throw new IllegalArgumentException("sourceSyntax must be non-null"); 077 } 078 if (order < 0) { 079 throw new IllegalArgumentException("order must be >= 0"); 080 } 081 this.joinType = joinType; 082 this.leftEndpoint = leftEndpoint; 083 this.rightEndpoint = rightEndpoint; 084 this.order = order; 085 this.sourceSyntax = sourceSyntax; 086 this.naturalFlag = naturalFlag; 087 this.lateralFlag = lateralFlag; 088 this.usingColumns = usingColumns == null 089 ? Collections.<String>emptyList() 090 : Collections.unmodifiableList(new ArrayList<String>(usingColumns)); 091 this.conditions = conditions == null 092 ? Collections.<Predicate>emptyList() 093 : Collections.unmodifiableList(new ArrayList<Predicate>(conditions)); 094 this.sourceSpan = sourceSpan; 095 this.conditionText = conditionText; 096 } 097 098 /** 099 * Return a copy of this entity with its ON-condition predicate list 100 * replaced (used by slice 168 to attach predicates without mutating 101 * the immutable entity built in slice 167). Preserves the lateral flag. 102 */ 103 public JoinEntity withConditions(List<Predicate> newConditions) { 104 return new JoinEntity(joinType, leftEndpoint, rightEndpoint, order, sourceSyntax, 105 naturalFlag, usingColumns, newConditions, sourceSpan, conditionText, lateralFlag); 106 } 107 108 public SemanticJoinType getJoinType() { 109 return joinType; 110 } 111 112 public JoinEndpoint getLeftEndpoint() { 113 return leftEndpoint; 114 } 115 116 public JoinEndpoint getRightEndpoint() { 117 return rightEndpoint; 118 } 119 120 public int getOrder() { 121 return order; 122 } 123 124 public JoinSourceSyntax getSourceSyntax() { 125 return sourceSyntax; 126 } 127 128 public boolean isNatural() { 129 return naturalFlag; 130 } 131 132 /** 133 * True for a lateral join (SQL Server {@code CROSS APPLY} / 134 * {@code OUTER APPLY}). Such a join has empty {@link #getConditions()} 135 * and {@link #getUsingColumns()}; the correlation lives inside the 136 * right operand. Distinguishes a lateral join from a cartesian INNER. 137 */ 138 public boolean isLateral() { 139 return lateralFlag; 140 } 141 142 /** Never null; empty unless a USING clause was written. */ 143 public List<String> getUsingColumns() { 144 return usingColumns; 145 } 146 147 /** Never null; empty until ON predicates are attached (slice 168). */ 148 public List<Predicate> getConditions() { 149 return conditions; 150 } 151 152 /** Optional; null when the parser cannot anchor the join clause. */ 153 public SourceSpan getSourceSpan() { 154 return sourceSpan; 155 } 156 157 /** Optional verbatim SQL substring of the ON condition; may be null. */ 158 public String getConditionText() { 159 return conditionText; 160 } 161 162 @Override 163 public boolean equals(Object o) { 164 if (this == o) return true; 165 if (!(o instanceof JoinEntity)) return false; 166 JoinEntity that = (JoinEntity) o; 167 return order == that.order 168 && naturalFlag == that.naturalFlag 169 && lateralFlag == that.lateralFlag 170 && joinType == that.joinType 171 && sourceSyntax == that.sourceSyntax 172 && leftEndpoint.equals(that.leftEndpoint) 173 && rightEndpoint.equals(that.rightEndpoint) 174 && usingColumns.equals(that.usingColumns) 175 && conditions.equals(that.conditions) 176 && Objects.equals(sourceSpan, that.sourceSpan) 177 && Objects.equals(conditionText, that.conditionText); 178 } 179 180 @Override 181 public int hashCode() { 182 return Objects.hash(joinType, leftEndpoint, rightEndpoint, order, sourceSyntax, 183 naturalFlag, lateralFlag, usingColumns, conditions, sourceSpan, conditionText); 184 } 185 186 @Override 187 public String toString() { 188 return "JoinEntity{order=" + order + ", " + joinType 189 + (lateralFlag ? " LATERAL" : "") + ", " 190 + leftEndpoint + " <-> " + rightEndpoint 191 + (conditions.isEmpty() ? "" : ", conds=" + conditions.size()) + "}"; 192 } 193}