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}