001package gudusoft.gsqlparser.dlineage.routine;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.TSourceToken;
005import gudusoft.gsqlparser.catalog.runtime.CatalogHierarchy;
006import gudusoft.gsqlparser.catalog.runtime.CatalogIdentifierPolicy;
007import gudusoft.gsqlparser.catalog.runtime.CatalogLookupName;
008import gudusoft.gsqlparser.catalog.runtime.CatalogObjectKind;
009import gudusoft.gsqlparser.catalog.runtime.CatalogParentConstraint;
010import gudusoft.gsqlparser.nodes.TObjectName;
011import gudusoft.gsqlparser.sqlenv.CanonKey;
012import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
013import gudusoft.gsqlparser.sqlenv.IdentifierComponentRole;
014import gudusoft.gsqlparser.sqlenv.IdentifierInputForm;
015import gudusoft.gsqlparser.sqlenv.IdentifierRolePolicy;
016import gudusoft.gsqlparser.sqlenv.IdentifierService;
017import gudusoft.gsqlparser.sqlenv.TSQLEnv;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.List;
022
023/**
024 * A routine name as written, at a declaration or a call: one interpretation of it as a
025 * {@link CatalogLookupName}, with the comparison keys of its named levels.
026 *
027 * <p>A routine is declared and called by a partial name ({@code f}, {@code s.f}, {@code pkg.f},
028 * {@code db..p}), which is a lookup, not an identity: the levels it leaves out are OMITTED and only
029 * the caller's context (session defaults, search path) could supply them. D3b compares written names;
030 * {@link RoutineRegistry} never reports a match that relies on an OMITTED level as resolved.</p>
031 *
032 * <p>The levels come from the roles the parser gave the name's tokens (server, database, schema,
033 * package, object). Each named level's key is minted by the analysis {@link IdentifierService} from
034 * its token as written (SQL_LEXICAL), or as stored for a name taken from a model, under the comparison
035 * type of its role, or of its kind for an object level
036 * ({@link IdentifierRolePolicy#comparisonTypeOf}, {@link CatalogIdentifierPolicy#comparisonTypeOfKind}).
037 * The terminal's key depends on the kind it is compared as, so it is minted per kind
038 * ({@link #terminalKey}).</p>
039 */
040public final class RoutineName {
041
042    /** One level of the name: its role, whether it was named, and its key when named. */
043    public static final class Level {
044        final IdentifierComponentRole role;
045        final CatalogLookupName.SlotState state;
046        final String text;
047        final CanonKey key;
048
049        Level(IdentifierComponentRole role, CatalogLookupName.SlotState state, String text, CanonKey key) {
050            this.role = role;
051            this.state = state;
052            this.text = text;
053            this.key = key;
054        }
055
056        public IdentifierComponentRole getRole() {
057            return role;
058        }
059
060        public CatalogLookupName.SlotState getState() {
061            return state;
062        }
063
064        public String getText() {
065            return text;
066        }
067
068        /** Null unless the level was named. */
069        public CanonKey getKey() {
070            return key;
071        }
072    }
073
074    private final CatalogLookupName lookupName;
075    /** The container levels, outermost first; a package is one, with role OBJECT. */
076    private final List<Level> containers;
077    private final String terminalText;
078    private final IdentifierInputForm terminalForm;
079    private final CatalogObjectKind kind;
080    private final IdentifierService service;
081    private final boolean packageMember;
082
083    private RoutineName(CatalogLookupName lookupName, List<Level> containers, String terminalText,
084            IdentifierInputForm terminalForm, CatalogObjectKind kind, IdentifierService service,
085            boolean packageMember) {
086        this.lookupName = lookupName;
087        this.containers = Collections.unmodifiableList(containers);
088        this.terminalText = terminalText;
089        this.terminalForm = terminalForm;
090        this.kind = kind;
091        this.service = service;
092        this.packageMember = packageMember;
093    }
094
095    /** The request, as the catalog layer states it. */
096    public CatalogLookupName getLookupName() {
097        return lookupName;
098    }
099
100    public List<Level> getContainers() {
101        return containers;
102    }
103
104    public String getTerminalText() {
105        return terminalText;
106    }
107
108    public CatalogObjectKind getKind() {
109        return kind;
110    }
111
112    /** Whether this interpretation names a member of a package. */
113    public boolean isPackageMember() {
114        return packageMember;
115    }
116
117    /** Whether any container level is named. */
118    public boolean isQualified() {
119        for (Level level : containers) {
120            if (level.state == CatalogLookupName.SlotState.PRESENT) {
121                return true;
122            }
123        }
124        return false;
125    }
126
127    /** The terminal's key when compared as a routine of {@code asKind}. */
128    public CanonKey terminalKey(CatalogObjectKind asKind) {
129        return service.canonKeyOf(terminalText, CatalogIdentifierPolicy.comparisonTypeOfKind(asKind), terminalForm);
130    }
131
132    /**
133     * The name a routine is declared with: a member of the enclosing package when it is declared inside one
134     * unqualified, else the name as written (a qualifier of a standalone declaration is its schema). Null
135     * when the name has no shape the catalog layer can state.
136     */
137    public static RoutineName declared(TObjectName name, CatalogObjectKind kind, EDbVendor vendor,
138            IdentifierService service, TObjectName enclosingPackage) {
139        TSourceToken[] t = tokensOf(name);
140        if (t == null) {
141            return null;
142        }
143        TSourceToken server = t[0], database = t[1], schema = t[2], pkg = t[3], object = t[4];
144        if (enclosingPackage != null && server == null && database == null && schema == null && pkg == null) {
145            return buildInPackage(vendor, service, kind, enclosingPackage, object);
146        }
147        return build(vendor, service, kind, server, database, schema, pkg, object, pkg != null);
148    }
149
150    /** The interpretations of a called name: one, or two for an Oracle name {@code x.f} or a call inside a package. */
151    public static List<RoutineName> interpretations(TObjectName name, CatalogObjectKind kind, EDbVendor vendor,
152            IdentifierService service, TObjectName enclosingPackage) {
153        List<RoutineName> out = new ArrayList<RoutineName>(2);
154        TSourceToken[] t = tokensOf(name);
155        if (t == null) {
156            return out;
157        }
158        TSourceToken server = t[0], database = t[1], schema = t[2], pkg = t[3], object = t[4];
159        if (vendor == EDbVendor.dbvoracle && pkg == null && database == null && schema != null) {
160            // Oracle `x.f`: x is a schema or a package; which one only the catalog or the scope decides.
161            add(out, build(vendor, service, kind, server, null, schema, null, object, false));
162            add(out, build(vendor, service, kind, server, null, null, schema, object, true));
163            return out;
164        }
165        if (pkg == null && enclosingPackage != null && schema == null && database == null && server == null) {
166            // An unqualified name inside a package: a member of that package, or a routine of its schema.
167            add(out, buildInPackage(vendor, service, kind, enclosingPackage, object));
168        }
169        add(out, build(vendor, service, kind, server, database, schema, pkg, object, pkg != null));
170        return out;
171    }
172
173    /**
174     * The name's tokens by role: server, database, schema, package, routine; null when it names no routine.
175     * A routine name has no column part, so a name the parser read as {@code object.part} (DB2's CALL:
176     * {@code s.p} as object {@code s}, part {@code p}) is a qualifier and a routine: each role moves up one.
177     */
178    private static TSourceToken[] tokensOf(TObjectName name) {
179        if (name == null) {
180            return null;
181        }
182        TSourceToken server = named(name.getServerToken());
183        TSourceToken database = named(name.getDatabaseToken());
184        TSourceToken schema = named(name.getSchemaToken());
185        TSourceToken pkg = named(name.getPackageToken());
186        TSourceToken object = named(name.getObjectToken());
187        TSourceToken part = named(name.getPartToken());
188        if (part != null) {
189            if (database == null) {
190                database = schema;
191            }
192            schema = object;
193            object = part;
194        }
195        if (object == null) {
196            return null;
197        }
198        return new TSourceToken[] {server, database, schema, pkg, object};
199    }
200
201    /** A token that names a level: null for none, and for the empty level of {@code db..p}, which is omitted. */
202    private static TSourceToken named(TSourceToken token) {
203        return token == null || token.toString().isEmpty() ? null : token;
204    }
205
206    private static void add(List<RoutineName> out, RoutineName name) {
207        if (name != null) {
208            out.add(name);
209        }
210    }
211
212    private static RoutineName buildInPackage(EDbVendor vendor, IdentifierService service, CatalogObjectKind kind,
213            TObjectName enclosingPackage, TSourceToken member) {
214        if (enclosingPackage.getObjectToken() == null) {
215            return null;
216        }
217        return build(vendor, service, kind, named(enclosingPackage.getServerToken()),
218                named(enclosingPackage.getDatabaseToken()), named(enclosingPackage.getSchemaToken()),
219                enclosingPackage.getObjectToken(), member, true);
220    }
221
222    private static RoutineName build(EDbVendor vendor, IdentifierService service, CatalogObjectKind kind,
223            TSourceToken server, TSourceToken database, TSourceToken schema, TSourceToken pkg, TSourceToken object,
224            boolean packageMember) {
225        CatalogHierarchy hierarchy = hierarchyOf(vendor);
226        CatalogLookupName.Builder builder = CatalogLookupName.builder(hierarchy);
227        List<Level> containers = new ArrayList<Level>();
228        IdentifierInputForm form = IdentifierInputForm.SQL_LEXICAL;
229        TSourceToken catalogToken = database;
230        TSourceToken schemaToken = schema;
231        if (catalogToken != null && catalogToken == schemaToken) {
232            // One written qualifier the parser gave both roles (Oracle `s.pkg.f`).
233            catalogToken = null;
234        }
235        if (!hierarchy.hasSchema() && schemaToken != null) {
236            // A vendor without schemas (MySQL, Teradata, Hive, Impala): the parser's schema token is its
237            // one container level, the database.
238            if (catalogToken != null) {
239                return null;
240            }
241            catalogToken = schemaToken;
242            schemaToken = null;
243        }
244        if (server != null) {
245            builder.present(IdentifierComponentRole.SERVER, server.toString(), form);
246            containers.add(level(service, IdentifierComponentRole.SERVER, server));
247        }
248        if (hierarchy.hasCatalog()) {
249            if (catalogToken != null) {
250                builder.present(IdentifierComponentRole.CATALOG, catalogToken.toString(), form);
251                containers.add(level(service, IdentifierComponentRole.CATALOG, catalogToken));
252            } else {
253                builder.omitted(IdentifierComponentRole.CATALOG);
254                containers.add(new Level(IdentifierComponentRole.CATALOG, CatalogLookupName.SlotState.OMITTED, null, null));
255            }
256        } else if (catalogToken != null) {
257            return null;
258        }
259        if (hierarchy.hasSchema()) {
260            if (schemaToken != null) {
261                builder.present(IdentifierComponentRole.SCHEMA, schemaToken.toString(), form);
262                containers.add(level(service, IdentifierComponentRole.SCHEMA, schemaToken));
263            } else {
264                builder.omitted(IdentifierComponentRole.SCHEMA);
265                containers.add(new Level(IdentifierComponentRole.SCHEMA, CatalogLookupName.SlotState.OMITTED, null, null));
266            }
267        }
268        if (pkg != null) {
269            builder.parentConstraint(CatalogParentConstraint.ROUTINE_CONTAINER);
270            builder.containerObject(pkg.toString(), form);
271            containers.add(new Level(IdentifierComponentRole.OBJECT, CatalogLookupName.SlotState.PRESENT,
272                    pkg.toString(), service.canonKeyOf(pkg.toString(),
273                            CatalogIdentifierPolicy.comparisonTypeOfKind(CatalogObjectKind.PACKAGE), form)));
274        }
275        builder.object(kind, object.toString(), form);
276        CatalogLookupName lookupName;
277        try {
278            lookupName = builder.build(kind);
279        } catch (IllegalArgumentException unrepresentable) {
280            return null;
281        }
282        return new RoutineName(lookupName, containers, object.toString(), form, kind, service, packageMember);
283    }
284
285    private static Level level(IdentifierService service, IdentifierComponentRole role, TSourceToken token) {
286        ESQLDataObjectType type = IdentifierRolePolicy.comparisonTypeOf(role);
287        return new Level(role, CatalogLookupName.SlotState.PRESENT, token.toString(),
288                service.canonKeyOf(token.toString(), type, IdentifierInputForm.SQL_LEXICAL));
289    }
290
291    /** The namespace topology of a vendor: whether it has catalogs and schemas. */
292    static CatalogHierarchy hierarchyOf(EDbVendor vendor) {
293        boolean catalog = TSQLEnv.supportCatalog(vendor);
294        boolean schema = TSQLEnv.supportSchema(vendor);
295        if (catalog && schema) {
296            return CatalogHierarchy.CATALOG_SCHEMA;
297        }
298        if (catalog) {
299            return CatalogHierarchy.CATALOG_ONLY;
300        }
301        if (schema) {
302            return CatalogHierarchy.SCHEMA_ONLY;
303        }
304        return CatalogHierarchy.ROOT_ONLY;
305    }
306
307    @Override
308    public String toString() {
309        StringBuilder b = new StringBuilder();
310        for (Level level : containers) {
311            b.append(level.state == CatalogLookupName.SlotState.PRESENT ? level.text : "<" + level.role + ">").append('.');
312        }
313        return b.append(terminalText).toString();
314    }
315}