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}