001package gudusoft.gsqlparser.dlineage.routine;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.TSourceToken;
005import gudusoft.gsqlparser.dlineage.dataflow.model.ModelBindingManager;
006import gudusoft.gsqlparser.nodes.TObjectName;
007import gudusoft.gsqlparser.sqlenv.CanonKey;
008import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
009import gudusoft.gsqlparser.sqlenv.IdentifierInputForm;
010import gudusoft.gsqlparser.sqlenv.IdentifierService;
011import gudusoft.gsqlparser.util.SQLUtil;
012
013import java.util.ArrayList;
014import java.util.Collections;
015import java.util.List;
016
017/**
018 * A key of the routine alias registry, the lineage model's name-keyed index of routines, on typed names.
019 *
020 * <p>The legacy index keys a routine by text: its name folded by the legacy normal-name function, followed by one of
021 * three alias suffixes rendered from the declaration ({@code (int,int)}, {@code (2)}, {@code (in1)}), appended
022 * unchanged. This key keeps that suffix as the same raw text, so aliases and overloads behave exactly as before, and
023 * replaces the folded name with the name's levels as written, in positional order, each keyed by the analysis's
024 * identifier rules. The routine level is keyed under the routine rules. Every qualifier is keyed under the schema
025 * rules, whether it is a server, a database, a schema or a package: which one it is cannot be known at a call, and
026 * two keys compare level by level.</p>
027 *
028 * <p>An explicitly empty middle level ({@code db..p}) is a level of its own, so {@code db..p} keeps three levels, as
029 * its legacy text did. A database link ({@code p@link}) is part of the key, as the legacy kept {@code @link} in the
030 * text.</p>
031 */
032public final class RoutineAliasKey {
033
034    /** The routine level's comparison type. Procedures and functions share one index, and one rule group. */
035    static final ESQLDataObjectType ROUTINE_TYPE = ESQLDataObjectType.dotProcedure;
036    /** Every qualifier's comparison type. */
037    static final ESQLDataObjectType QUALIFIER_TYPE = ESQLDataObjectType.dotSchema;
038
039    /** The level of the empty qualifier in {@code db..p}. */
040    private static final Object EMPTY_LEVEL = new Object() {
041        @Override
042        public String toString() {
043            return "<empty>";
044        }
045    };
046
047    private final List<Object> levels;
048    private final CanonKey dblink;
049    private final String signature;
050    private final CanonKey storedSignature;
051    /**
052     * Whether a level's text as written contains {@code (} ({@code [P(]}): the legacy lookup chose its branch by a
053     * {@code (} anywhere in the key text, so such a name is looked up as the legacy looked up a name with a suffix.
054     */
055    private final boolean parenInName;
056    /**
057     * The routine level keyed as a qualifier: a package's name is a routine-level name of its own, and a qualifier of
058     * its members' names ({@link #qualifiedBy}), where it must compare with a call's qualifier.
059     */
060    private final CanonKey routineAsQualifier;
061    private final int hash;
062
063    private RoutineAliasKey(List<Object> levels, CanonKey dblink, String signature, CanonKey storedSignature,
064            boolean parenInName, CanonKey routineAsQualifier) {
065        this.levels = Collections.unmodifiableList(levels);
066        this.dblink = dblink;
067        this.signature = signature;
068        this.storedSignature = storedSignature;
069        this.parenInName = parenInName;
070        this.routineAsQualifier = routineAsQualifier;
071        int h = levels.hashCode();
072        h = 31 * h + (dblink == null ? 0 : dblink.hashCode());
073        h = 31 * h + (signature == null ? 0 : signature.hashCode());
074        this.hash = h;
075    }
076
077    /** The analysis's identifier service, or the vendor default when no analysis has published one. */
078    public static IdentifierService service() {
079        IdentifierService service = ModelBindingManager.getGlobalIdentifierService();
080        if (service != null) {
081            return service;
082        }
083        EDbVendor vendor = ModelBindingManager.getGlobalVendor();
084        return vendor == null ? null : IdentifierService.defaultServiceFor(vendor);
085    }
086
087    /**
088     * The key of a routine name as written at a call or a declaration, with the legacy alias suffix
089     * {@code signature} (null when the legacy key had none). Null when the name has no routine level.
090     */
091    public static RoutineAliasKey written(TObjectName name, String signature) {
092        return written(name, signature, service());
093    }
094
095    public static RoutineAliasKey written(TObjectName name, String signature, IdentifierService service) {
096        if (name == null || service == null) {
097            return null;
098        }
099        try {
100            return writtenKey(name, signature, service);
101        } catch (RuntimeException unkeyable) {
102            // A name the identifier rules cannot key (it could not be declared or called either) names nothing here.
103            return null;
104        }
105    }
106
107    private static RoutineAliasKey writtenKey(TObjectName name, String signature, IdentifierService service) {
108        TSourceToken server = name.getServerToken();
109        TSourceToken database = name.getDatabaseToken();
110        TSourceToken schema = name.getSchemaToken();
111        TSourceToken pkg = name.getPackageToken();
112        TSourceToken object = name.getObjectToken();
113        TSourceToken part = name.getPartToken();
114        if (part != null && !part.toString().isEmpty()) {
115            // A name the parser split as object.part (DB2 CALL s.p): the part is the routine.
116            if (database == null) {
117                database = schema;
118            }
119            schema = object;
120            object = part;
121        }
122        if (object == null || object.toString().isEmpty()) {
123            return null;
124        }
125        List<Object> levels = new ArrayList<Object>(5);
126        boolean paren = false;
127        for (TSourceToken qualifier : new TSourceToken[] {server, database, schema, pkg}) {
128            if (qualifier != null) {
129                levels.add(qualifierLevel(qualifier.toString(), service));
130                paren |= qualifier.toString().indexOf('(') >= 0;
131            }
132        }
133        levels.add(routineKey(object.toString(), service));
134        paren |= object.toString().indexOf('(') >= 0;
135        CanonKey dblink = null;
136        if (name.getDblink() != null && name.getDblink().toString() != null && !name.getDblink().toString().isEmpty()) {
137            dblink = service.canonKeyOf(name.getDblink().toString(), ESQLDataObjectType.dotDblink,
138                    IdentifierInputForm.SQL_LEXICAL);
139            paren |= name.getDblink().toString().indexOf('(') >= 0;
140        }
141        return new RoutineAliasKey(levels, dblink, signature, storedSignature(signature, service), paren,
142                qualifierKey(object.toString(), service));
143    }
144
145    /**
146     * The key of a name whose levels are already separate identifiers as written: a model's database and schema
147     * fields and its routine name. Never a way to split a rendered, dotted name; an empty level is the empty
148     * qualifier of {@code db..p}.
149     */
150    public static RoutineAliasKey ofWrittenLevels(List<String> qualifiers, String routine, String signature,
151            IdentifierService service) {
152        if (routine == null || routine.isEmpty() || service == null) {
153            return null;
154        }
155        List<Object> levels = new ArrayList<Object>();
156        boolean paren = routine.indexOf('(') >= 0;
157        if (qualifiers != null) {
158            for (String qualifier : qualifiers) {
159                levels.add(qualifierLevel(qualifier, service));
160                paren |= qualifier != null && qualifier.indexOf('(') >= 0;
161            }
162        }
163        levels.add(routineKey(routine, service));
164        return new RoutineAliasKey(levels, null, signature, storedSignature(signature, service), paren,
165                qualifierKey(routine, service));
166    }
167
168    /**
169     * The key of a routine name given as legacy normal-name text ({@code SCHEMA.NAME(suffix)}), for the deprecated
170     * text-keyed API of {@code ModelBindingManager} only. That text is already a stored spelling (the legacy maps
171     * compared it as stored text: {@code foo} there is not {@code FOO}), so its levels are keyed as stored names: the text
172     * before its last {@code (} (when it ends with {@code )}) is split into levels, and the rest is the alias suffix.
173     * Null when it names nothing.
174     */
175    public static RoutineAliasKey ofLegacyText(String text) {
176        return ofLegacyText(text, service());
177    }
178
179    /** {@link #ofLegacyText(String)} under the given analysis's identifier rules. */
180    public static RoutineAliasKey ofLegacyText(String text, IdentifierService service) {
181        if (text == null || text.isEmpty() || service == null) {
182            return null;
183        }
184        String name = text;
185        String signature = null;
186        int cut = text.lastIndexOf('(');
187        if (text.endsWith(")") && cut > 0) {
188            name = text.substring(0, cut);
189            signature = text.substring(cut);
190        }
191        try {
192            List<String> segments = SQLUtil.parseNames(name);
193            if (segments == null || segments.isEmpty()) {
194                return null;
195            }
196            List<Object> levels = new ArrayList<Object>(segments.size());
197            boolean paren = false;
198            for (int i = 0; i < segments.size(); i++) {
199                String segment = segments.get(i);
200                if (segment == null || segment.isEmpty()) {
201                    return null;
202                }
203                levels.add(service.canonKeyOf(segment, i == segments.size() - 1 ? ROUTINE_TYPE : QUALIFIER_TYPE,
204                        IdentifierInputForm.CATALOG_STORED));
205                paren |= segment.indexOf('(') >= 0;
206            }
207            String routine = segments.get(segments.size() - 1);
208            return new RoutineAliasKey(levels, null, signature, storedSignature(signature, service), paren,
209                    service.canonKeyOf(routine, QUALIFIER_TYPE, IdentifierInputForm.CATALOG_STORED));
210        } catch (RuntimeException unkeyable) {
211            return null;
212        }
213    }
214
215    /**
216     * A qualifier level. The empty one of {@code db..p} is SQL Server's default schema there, as the legacy normalizer
217     * read it (the session's schema, else {@code dbo}); elsewhere it stays an empty level.
218     */
219    private static Object qualifierLevel(String text, IdentifierService service) {
220        if (text != null && !text.isEmpty()) {
221            return qualifierKey(text, service);
222        }
223        EDbVendor vendor = service.getProfile() == null ? null : service.getProfile().getVendor();
224        if (vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql) {
225            String schema = ModelBindingManager.getGlobalSchema();
226            return qualifierKey(schema != null ? schema : "dbo", service);
227        }
228        return EMPTY_LEVEL;
229    }
230
231    static CanonKey qualifierKey(String text, IdentifierService service) {
232        return service.canonKeyOf(text, QUALIFIER_TYPE, IdentifierInputForm.SQL_LEXICAL);
233    }
234
235    static CanonKey routineKey(String text, IdentifierService service) {
236        return service.canonKeyOf(text, ROUTINE_TYPE, IdentifierInputForm.SQL_LEXICAL);
237    }
238
239    /**
240     * The alias suffix as the legacy leaf comparison saw it: part of one stored routine name, compared by the
241     * routine rules (so {@code (MyType)} and {@code (mytype)} are one suffix where routine names ignore case).
242     */
243    private static CanonKey storedSignature(String signature, IdentifierService service) {
244        if (signature == null || signature.isEmpty()) {
245            return null;
246        }
247        return service.canonKeyOf(signature, ROUTINE_TYPE, IdentifierInputForm.CATALOG_STORED);
248    }
249
250    /**
251     * The key of a model's display name ({@code Procedure}, {@code OraclePackage}), built beside the composition that
252     * renders it, branch for branch: the composition appends written segments and filled-in database or schema
253     * values, and this appends the matching levels. A written segment's level is the written name's own level when the
254     * name's levels line up with the composition's segments; a filled-in value is keyed as one identifier.
255     */
256    public static final class Display {
257        private final RoutineAliasKey written;
258        private final List<String> segments;
259        private final IdentifierService service;
260        private final List<Object> levels = new ArrayList<Object>();
261        private boolean paren;
262        private boolean failed;
263
264        public Display(TObjectName name, List<String> segments) {
265            this.service = service();
266            RoutineAliasKey key = null;
267            try {
268                key = written(name, null, service);
269            } catch (RuntimeException failure) {
270                failed = true;
271            }
272            this.written = key != null && segments != null && key.size() == segments.size() ? key : null;
273            this.segments = segments;
274            if (service == null) {
275                failed = true;
276            }
277        }
278
279        /** The composition appended written segment {@code index} as a qualifier. */
280        public void segment(int index) {
281            if (failed) {
282                return;
283            }
284            try {
285                levels.add(written != null ? written.levels.get(index) : qualifierLevel(segments.get(index), service));
286                paren |= segments.get(index) != null && segments.get(index).indexOf('(') >= 0;
287            } catch (RuntimeException failure) {
288                failed = true;
289            }
290        }
291
292        /** The composition appended a filled-in database or schema value. */
293        public void field(String value) {
294            if (failed) {
295                return;
296            }
297            try {
298                levels.add(qualifierLevel(value, service));
299                paren |= value != null && value.indexOf('(') >= 0;
300            } catch (RuntimeException failure) {
301                failed = true;
302            }
303        }
304
305        /** The composition appended the routine's own (last) segment: the key of the display name, or null. */
306        public RoutineAliasKey finish() {
307            if (failed) {
308                return null;
309            }
310            try {
311                if (segments == null || segments.isEmpty()) {
312                    return null;
313                }
314                String last = segments.get(segments.size() - 1);
315                paren |= last != null && last.indexOf('(') >= 0;
316                if (written != null) {
317                    levels.add(written.getRoutineKey());
318                    return new RoutineAliasKey(levels, written.dblink, null, null, paren, written.routineAsQualifier);
319                }
320                levels.add(routineKey(last, service));
321                return new RoutineAliasKey(levels, null, null, null, paren, qualifierKey(last, service));
322            } catch (RuntimeException failure) {
323                return null;
324            }
325        }
326    }
327
328    /** This key with {@code qualifiers}' levels in front, for a member of the package {@code qualifiers} names. */
329    public RoutineAliasKey qualifiedBy(RoutineAliasKey qualifiers) {
330        if (qualifiers == null) {
331            return this;
332        }
333        List<Object> joined = new ArrayList<Object>(qualifiers.levels.size() + levels.size());
334        joined.addAll(qualifiers.levels.subList(0, qualifiers.levels.size() - 1));
335        joined.add(qualifiers.routineAsQualifier);
336        joined.addAll(levels);
337        return new RoutineAliasKey(joined, dblink, signature, storedSignature, parenInName || qualifiers.parenInName,
338                routineAsQualifier);
339    }
340
341    /** This key's levels with another alias suffix. */
342    public RoutineAliasKey withSignature(String otherSignature) {
343        return withSignature(otherSignature, service());
344    }
345
346    public RoutineAliasKey withSignature(String otherSignature, IdentifierService service) {
347        if (otherSignature == null ? signature == null : otherSignature.equals(signature)) {
348            return this;
349        }
350        return new RoutineAliasKey(new ArrayList<Object>(levels), dblink, otherSignature,
351                service == null ? null : storedSignature(otherSignature, service), parenInName, routineAsQualifier);
352    }
353
354    /** This key without an alias suffix: the name alone. */
355    public RoutineAliasKey nameOnly() {
356        return signature == null ? this
357                : new RoutineAliasKey(new ArrayList<Object>(levels), dblink, null, null, parenInName, routineAsQualifier);
358    }
359
360    public int size() {
361        return levels.size();
362    }
363
364    public boolean isQualified() {
365        return levels.size() > 1;
366    }
367
368    public boolean hasSignature() {
369        return signature != null;
370    }
371
372    /** Whether the legacy lookup took this key's text for a name with a suffix: it has one, or a {@code (} as written. */
373    public boolean looksSuffixed() {
374        return signature != null || parenInName;
375    }
376
377    /**
378     * This key with {@code qualifier} as written in front, a single identifier (a model's schema field); for a lookup
379     * the legacy made by prefixing that text.
380     */
381    public RoutineAliasKey withQualifier(String qualifier) {
382        return withQualifier(qualifier, service());
383    }
384
385    public RoutineAliasKey withQualifier(String qualifier, IdentifierService service) {
386        if (qualifier == null || service == null) {
387            return this;
388        }
389        List<Object> joined = new ArrayList<Object>(levels.size() + 1);
390        joined.add(qualifierLevel(qualifier, service));
391        joined.addAll(levels);
392        return new RoutineAliasKey(joined, dblink, signature, storedSignature,
393                parenInName || qualifier.indexOf('(') >= 0, routineAsQualifier);
394    }
395
396    /** The alias suffix as raw text, or null. */
397    public String getSignature() {
398        return signature;
399    }
400
401    /** The routine level's key. */
402    public CanonKey getRoutineKey() {
403        return (CanonKey) levels.get(levels.size() - 1);
404    }
405
406    /** The name's levels (a {@code CanonKey}, or the empty level), outermost first. */
407    public List<Object> getLevels() {
408        return levels;
409    }
410
411    boolean sameName(RoutineAliasKey other) {
412        return levels.equals(other.levels) && sameLink(other);
413    }
414
415    boolean sameRoutine(RoutineAliasKey other) {
416        return getRoutineKey().equals(other.getRoutineKey()) && sameLink(other);
417    }
418
419    private boolean sameLink(RoutineAliasKey other) {
420        return dblink == null ? other.dblink == null : dblink.equals(other.dblink);
421    }
422
423    /** The legacy {@code startsWith} of the full key text, once the names are equal: the suffix as raw text. */
424    boolean signatureStartsWith(RoutineAliasKey other) {
425        return signature != null && other.signature != null && signature.startsWith(other.signature);
426    }
427
428    /** The legacy leaf comparison's suffix part: both suffixes, as stored routine-name text. */
429    boolean sameStoredSignature(RoutineAliasKey other) {
430        if (signature == null || other.signature == null) {
431            return signature == null && other.signature == null;
432        }
433        if (storedSignature == null || other.storedSignature == null) {
434            return signature.equals(other.signature);
435        }
436        return storedSignature.equals(other.storedSignature);
437    }
438
439    @Override
440    public boolean equals(Object o) {
441        if (this == o) {
442            return true;
443        }
444        if (!(o instanceof RoutineAliasKey)) {
445            return false;
446        }
447        RoutineAliasKey that = (RoutineAliasKey) o;
448        return hash == that.hash && levels.equals(that.levels) && sameLink(that)
449                && (signature == null ? that.signature == null : signature.equals(that.signature));
450    }
451
452    @Override
453    public int hashCode() {
454        return hash;
455    }
456
457    /** For diagnostics only: never compare or key by this text. */
458    @Override
459    public String toString() {
460        StringBuilder sb = new StringBuilder();
461        for (int i = 0; i < levels.size(); i++) {
462            if (i > 0) {
463                sb.append('.');
464            }
465            Object level = levels.get(i);
466            sb.append(level instanceof CanonKey ? ((CanonKey) level).getCanonText() : String.valueOf(level));
467        }
468        if (dblink != null) {
469            sb.append('@').append(dblink.getCanonText());
470        }
471        if (signature != null) {
472            sb.append(signature);
473        }
474        return sb.toString();
475    }
476}