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}