001package gudusoft.gsqlparser.ir.semantic.diff;
002
003import java.util.Comparator;
004import java.util.Objects;
005
006/**
007 * One classified divergence between IR and dlineage projections.
008 *
009 * <p>{@link #outputName} encodes which output the divergence belongs to:
010 *
011 * <ul>
012 *   <li>The lower-cased outer-output name for SELECT and aggregate divergences.</li>
013 *   <li>{@code null} for FILTER/JOIN row-influence divergences (matches the
014 *       null-anchor convention on {@link CanonicalLineageEdge}).</li>
015 *   <li>The literal string {@code "<query>"} for the two query-wide
016 *       unsupported classes.</li>
017 * </ul>
018 */
019public final class Divergence {
020
021    /** Reserved string used by {@link DivergenceClass#UNSUPPORTED_BY_IR}/{@code _DLINEAGE}. */
022    public static final String QUERY_WIDE = "<query>";
023
024    /** Detail value used by the output-presence pass. */
025    public static final String DETAIL_OUTPUT_PRESENT = "output-present";
026
027    /** Stable ordering for deterministic JSON output. */
028    public static final Comparator<Divergence> ORDER =
029            Comparator
030                    .comparing((Divergence d) -> d.kind.name())
031                    // null sorts before non-null
032                    .thenComparing(d -> d.outputName == null ? 0 : 1)
033                    .thenComparing(d -> d.outputName == null ? "" : d.outputName)
034                    .thenComparing(d -> d.detail);
035
036    private final DivergenceClass kind;
037    private final String outputName;
038    private final String detail;
039
040    public Divergence(DivergenceClass kind, String outputName, String detail) {
041        if (kind == null) {
042            throw new IllegalArgumentException("kind must not be null");
043        }
044        if (detail == null || detail.isEmpty()) {
045            throw new IllegalArgumentException("detail must be non-empty");
046        }
047        this.kind = kind;
048        this.outputName = outputName;
049        this.detail = detail;
050    }
051
052    public DivergenceClass getKind() {
053        return kind;
054    }
055
056    /** May be null for FILTER/JOIN row-influence divergences. */
057    public String getOutputName() {
058        return outputName;
059    }
060
061    public String getDetail() {
062        return detail;
063    }
064
065    @Override
066    public boolean equals(Object o) {
067        if (this == o) return true;
068        if (!(o instanceof Divergence)) return false;
069        Divergence other = (Divergence) o;
070        return kind == other.kind
071                && Objects.equals(outputName, other.outputName)
072                && detail.equals(other.detail);
073    }
074
075    @Override
076    public int hashCode() {
077        return Objects.hash(kind, outputName, detail);
078    }
079
080    @Override
081    public String toString() {
082        return kind + " " + (outputName == null ? "<row-set>" : outputName) + " — " + detail;
083    }
084}