001package gudusoft.gsqlparser.dlineage.dynamicsql;
002
003import java.util.Collections;
004import java.util.Set;
005import java.util.TreeSet;
006
007/**
008 * One boundary→boundary symbolic edge of a {@link RoutineSummary}
009 * (design {@code routine-summary-scc-design.md} §2.2): the reachability
010 * quotient of the routine's analyzed relation graph, interior nodes
011 * (locals, cursor/record columns, intermediate result sets, function hops)
012 * composed away.
013 *
014 * <p>Relation semantics mirror the inline engine: a boundary pair connected
015 * by at least one all-{@code fdd} path carries an {@code fdd} edge, and a
016 * pair connected by at least one path containing an {@code fdr} hop carries
017 * an {@code fdr} edge — the two COEXIST exactly as dlineage's own fdd/fdr
018 * relationships do (never OR-merged; §4.1 ROW-dominates applies per chain,
019 * not per pair).
020 */
021public final class RoutineSummaryEdge {
022
023    /** B5 advisory-metadata top: observed function names exceeded the finite
024     *  bound. The candidate edge survives widening; function order/path
025     *  attribution was never claimed. */
026    public static final String OBSERVED_FUNCTIONS_TOP = "OBSERVED_FUNCTIONS_TOP";
027    /** @deprecated use {@link #OBSERVED_FUNCTIONS_TOP}. */
028    @Deprecated
029    public static final String TRANSFORM_TOP = OBSERVED_FUNCTIONS_TOP;
030
031    /** Boundary endpoint classes of the B2 quotient (§2.2 endpoint table). */
032    public enum EndpointKind {
033        /** Declared formal parameter of the routine (any mode). */
034        FORMAL,
035        /** Physical (catalog) table column. */
036        TABLE_COLUMN,
037        /** Caller-visible temporary / table-variable column. */
038        TEMP_COLUMN,
039        /** Terminal result-set column identified by its recorded output name.
040         *  Positional projection is allowed only after a unique-shape preflight. */
041        RESULT_SET_COLUMN
042    }
043
044    /** One boundary endpoint: kind + resolved parent name + column name. */
045    public static final class Endpoint implements Comparable<Endpoint> {
046        private final EndpointKind kind;
047        private final String parent;
048        private final String column;
049
050        public Endpoint(EndpointKind kind, String parent, String column) {
051            this.kind = kind;
052            this.parent = parent == null ? "" : parent;
053            this.column = column == null ? "" : column;
054        }
055
056        public EndpointKind getKind() { return kind; }
057        public String getParent() { return parent; }
058        public String getColumn() { return column; }
059
060        @Override
061        public boolean equals(Object o) {
062            if (this == o) {
063                return true;
064            }
065            if (!(o instanceof Endpoint)) {
066                return false;
067            }
068            Endpoint e = (Endpoint) o;
069            // Names here are the analyzer's resolved display names captured
070            // from ONE model instance — string-identical by construction, so
071            // plain equality is correct (no re-normalization of identifiers).
072            return kind == e.kind && parent.equals(e.parent) && column.equals(e.column);
073        }
074
075        @Override
076        public int hashCode() {
077            return (kind.hashCode() * 31 + parent.hashCode()) * 31 + column.hashCode();
078        }
079
080        @Override
081        public int compareTo(Endpoint o) {
082            int c = kind.compareTo(o.kind);
083            if (c != 0) return c;
084            c = parent.compareTo(o.parent);
085            if (c != 0) return c;
086            return column.compareTo(o.column);
087        }
088
089        @Override
090        public String toString() {
091            return kind + ":" + parent + "." + column;
092        }
093    }
094
095    private final Endpoint source;
096    private final Endpoint target;
097    /** {@code "fdd"} (value path) or {@code "fdr"} (row-affecting path). */
098    private final String relationType;
099    /**
100     * Function hops observed on contributing value paths. ADVISORY ONLY in
101     * B2: an unordered union across all paths is not a composable transform
102     * descriptor (it loses order, multiplicity, and per-path attribution).
103     * Never use this field for equality, caching, or application decisions.
104     * B5 bounds this advisory union and
105     * widens it to {@link #OBSERVED_FUNCTIONS_TOP}; ordered, per-path transform
106     * descriptors remain future work.
107     */
108    private final Set<String> observedFunctions;
109
110    public RoutineSummaryEdge(Endpoint source, Endpoint target, String relationType,
111            Set<String> functions) {
112        this.source = source;
113        this.target = target;
114        this.relationType = relationType;
115        this.observedFunctions = functions == null
116                ? Collections.<String>emptySet()
117                : Collections.unmodifiableSet(new TreeSet<String>(functions));
118    }
119
120    public Endpoint getSource() { return source; }
121    public Endpoint getTarget() { return target; }
122    public String getRelationType() { return relationType; }
123    /** Unordered advisory union of function names observed on contributing
124     *  value paths. It is not an ordered transform descriptor. */
125    public Set<String> getObservedFunctions() { return observedFunctions; }
126
127    /** @deprecated use {@link #getObservedFunctions()}; retained for source
128     *  compatibility with the B2/B3 summary API. */
129    @Deprecated
130    public Set<String> getFunctions() { return observedFunctions; }
131
132    public boolean isObservedFunctionsTop() {
133        return observedFunctions.contains(OBSERVED_FUNCTIONS_TOP);
134    }
135
136    /** @deprecated use {@link #isObservedFunctionsTop()}. */
137    @Deprecated
138    public boolean isTransformTop() {
139        return isObservedFunctionsTop();
140    }
141
142    /**
143     * Join value metadata for two edges with the same edge identity. The
144     * identity itself deliberately excludes advisory function metadata; once the finite
145     * advisory-name bound is exceeded the value widens to
146     * {@link #OBSERVED_FUNCTIONS_TOP} and can never narrow again. This gives
147     * recursive summary composition finite metadata height while preserving
148     * the lineage edge. No function order or per-path attribution is implied.
149     */
150    RoutineSummaryEdge joinMetadata(RoutineSummaryEdge other,
151            int maxObservedFunctions) {
152        if (!equals(other)) {
153            throw new IllegalArgumentException("cannot join different summary edges");
154        }
155        if (isObservedFunctionsTop() || other.isObservedFunctionsTop()) {
156            return new RoutineSummaryEdge(source, target, relationType,
157                    Collections.singleton(OBSERVED_FUNCTIONS_TOP));
158        }
159        Set<String> joined = new TreeSet<String>(observedFunctions);
160        joined.addAll(other.observedFunctions);
161        if (joined.size() > maxObservedFunctions) {
162            joined = Collections.singleton(OBSERVED_FUNCTIONS_TOP);
163        }
164        return new RoutineSummaryEdge(source, target, relationType, joined);
165    }
166
167    /** Identity is (source, target, relationType) — §2.5 edge identity; the
168     *  observed-function set is advisory metadata joined by union, not identity. */
169    @Override
170    public boolean equals(Object o) {
171        if (this == o) {
172            return true;
173        }
174        if (!(o instanceof RoutineSummaryEdge)) {
175            return false;
176        }
177        RoutineSummaryEdge e = (RoutineSummaryEdge) o;
178        // non-identifier-compare: relationType is a fixed enum-like token
179        return source.equals(e.source) && target.equals(e.target)
180                && relationType.equals(e.relationType);
181    }
182
183    @Override
184    public int hashCode() {
185        return (source.hashCode() * 31 + target.hashCode()) * 31 + relationType.hashCode();
186    }
187
188    @Override
189    public String toString() {
190        return source + " -" + relationType + "-> " + target
191                // Preserve the established rendering; the accessor/Javadoc carry
192                // the stricter advisory-only semantics without breaking logs.
193                + (observedFunctions.isEmpty() ? "" : " via " + observedFunctions);
194    }
195}