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 List<String> usingColumns; 036 private final List<Predicate> conditions; 037 private final SourceSpan sourceSpan; 038 private final String conditionText; 039 040 public JoinEntity(SemanticJoinType joinType, 041 JoinEndpoint leftEndpoint, JoinEndpoint rightEndpoint, 042 int order, JoinSourceSyntax sourceSyntax, 043 boolean naturalFlag, List<String> usingColumns, 044 List<Predicate> conditions, SourceSpan sourceSpan, 045 String conditionText) { 046 if (joinType == null) { 047 throw new IllegalArgumentException("joinType must be non-null"); 048 } 049 if (leftEndpoint == null || rightEndpoint == null) { 050 throw new IllegalArgumentException("both endpoints must be non-null"); 051 } 052 if (sourceSyntax == null) { 053 throw new IllegalArgumentException("sourceSyntax must be non-null"); 054 } 055 if (order < 0) { 056 throw new IllegalArgumentException("order must be >= 0"); 057 } 058 this.joinType = joinType; 059 this.leftEndpoint = leftEndpoint; 060 this.rightEndpoint = rightEndpoint; 061 this.order = order; 062 this.sourceSyntax = sourceSyntax; 063 this.naturalFlag = naturalFlag; 064 this.usingColumns = usingColumns == null 065 ? Collections.<String>emptyList() 066 : Collections.unmodifiableList(new ArrayList<String>(usingColumns)); 067 this.conditions = conditions == null 068 ? Collections.<Predicate>emptyList() 069 : Collections.unmodifiableList(new ArrayList<Predicate>(conditions)); 070 this.sourceSpan = sourceSpan; 071 this.conditionText = conditionText; 072 } 073 074 /** 075 * Return a copy of this entity with its ON-condition predicate list 076 * replaced (used by slice 168 to attach predicates without mutating 077 * the immutable entity built in slice 167). 078 */ 079 public JoinEntity withConditions(List<Predicate> newConditions) { 080 return new JoinEntity(joinType, leftEndpoint, rightEndpoint, order, sourceSyntax, 081 naturalFlag, usingColumns, newConditions, sourceSpan, conditionText); 082 } 083 084 public SemanticJoinType getJoinType() { 085 return joinType; 086 } 087 088 public JoinEndpoint getLeftEndpoint() { 089 return leftEndpoint; 090 } 091 092 public JoinEndpoint getRightEndpoint() { 093 return rightEndpoint; 094 } 095 096 public int getOrder() { 097 return order; 098 } 099 100 public JoinSourceSyntax getSourceSyntax() { 101 return sourceSyntax; 102 } 103 104 public boolean isNatural() { 105 return naturalFlag; 106 } 107 108 /** Never null; empty unless a USING clause was written. */ 109 public List<String> getUsingColumns() { 110 return usingColumns; 111 } 112 113 /** Never null; empty until ON predicates are attached (slice 168). */ 114 public List<Predicate> getConditions() { 115 return conditions; 116 } 117 118 /** Optional; null when the parser cannot anchor the join clause. */ 119 public SourceSpan getSourceSpan() { 120 return sourceSpan; 121 } 122 123 /** Optional verbatim SQL substring of the ON condition; may be null. */ 124 public String getConditionText() { 125 return conditionText; 126 } 127 128 @Override 129 public boolean equals(Object o) { 130 if (this == o) return true; 131 if (!(o instanceof JoinEntity)) return false; 132 JoinEntity that = (JoinEntity) o; 133 return order == that.order 134 && naturalFlag == that.naturalFlag 135 && joinType == that.joinType 136 && sourceSyntax == that.sourceSyntax 137 && leftEndpoint.equals(that.leftEndpoint) 138 && rightEndpoint.equals(that.rightEndpoint) 139 && usingColumns.equals(that.usingColumns) 140 && conditions.equals(that.conditions) 141 && Objects.equals(sourceSpan, that.sourceSpan) 142 && Objects.equals(conditionText, that.conditionText); 143 } 144 145 @Override 146 public int hashCode() { 147 return Objects.hash(joinType, leftEndpoint, rightEndpoint, order, sourceSyntax, 148 naturalFlag, usingColumns, conditions, sourceSpan, conditionText); 149 } 150 151 @Override 152 public String toString() { 153 return "JoinEntity{order=" + order + ", " + joinType + ", " 154 + leftEndpoint + " <-> " + rightEndpoint 155 + (conditions.isEmpty() ? "" : ", conds=" + conditions.size()) + "}"; 156 } 157}