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}