001package gudusoft.gsqlparser.dlineage.dynamicsql; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.nodes.TParameterDeclaration; 005import gudusoft.gsqlparser.nodes.TParameterDeclarationList; 006import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 007import gudusoft.gsqlparser.util.SQLUtil; 008 009import java.util.ArrayList; 010import java.util.Collections; 011import java.util.List; 012 013/** 014 * Canonical routine DEFINITION SIGNATURE DESCRIPTOR (design 015 * {@code docs/designs/sp/routine-summary-scc-design.md} §2.1a): the uniqueness 016 * key for catalog entries, summary caches, and call-graph nodes. 017 * 018 * <p><b>This is NOT a resolution key.</b> Overload resolution (named/positional 019 * binding, default-fill, vendor type-family matching) is a separate function 020 * over a candidate set — {@code paramTypeNames} here carries the canonicalized 021 * declared type TEXT purely as a definition discriminator (two definitions 022 * differing only in {@code NUMBER} vs {@code INTEGER} spelling are distinct 023 * definitions), which deliberately must NOT be used to decide whether an 024 * actual argument is compatible with a formal. 025 * 026 * <p>All name-derived fields hold the canonical text produced by 027 * {@link SQLUtil#canonKey} — never raw identifier spellings — so 028 * {@link #equals(Object)}/{@link #hashCode()} compare canonical values with 029 * plain string equality (the one sanctioned way to compare identifiers is to 030 * canonicalize once and compare the canonical outputs; see 031 * {@code identifier_normalization_guide.md}). Absent segments are stored as 032 * the empty string, mirroring the identity layer's null-segment rule. 033 * 034 * <p>Occurrence provenance (which unit, which statement index) is deliberately 035 * NOT part of identity: duplicate definitions of the same identity must stay 036 * visible as duplicates (AMBIGUOUS — bind nothing), not overwrite one another. 037 * The catalog carries provenance beside the identity. 038 */ 039public final class RoutineIdentity { 040 041 private final String canonCatalog; 042 private final String canonSchema; 043 private final String canonPackage; 044 private final String canonName; 045 private final RoutineKind kind; 046 private final List<String> paramNames; 047 private final List<String> paramModes; 048 private final List<String> paramTypeNames; 049 private final List<Boolean> paramHasDefault; 050 private final String overloadDiscriminator; 051 /** ORIGINAL declared parameter spellings — matching material only 052 * (named-argument lookup goes through SQLUtil.sameName over raw 053 * spellings; re-canonicalizing stored canonText double-folds quoted 054 * names — codex-B3-r2 finding 12). NEVER part of equals/hashCode. */ 055 private final List<String> rawParamNames; 056 057 private RoutineIdentity(String canonCatalog, String canonSchema, String canonPackage, 058 String canonName, RoutineKind kind, List<String> paramNames, 059 List<String> paramModes, List<String> paramTypeNames, 060 List<Boolean> paramHasDefault, String overloadDiscriminator, 061 List<String> rawParamNames) { 062 this.canonCatalog = canonCatalog; 063 this.canonSchema = canonSchema; 064 this.canonPackage = canonPackage; 065 this.canonName = canonName; 066 this.kind = kind; 067 this.paramNames = Collections.unmodifiableList(paramNames); 068 this.paramModes = Collections.unmodifiableList(paramModes); 069 this.paramTypeNames = Collections.unmodifiableList(paramTypeNames); 070 this.paramHasDefault = Collections.unmodifiableList(paramHasDefault); 071 this.overloadDiscriminator = overloadDiscriminator; 072 this.rawParamNames = Collections.unmodifiableList(rawParamNames); 073 } 074 075 /** 076 * Builds the identity of one definition from its AST parameter list and 077 * name segments (raw spellings; canonicalized here, exactly once). 078 * 079 * @param vendor analysis vendor (drives canonicalization rules) 080 * @param kind routine kind discriminator 081 * @param catalog raw catalog/database segment, null when absent 082 * @param schema raw schema segment, null when absent 083 * @param packageName raw package segment, null for non-package routines 084 * @param simpleName raw simple routine name (required) 085 * @param params declared parameter list, null for parameterless 086 * @param overloadDiscriminator vendor extra (mssql numbered-proc ";N" 087 * suffix), null/empty when none 088 */ 089 public static RoutineIdentity of(EDbVendor vendor, RoutineKind kind, String catalog, 090 String schema, String packageName, String simpleName, 091 TParameterDeclarationList params, String overloadDiscriminator) { 092 List<String> names = new ArrayList<String>(); 093 List<String> rawNames = new ArrayList<String>(); 094 List<String> modes = new ArrayList<String>(); 095 List<String> types = new ArrayList<String>(); 096 List<Boolean> defaults = new ArrayList<Boolean>(); 097 if (params != null) { 098 for (int i = 0; i < params.size(); i++) { 099 TParameterDeclaration p = params.getParameterDeclarationItem(i); 100 names.add(p.getParameterName() == null ? "" 101 : canon(vendor, ESQLDataObjectType.dotColumn, 102 p.getParameterName().toString())); 103 rawNames.add(p.getParameterName() == null ? "" 104 : p.getParameterName().toString()); 105 modes.add(p.getParameterMode() == null ? "" : p.getParameterMode().name()); 106 types.add(p.getDataType() == null ? "" 107 : canonTypeText(p.getDataType().toString())); 108 defaults.add(Boolean.valueOf(p.getDefaultValue() != null)); 109 } 110 } 111 RoutineKind routineKind = kind == null ? RoutineKind.PROCEDURE : kind; 112 ESQLDataObjectType nameType = 113 (routineKind == RoutineKind.FUNCTION 114 || routineKind == RoutineKind.PACKAGE_MEMBER_FUNCTION) 115 ? ESQLDataObjectType.dotFunction : ESQLDataObjectType.dotProcedure; 116 return new RoutineIdentity( 117 canon(vendor, ESQLDataObjectType.dotTable, catalog), 118 canon(vendor, ESQLDataObjectType.dotTable, schema), 119 canon(vendor, nameType, packageName), 120 canon(vendor, nameType, simpleName), 121 routineKind, names, modes, types, defaults, 122 overloadDiscriminator == null ? "" : overloadDiscriminator, 123 rawNames); 124 } 125 126 /** Canonical text of one raw name segment; "" for absent. */ 127 private static String canon(EDbVendor vendor, ESQLDataObjectType type, String raw) { 128 if (raw == null || raw.length() == 0) { 129 return ""; 130 } 131 return SQLUtil.canonKey(vendor, type, raw).getCanonText(); 132 } 133 134 /** 135 * Declared type text normalized for use as a definition discriminator: 136 * whitespace collapsed and upper-cased. Type TEXT is deliberately not 137 * resolved to a vendor type family here — see the class contract. 138 */ 139 // non-identifier-compare: declared type text is not a database object name 140 private static String canonTypeText(String typeText) { 141 if (typeText == null) { 142 return ""; 143 } 144 return typeText.trim().replaceAll("\\s+", " ").toUpperCase(java.util.Locale.ROOT); 145 } 146 147 public String getCanonCatalog() { return canonCatalog; } 148 public String getCanonSchema() { return canonSchema; } 149 public String getCanonPackage() { return canonPackage; } 150 public String getCanonName() { return canonName; } 151 public RoutineKind getKind() { return kind; } 152 public List<String> getParamNames() { return paramNames; } 153 public List<String> getParamModes() { return paramModes; } 154 public List<String> getParamTypeNames() { return paramTypeNames; } 155 public List<Boolean> getParamHasDefault() { return paramHasDefault; } 156 /** Raw declared spellings, position-aligned with {@link #getParamNames()}; 157 * matching material only — never identity. */ 158 public List<String> getRawParamNames() { return rawParamNames; } 159 public String getOverloadDiscriminator() { return overloadDiscriminator; } 160 public int getParamCount() { return paramNames.size(); } 161 162 @Override 163 public boolean equals(Object o) { 164 if (this == o) { 165 return true; 166 } 167 if (!(o instanceof RoutineIdentity)) { 168 return false; 169 } 170 RoutineIdentity other = (RoutineIdentity) o; 171 // All string fields hold canonical outputs of SQLUtil.canonKey (or 172 // normalized type text) — plain equality over canonical values is the 173 // sanctioned identifier comparison for map/set keys. 174 return canonCatalog.equals(other.canonCatalog) 175 && canonSchema.equals(other.canonSchema) 176 && canonPackage.equals(other.canonPackage) 177 && canonName.equals(other.canonName) 178 && kind == other.kind 179 && paramNames.equals(other.paramNames) 180 && paramModes.equals(other.paramModes) 181 && paramTypeNames.equals(other.paramTypeNames) 182 && paramHasDefault.equals(other.paramHasDefault) 183 && overloadDiscriminator.equals(other.overloadDiscriminator); 184 } 185 186 @Override 187 public int hashCode() { 188 int h = canonCatalog.hashCode(); 189 h = h * 31 + canonSchema.hashCode(); 190 h = h * 31 + canonPackage.hashCode(); 191 h = h * 31 + canonName.hashCode(); 192 h = h * 31 + kind.hashCode(); 193 h = h * 31 + paramNames.hashCode(); 194 h = h * 31 + paramModes.hashCode(); 195 h = h * 31 + paramTypeNames.hashCode(); 196 h = h * 31 + paramHasDefault.hashCode(); 197 h = h * 31 + overloadDiscriminator.hashCode(); 198 return h; 199 } 200 201 /** 202 * INJECTIVE canonical signature text: two identities render equal here 203 * iff {@link #equals} holds. Every field is LENGTH-PREFIXED 204 * ({@code <len>:<value>}), so the encoding is injective by construction 205 * regardless of field CONTENT — a delimiter-based rendering was not 206 * (quoted identifiers may contain any character, including the 207 * delimiters; codex-B3-r4 finding 2). Used wherever a STRING must stand 208 * in for the identity (scope keys); a hash digest is not injective 209 * either and collided in practice. 210 */ 211 public String signatureKey() { 212 StringBuilder sb = new StringBuilder(); 213 appendLengthPrefixed(sb, kind.name()); 214 appendLengthPrefixed(sb, canonCatalog); 215 appendLengthPrefixed(sb, canonSchema); 216 appendLengthPrefixed(sb, canonPackage); 217 appendLengthPrefixed(sb, canonName); 218 appendLengthPrefixed(sb, overloadDiscriminator); 219 sb.append(paramNames.size()).append('#'); 220 for (int i = 0; i < paramNames.size(); i++) { 221 appendLengthPrefixed(sb, paramNames.get(i)); 222 appendLengthPrefixed(sb, paramModes.get(i)); 223 appendLengthPrefixed(sb, paramTypeNames.get(i)); 224 appendLengthPrefixed(sb, paramHasDefault.get(i).toString()); 225 } 226 return sb.toString(); 227 } 228 229 private static void appendLengthPrefixed(StringBuilder sb, String value) { 230 sb.append(value.length()).append(':').append(value); 231 } 232 233 @Override 234 public String toString() { 235 StringBuilder sb = new StringBuilder(kind.name()).append(' '); 236 if (canonCatalog.length() > 0) { 237 sb.append(canonCatalog).append('.'); 238 } 239 if (canonSchema.length() > 0) { 240 sb.append(canonSchema).append('.'); 241 } 242 if (canonPackage.length() > 0) { 243 sb.append(canonPackage).append('.'); 244 } 245 sb.append(canonName).append('/').append(paramNames.size()); 246 if (overloadDiscriminator.length() > 0) { 247 sb.append(';').append(overloadDiscriminator); 248 } 249 return sb.toString(); 250 } 251}