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 semantic predicate list 014 * (GAP 2, attached in slice 168), and optional verbatim source 015 * {@code conditionText}. The source text and semantic predicates are 016 * deliberately separate representations: {@code conditionText} is never 017 * synthesized from {@link #getConditions()} or reformatted. 018 * 019 * <p>Endpoints are {@link JoinEndpoint}s, never bare aliases, so a 020 * left-deep chain is modelled correctly: {@code rightEndpoint} is the 021 * newly added relation; {@code leftEndpoint} is the accumulated 022 * {@link JoinEndpointKind#JOIN_RESULT} of prior joins (or the first 023 * {@link JoinEndpointKind#RELATION}). 024 * 025 * <p>Immutable. Introduced by join-analysis slice 162 (S1); built in 026 * slice 167 (S6); predicates attached in slice 168 (S7). 027 * 028 * <p><b>API status: supported read-only result.</b> Public getters are part of 029 * Join Analysis Consumption Profile v1. Constructors and {@code with*} 030 * helpers are producer-oriented and are outside that profile. 031 */ 032public final class JoinEntity { 033 034 private final SemanticJoinType joinType; 035 private final JoinEndpoint leftEndpoint; 036 private final JoinEndpoint rightEndpoint; 037 private final int order; 038 private final JoinSourceSyntax sourceSyntax; 039 private final boolean naturalFlag; 040 private final boolean lateralFlag; 041 private final List<String> usingColumns; 042 private final List<Predicate> conditions; 043 private final SourceSpan sourceSpan; 044 private final String conditionText; 045 046 /** 047 * Original 10-arg constructor (non-lateral). Retained so existing 048 * callers and tests compile unchanged; delegates with 049 * {@code lateralFlag = false}. 050 */ 051 public JoinEntity(SemanticJoinType joinType, 052 JoinEndpoint leftEndpoint, JoinEndpoint rightEndpoint, 053 int order, JoinSourceSyntax sourceSyntax, 054 boolean naturalFlag, List<String> usingColumns, 055 List<Predicate> conditions, SourceSpan sourceSpan, 056 String conditionText) { 057 this(joinType, leftEndpoint, rightEndpoint, order, sourceSyntax, 058 naturalFlag, usingColumns, conditions, sourceSpan, conditionText, false); 059 } 060 061 /** 062 * Full constructor. {@code lateralFlag} marks a lateral join 063 * (SQL Server {@code CROSS APPLY} / {@code OUTER APPLY}); such a join 064 * carries no ON/USING and empty {@code conditions} — the correlation 065 * lives inside the right operand's own statement graph. The flag lets 066 * consumers distinguish a lateral join from a buggy cartesian INNER. 067 */ 068 public JoinEntity(SemanticJoinType joinType, 069 JoinEndpoint leftEndpoint, JoinEndpoint rightEndpoint, 070 int order, JoinSourceSyntax sourceSyntax, 071 boolean naturalFlag, List<String> usingColumns, 072 List<Predicate> conditions, SourceSpan sourceSpan, 073 String conditionText, boolean lateralFlag) { 074 if (joinType == null) { 075 throw new IllegalArgumentException("joinType must be non-null"); 076 } 077 if (leftEndpoint == null || rightEndpoint == null) { 078 throw new IllegalArgumentException("both endpoints must be non-null"); 079 } 080 if (sourceSyntax == null) { 081 throw new IllegalArgumentException("sourceSyntax must be non-null"); 082 } 083 if (order < 0) { 084 throw new IllegalArgumentException("order must be >= 0"); 085 } 086 this.joinType = joinType; 087 this.leftEndpoint = leftEndpoint; 088 this.rightEndpoint = rightEndpoint; 089 this.order = order; 090 this.sourceSyntax = sourceSyntax; 091 this.naturalFlag = naturalFlag; 092 this.lateralFlag = lateralFlag; 093 this.usingColumns = usingColumns == null 094 ? Collections.<String>emptyList() 095 : Collections.unmodifiableList(new ArrayList<String>(usingColumns)); 096 this.conditions = conditions == null 097 ? Collections.<Predicate>emptyList() 098 : Collections.unmodifiableList(new ArrayList<Predicate>(conditions)); 099 this.sourceSpan = sourceSpan; 100 this.conditionText = conditionText; 101 } 102 103 /** 104 * Return a copy of this entity with its ON-condition predicate list 105 * replaced (used by slice 168 to attach predicates without mutating 106 * the immutable entity built in slice 167). Preserves the lateral flag. 107 */ 108 public JoinEntity withConditions(List<Predicate> newConditions) { 109 return new JoinEntity(joinType, leftEndpoint, rightEndpoint, order, sourceSyntax, 110 naturalFlag, usingColumns, newConditions, sourceSpan, conditionText, lateralFlag); 111 } 112 113 public SemanticJoinType getJoinType() { 114 return joinType; 115 } 116 117 public JoinEndpoint getLeftEndpoint() { 118 return leftEndpoint; 119 } 120 121 public JoinEndpoint getRightEndpoint() { 122 return rightEndpoint; 123 } 124 125 public int getOrder() { 126 return order; 127 } 128 129 public JoinSourceSyntax getSourceSyntax() { 130 return sourceSyntax; 131 } 132 133 public boolean isNatural() { 134 return naturalFlag; 135 } 136 137 /** 138 * True for a lateral join (SQL Server {@code CROSS APPLY} / 139 * {@code OUTER APPLY}). Such a join has empty {@link #getConditions()} 140 * and {@link #getUsingColumns()}; the correlation lives inside the 141 * right operand. Distinguishes a lateral join from a cartesian INNER. 142 */ 143 public boolean isLateral() { 144 return lateralFlag; 145 } 146 147 /** Never null; empty unless a USING clause was written. */ 148 public List<String> getUsingColumns() { 149 return usingColumns; 150 } 151 152 /** 153 * Semantic conditions attached to this join. Never {@code null}; may be 154 * populated from {@code ON}, promoted {@code WHERE} predicates, or a 155 * predicate-derived semi-join independently of {@link #getConditionText()}. 156 */ 157 public List<Predicate> getConditions() { 158 return conditions; 159 } 160 161 /** Optional; null when the parser cannot anchor the join clause. */ 162 public SourceSpan getSourceSpan() { 163 return sourceSpan; 164 } 165 166 /** 167 * Returns the optional verbatim source expression that directly defines 168 * this join entity's condition. 169 * 170 * <p>For an {@link JoinSourceSyntax#EXPLICIT EXPLICIT} join with 171 * {@code ON}, this is the {@code ON} expression without the {@code ON} 172 * keyword. For a predicate-derived {@link JoinSourceSyntax#SEMI SEMI} or 173 * anti-semi join, this is the complete {@code EXISTS}/{@code IN} wrapper 174 * expression, including {@code NOT} when written. 175 * 176 * <p>It is {@code null} for {@link JoinSourceSyntax#COMMA COMMA} joins, 177 * even when {@link #getConditions()} contains predicates promoted from 178 * {@code WHERE}, because no join-local condition expression was written. 179 * It is also {@code null} for {@code CROSS}, {@code USING}, 180 * {@code NATURAL}, and condition-less {@code LATERAL}/{@code APPLY} 181 * source forms, or when the source expression cannot be anchored. 182 * 183 * <p>The returned text is never synthesized or reformatted. It is not a 184 * serialization of {@link #getConditions()} and is not guaranteed to be 185 * valid as an {@code ON} expression in another SQL dialect. 186 */ 187 public String getConditionText() { 188 return conditionText; 189 } 190 191 @Override 192 public boolean equals(Object o) { 193 if (this == o) return true; 194 if (!(o instanceof JoinEntity)) return false; 195 JoinEntity that = (JoinEntity) o; 196 return order == that.order 197 && naturalFlag == that.naturalFlag 198 && lateralFlag == that.lateralFlag 199 && joinType == that.joinType 200 && sourceSyntax == that.sourceSyntax 201 && leftEndpoint.equals(that.leftEndpoint) 202 && rightEndpoint.equals(that.rightEndpoint) 203 && usingColumns.equals(that.usingColumns) 204 && conditions.equals(that.conditions) 205 && Objects.equals(sourceSpan, that.sourceSpan) 206 && Objects.equals(conditionText, that.conditionText); 207 } 208 209 @Override 210 public int hashCode() { 211 return Objects.hash(joinType, leftEndpoint, rightEndpoint, order, sourceSyntax, 212 naturalFlag, lateralFlag, usingColumns, conditions, sourceSpan, conditionText); 213 } 214 215 @Override 216 public String toString() { 217 return "JoinEntity{order=" + order + ", " + joinType 218 + (lateralFlag ? " LATERAL" : "") + ", " 219 + leftEndpoint + " <-> " + rightEndpoint 220 + (conditions.isEmpty() ? "" : ", conds=" + conditions.size()) + "}"; 221 } 222}