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}