001package gudusoft.gsqlparser.catalog.runtime;
002
003import gudusoft.gsqlparser.sqlenv.CanonKey;
004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
005import gudusoft.gsqlparser.sqlenv.IdentifierComponentRole;
006import gudusoft.gsqlparser.sqlenv.IdentifierInputForm;
007import gudusoft.gsqlparser.sqlenv.IdentifierRolePolicy;
008import gudusoft.gsqlparser.sqlenv.IdentifierService;
009
010import java.util.ArrayList;
011import java.util.Collections;
012import java.util.List;
013
014/**
015 * Identity of one named catalog object, built from EXPLICIT semantic roles (stage S0R of
016 * docs/refactor/identifier_identity_display_separation_plan.md).
017 *
018 * <h3>What this replaces and why</h3>
019 * The deleted {@code QualifiedCanonKey} inferred each component's role from how many components the
020 * name had. That is a guess, and it is wrong in both directions: it read BigQuery
021 * {@code customer.name} as {@code table.column} when the repository's own {@code FieldPath} and
022 * {@code TestBigQueryNestedColumns} say it is base column plus STRUCT field — and BigQuery folds
023 * table names case-sensitively while folding column names case-insensitively, so the guess changed
024 * the ANSWER, not merely the label. It also had no server level at all, so SQL Server's
025 * {@code srv.db.dbo.t} had its server compared as a catalog.
026 *
027 * <p>Here nothing is inferred. Each component carries its role; each OBJECT component carries its
028 * entity kind; the sequence is validated by {@link CatalogNameGrammar} at construction; and the
029 * comparison rules for each component come from the one mapping that owns them
030 * ({@link IdentifierRolePolicy} for role-decided components,
031 * {@link CatalogIdentifierPolicy#comparisonTypeOfKind} for objects).
032 *
033 * <h3>Equality</h3>
034 * Two keys are equal iff they have the same terminal kind, the same number of components, and
035 * component-wise equal role, kind and {@link CanonKey} — plus an equal database link. So a table
036 * named {@code x} never equals a view named {@code x}, and {@code db.s.t} never equals a name whose
037 * first component is a server.
038 *
039 * <h3>In-process only</h3>
040 * It contains {@link CanonKey}s, whose collation payloads are stable only within one JVM and
041 * collator configuration. Do NOT stringify it to produce a {@code CatalogObjectId}: that type
042 * promises cross-snapshot stability, which this cannot give. Use {@code PersistentIdentifierKey}
043 * for anything persisted, and never {@code CanonKey.getCanonText()} or this type's
044 * {@code toString()}.
045 */
046public final class CatalogObjectKey {
047
048    /** One component: its role, its entity kind when it is an object, and its canonical key. */
049    public static final class Component {
050        private final IdentifierComponentRole role;
051        private final CatalogObjectKind kind;
052        private final CanonKey key;
053
054        Component(IdentifierComponentRole role, CatalogObjectKind kind, CanonKey key) {
055            this.role = role;
056            this.kind = kind;
057            this.key = key;
058        }
059
060        public IdentifierComponentRole getRole() {
061            return role;
062        }
063
064        /** The entity kind, non-null exactly when the role is {@link IdentifierComponentRole#OBJECT}. */
065        public CatalogObjectKind getKind() {
066            return kind;
067        }
068
069        public CanonKey getKey() {
070            return key;
071        }
072
073        @Override
074        public boolean equals(Object o) {
075            if (this == o) {
076                return true;
077            }
078            if (o == null || getClass() != o.getClass()) {
079                return false;
080            }
081            Component that = (Component) o;
082            return role == that.role && kind == that.kind && key.equals(that.key);
083        }
084
085        @Override
086        public int hashCode() {
087            int h = role.hashCode();
088            h = 31 * h + (kind == null ? 0 : kind.hashCode());
089            return 31 * h + key.hashCode();
090        }
091
092        @Override
093        public String toString() {
094            return role + (kind == null ? "" : "(" + kind + ")") + "=" + key;
095        }
096    }
097
098    private final List<Component> components;
099    private final CatalogObjectKind terminalKind;
100    private final CatalogHierarchy hierarchy;
101    private final CanonKey databaseLink;
102    private final int hash;
103
104    private CatalogObjectKey(List<Component> components, CatalogObjectKind terminalKind,
105                             CatalogHierarchy hierarchy, CanonKey databaseLink) {
106        this.components = Collections.unmodifiableList(new ArrayList<Component>(components));
107        this.terminalKind = terminalKind;
108        this.hierarchy = hierarchy;
109        this.databaseLink = databaseLink;
110        int h = this.components.hashCode();
111        h = 31 * h + terminalKind.hashCode();
112        h = 31 * h + hierarchy.hashCode();
113        this.hash = 31 * h + (databaseLink == null ? 0 : databaseLink.hashCode());
114    }
115
116    // ------------------------------------------------------------------ builder
117
118    /**
119     * Assembles a key one component at a time. <strong>Deliberately not public.</strong> It is the
120     * widest and least compiler-guided construction path, so while the grammar is still growing it
121     * would be the way an unexamined shape gets built. Callers use the named factories below, which
122     * state the topology and the parent relationship. If production later proves a general entry
123     * point necessary, it should take an explicit layout and run the full grammar.
124     */
125    static Builder builder(IdentifierService service, CatalogHierarchy hierarchy) {
126        return new Builder(service, hierarchy);
127    }
128
129    /** @see #builder(IdentifierService, CatalogHierarchy) */
130    static final class Builder {
131        private final IdentifierService service;
132        private final CatalogHierarchy hierarchy;
133        private final List<IdentifierComponentRole> roles = new ArrayList<IdentifierComponentRole>();
134        private final List<CatalogObjectKind> kinds = new ArrayList<CatalogObjectKind>();
135        private final List<CanonKey> keys = new ArrayList<CanonKey>();
136        private CanonKey databaseLink;
137        private boolean databaseLinkInherited;
138
139        private Builder(IdentifierService service, CatalogHierarchy hierarchy) {
140            if (service == null) {
141                throw new IllegalArgumentException("an identifier service is required: it carries "
142                        + "the vendor profile the component keys are computed under");
143            }
144            if (hierarchy == null) {
145                throw new IllegalArgumentException("the namespace topology must be declared");
146            }
147            this.service = service;
148            this.hierarchy = hierarchy;
149        }
150
151        /** A container component — server, catalog or schema. */
152        public Builder container(IdentifierComponentRole role, String text, IdentifierInputForm form) {
153            if (role == IdentifierComponentRole.OBJECT) {
154                throw new IllegalArgumentException("use object(kind, text, form) for an object");
155            }
156            ESQLDataObjectType type = IdentifierRolePolicy.comparisonTypeOf(role);
157            return add(role, null, requireKey(text, type, form, role.toString()));
158        }
159
160        /** A named object component, with the entity kind that decides how its name folds. */
161        public Builder object(CatalogObjectKind kind, String text, IdentifierInputForm form) {
162            ESQLDataObjectType type = CatalogIdentifierPolicy.comparisonTypeOfKind(kind);
163            return add(IdentifierComponentRole.OBJECT, kind,
164                    requireKey(text, type, form, "OBJECT(" + kind + ")"));
165        }
166
167        /** The base column of a relation. */
168        public Builder baseColumn(String text, IdentifierInputForm form) {
169            ESQLDataObjectType type =
170                    IdentifierRolePolicy.comparisonTypeOf(IdentifierComponentRole.BASE_COLUMN);
171            return add(IdentifierComponentRole.BASE_COLUMN, null,
172                    requireKey(text, type, form, "BASE_COLUMN"));
173        }
174
175        /**
176         * An Oracle-style database link. Stored out of line because it qualifies the whole name
177         * rather than occupying a position in the hierarchy — but it does participate in equality,
178         * since {@code t@remote} and {@code t} are different objects.
179         */
180        public Builder databaseLink(String text, IdentifierInputForm form) {
181            if (databaseLink != null) {
182                throw new IllegalArgumentException(databaseLinkInherited
183                        ? "this identity already carries its parent's database link; a child of a "
184                        + "remote object is part of the same remote identity and may not change it"
185                        : "a name has one database link");
186            }
187            ESQLDataObjectType type =
188                    IdentifierRolePolicy.comparisonTypeOf(IdentifierComponentRole.DATABASE_LINK);
189            this.databaseLink = requireKey(text, type, form, "DATABASE_LINK");
190            return this;
191        }
192
193        private Builder add(IdentifierComponentRole role, CatalogObjectKind kind, CanonKey key) {
194            roles.add(role);
195            kinds.add(kind);
196            keys.add(key);
197            return this;
198        }
199
200        private CanonKey requireKey(String text, ESQLDataObjectType type, IdentifierInputForm form,
201                                    String what) {
202            if (text == null || text.isEmpty()) {
203                throw new IllegalArgumentException(what + " component has no text; an absent level "
204                        + "is not an empty name — build a CatalogLookupName instead");
205            }
206            CanonKey key = service.canonKeyOf(text, type, form);
207            if (key == null) {
208                throw new IllegalStateException("no canonical key for " + what + " text " + text);
209            }
210            return key;
211        }
212
213        /**
214         * Validates the whole sequence and builds the key.
215         *
216         * @param terminalKind what the name as a whole denotes; must agree with the last component
217         */
218        CatalogObjectKey build(CatalogObjectKind terminalKind) {
219            CatalogNameGrammar.validateResolvedIdentity(roles, kinds, terminalKind, hierarchy);
220            if (databaseLink != null && !databaseLinkInherited) {
221                // Only a DIRECTLY declared link is checked against the shapes a remote reference is
222                // known to take. An inherited one needs no re-check and must not be re-judged: a
223                // column of a remote table is perfectly reachable as `t@remote.c`, and rejecting it
224                // would mean a remote relation could have no remote columns.
225                CatalogNameGrammar.requireLinkableTerminal(terminalKind);
226            }
227            List<Component> components = new ArrayList<Component>(roles.size());
228            for (int i = 0; i < roles.size(); i++) {
229                components.add(new Component(roles.get(i), kinds.get(i), keys.get(i)));
230            }
231            requireCompatibleRuleDomains(keys, databaseLink);
232            return new CatalogObjectKey(components, terminalKind, hierarchy, databaseLink);
233        }
234    }
235
236    /**
237     * Every component of one identity must be composable with the others.
238     *
239     * <p>This is what stops an Oracle parent from being composed with a MySQL-canonicalized column:
240     * such a key can never equal the correctly built one, and the lookup misses silently. Getting
241     * the strictness right took two corrections, both of which were false NEGATIVES of their own:
242     *
243     * <ul>
244     *   <li>comparing the producing SERVICES split two genuinely equal identities apart, because two
245     *       services for one profile that differ only in carrying a {@code CollatorProvider} produce
246     *       identical fold-based keys;</li>
247     *   <li>requiring every component to use the same fold-versus-collation MECHANISM split a valid
248     *       identity apart at its column, because one profile may fold its name group and collate
249     *       its column group.</li>
250     * </ul>
251     *
252     * So the rule is: one rule policy everywhere, and one collation domain among the components that
253     * collate at all.
254     */
255    private static void requireCompatibleRuleDomains(List<CanonKey> keys, CanonKey databaseLink) {
256        List<CanonKey> all = new ArrayList<CanonKey>(keys);
257        if (databaseLink != null) {
258            all.add(databaseLink);
259        }
260        CanonKey first = all.get(0);
261        String collatorDomain = first.getCollatorDomainId();
262        for (int i = 1; i < all.size(); i++) {
263            CanonKey key = all.get(i);
264            // The POLICY must match everywhere, and policy equality is transitive, so comparing
265            // against the first component is sufficient for this half.
266            if (!first.sameRulePolicyAs(key)) {
267                throw new IllegalArgumentException("component " + i + " was canonicalized under "
268                        + key.describeRuleDomain() + " but component 0 under "
269                        + first.describeRuleDomain()
270                        + ": a key mixing rule policies could never equal the correctly built one, "
271                        + "and the lookup would miss silently");
272            }
273            // The COLLATOR half is not transitive: a fold-based key is compatible with collator A
274            // and with collator B while A and B are incompatible, so a loop against component 0
275            // would accept [fold, A, B]. Accumulate the first collation domain seen and require
276            // every other collating component to agree with it.
277            String keyDomain = key.getCollatorDomainId();
278            if (keyDomain == null) {
279                continue;
280            }
281            if (collatorDomain == null) {
282                collatorDomain = keyDomain;
283            } else if (!collatorDomain.equals(keyDomain)) {
284                throw new IllegalArgumentException("component " + i + " collates under " + keyDomain
285                        + " but another component collates under " + collatorDomain
286                        + ": two collators produce incomparable payloads");
287            }
288        }
289    }
290
291    // ------------------------------------------------- parent-plus-local factories
292
293    /**
294     * A catalog, optionally under a server. Parts are passed SEPARATELY — a caller holding them
295     * apart must not join them, because joining and re-splitting is how boundary information is
296     * lost.
297     *
298     * <p>A catalog needs nothing above it: a name without a server means the local server, which is
299     * an absence rather than a missing level.
300     */
301    public static CatalogObjectKey catalog(IdentifierService service, CatalogHierarchy hierarchy,
302                                          String server, String catalog, IdentifierInputForm form) {
303        Builder builder = builder(service, hierarchy);
304        if (server != null && !server.isEmpty()) {
305            builder.container(IdentifierComponentRole.SERVER, server, form);
306        }
307        return builder.container(IdentifierComponentRole.CATALOG, catalog, form)
308                .build(CatalogObjectKind.CATALOG);
309    }
310
311    /**
312     * A schema inside an already-identified catalog. The parent is REQUIRED on a topology that has a
313     * catalog level: a schema identity that does not name its catalog is not resolved, and keying it
314     * would collide with the same schema name in another catalog. Use
315     * {@link #rootSchema} for a topology that genuinely has no catalog.
316     */
317    public static CatalogObjectKey schemaIn(IdentifierService service, CatalogObjectKey parentCatalog,
318                                           String schema, IdentifierInputForm form) {
319        requireNonNullParent(parentCatalog, "schema");
320        requireKindOf(parentCatalog, CatalogObjectKind.CATALOG, "schema");
321        Builder builder = builder(service, parentCatalog.hierarchy);
322        replay(builder, parentCatalog);
323        return builder.container(IdentifierComponentRole.SCHEMA, schema, form)
324                .build(CatalogObjectKind.SCHEMA);
325    }
326
327    /**
328     * A schema under a topology that has no catalog level. The topology must say so — that is what
329     * proves the missing catalog is structural rather than unresolved.
330     */
331    public static CatalogObjectKey rootSchema(IdentifierService service, CatalogHierarchy hierarchy,
332                                              String schema, IdentifierInputForm form) {
333        requireNoCatalog(hierarchy, "schema");
334        return builder(service, hierarchy)
335                .container(IdentifierComponentRole.SCHEMA, schema, form)
336                .build(CatalogObjectKind.SCHEMA);
337    }
338
339    /** An object inside an already-identified schema. */
340    public static CatalogObjectKey objectInSchema(IdentifierService service, CatalogObjectKey parentSchema,
341                                                 CatalogObjectKind kind, String localName,
342                                                 IdentifierInputForm form) {
343        requireNonNullParent(parentSchema, "object");
344        requireKindOf(parentSchema, CatalogObjectKind.SCHEMA, "object");
345        Builder builder = builder(service, parentSchema.hierarchy);
346        replay(builder, parentSchema);
347        return builder.object(kind, localName, form).build(kind);
348    }
349
350    /**
351     * An object directly inside a catalog, for a schemaless topology such as MySQL's. The topology
352     * must declare that it has no schema level; otherwise the missing schema is an omission and this
353     * is a lookup, not an identity.
354     */
355    public static CatalogObjectKey objectInCatalogOnly(IdentifierService service,
356                                                      CatalogObjectKey parentCatalog,
357                                                      CatalogObjectKind kind, String localName,
358                                                      IdentifierInputForm form) {
359        requireNonNullParent(parentCatalog, "object");
360        requireKindOf(parentCatalog, CatalogObjectKind.CATALOG, "object");
361        requireNoSchema(parentCatalog.hierarchy, "object");
362        Builder builder = builder(service, parentCatalog.hierarchy);
363        replay(builder, parentCatalog);
364        return builder.object(kind, localName, form).build(kind);
365    }
366
367    /**
368     * An object at the root of a topology with no container levels at all. Rare and deliberately
369     * awkward to reach: on any dialect that HAS containers, a bare object name is a lookup.
370     */
371    public static CatalogObjectKey rootObject(IdentifierService service, CatalogHierarchy hierarchy,
372                                              CatalogObjectKind kind, String localName,
373                                              IdentifierInputForm form) {
374        if (hierarchy != CatalogHierarchy.ROOT_ONLY) {
375            throw new IllegalArgumentException("topology " + hierarchy + " has container levels, so "
376                    + "a bare object name is not a resolved identity — build a CatalogLookupName");
377        }
378        return builder(service, hierarchy).object(kind, localName, form).build(kind);
379    }
380
381    /** A column of an already-identified relation. */
382    public static CatalogObjectKey column(IdentifierService service, CatalogObjectKey parentRelation,
383                                         String localName, IdentifierInputForm form) {
384        requireNonNullParent(parentRelation, "column");
385        if (!CatalogNameGrammar.isRelation(parentRelation.terminalKind)) {
386            throw new IllegalArgumentException("a " + parentRelation.terminalKind
387                    + " has no columns; only a table, view or materialized view does");
388        }
389        Builder builder = builder(service, parentRelation.hierarchy);
390        replay(builder, parentRelation);
391        return builder.baseColumn(localName, form).build(CatalogObjectKind.COLUMN);
392    }
393
394    /** An index, constraint or package member of an already-identified parent object. */
395    public static CatalogObjectKey childObject(IdentifierService service, CatalogObjectKey parent,
396                                              CatalogObjectKind childKind, String localName,
397                                              IdentifierInputForm form) {
398        requireNonNullParent(parent, "child object");
399        Builder builder = builder(service, parent.hierarchy);
400        replay(builder, parent);
401        return builder.object(childKind, localName, form).build(childKind);
402    }
403
404    /**
405     * The same identity, reached through a database link. This is the only PUBLIC way to attach one,
406     * because the general builder is package-private — without it a caller could inherit a link but
407     * never declare one.
408     *
409     * <p>The local key must already be complete, and its terminal must be a shape a remote reference
410     * is known to take. Descendants composed onto the result inherit the link, which is correct: a
411     * column of a remote table is reachable as {@code t@remote.c}.
412     */
413    public static CatalogObjectKey atDatabaseLink(IdentifierService service,
414                                                  CatalogObjectKey completeLocalKey,
415                                                  String link, IdentifierInputForm form) {
416        requireNonNullParent(completeLocalKey, "remote reference");
417        if (completeLocalKey.databaseLink != null) {
418            throw new IllegalArgumentException("this identity already carries a database link");
419        }
420        CatalogNameGrammar.requireLinkableTerminal(completeLocalKey.terminalKind);
421        Builder builder = builder(service, completeLocalKey.hierarchy);
422        replay(builder, completeLocalKey);
423        builder.databaseLinkInherited = false;
424        return builder.databaseLink(link, form).build(completeLocalKey.terminalKind);
425    }
426
427    private static void requireNonNullParent(CatalogObjectKey parent, String what) {
428        if (parent == null) {
429            throw new IllegalArgumentException("a " + what + " needs its parent's identity: it is "
430                    + "scoped by the parent and has no meaning without it");
431        }
432    }
433
434    private static void requireKindOf(CatalogObjectKey parent, CatalogObjectKind expected,
435                                      String what) {
436        if (parent.terminalKind != expected) {
437            throw new IllegalArgumentException("a " + what + "'s parent is a " + expected
438                    + ", not a " + parent.terminalKind);
439        }
440    }
441
442    private static void requireNoCatalog(CatalogHierarchy hierarchy, String what) {
443        if (hierarchy == null || hierarchy.hasCatalog()) {
444            throw new IllegalArgumentException("topology " + hierarchy + " has a catalog level, so a "
445                    + what + " that does not name it is not resolved");
446        }
447    }
448
449    private static void requireNoSchema(CatalogHierarchy hierarchy, String what) {
450        if (hierarchy.hasSchema()) {
451            throw new IllegalArgumentException("topology " + hierarchy + " has a schema level, so an "
452                    + what + " directly in a catalog is not resolved — name the schema, or declare a "
453                    + "schemaless topology");
454        }
455    }
456
457    /**
458     * Re-seeds a builder with an existing key's components. The components are already canonical, so
459     * nothing is re-parsed or re-folded — which is the point of composing from a parent identity
460     * rather than re-deriving one from a joined string. An inherited database link is carried along
461     * and may not then be changed: a child of a remote object is part of the same remote identity.
462     */
463    private static void replay(Builder builder, CatalogObjectKey parent) {
464        if (parent.databaseLink != null) {
465            builder.databaseLink = parent.databaseLink;
466            builder.databaseLinkInherited = true;
467        }
468        for (Component component : parent.components) {
469            builder.add(component.role, component.kind, component.key);
470        }
471    }
472
473    // ------------------------------------------------------------------ accessors
474
475    /** The components, outermost first. Never empty. */
476    public List<Component> getComponents() {
477        return components;
478    }
479
480    /** What the name as a whole denotes. */
481    public CatalogObjectKind getTerminalKind() {
482        return terminalKind;
483    }
484
485    /** The namespace topology this identity was declared complete against. */
486    public CatalogHierarchy getHierarchy() {
487        return hierarchy;
488    }
489
490    /**
491     * A description of the rule domain this identity's components were canonicalized under, for
492     * diagnostics. It is NOT part of equality: equality is component-wise {@link CanonKey} equality,
493     * and two keys whose components are equal are the same identity regardless of which service
494     * object produced them.
495     */
496    public String describeRuleDomain() {
497        return components.get(0).getKey().describeRuleDomain();
498    }
499
500    /** The database link qualifying this name, or null. */
501    public CanonKey getDatabaseLink() {
502        return databaseLink;
503    }
504
505    public int size() {
506        return components.size();
507    }
508
509    /** The innermost component — the object or column this key names. */
510    public Component getLeaf() {
511        return components.get(components.size() - 1);
512    }
513
514    @Override
515    public boolean equals(Object o) {
516        if (this == o) {
517            return true;
518        }
519        if (o == null || getClass() != o.getClass()) {
520            return false;
521        }
522        CatalogObjectKey that = (CatalogObjectKey) o;
523        if (terminalKind != that.terminalKind || hierarchy != that.hierarchy
524                || !components.equals(that.components)) {
525            return false;
526        }
527        return databaseLink == null ? that.databaseLink == null
528                : databaseLink.equals(that.databaseLink);
529    }
530
531    @Override
532    public int hashCode() {
533        return hash;
534    }
535
536    @Override
537    public String toString() {
538        StringBuilder sb = new StringBuilder("CatalogObjectKey(").append(terminalKind).append(") ");
539        for (int i = 0; i < components.size(); i++) {
540            if (i > 0) {
541                sb.append(" / ");
542            }
543            sb.append(components.get(i));
544        }
545        if (databaseLink != null) {
546            sb.append(" @").append(databaseLink);
547        }
548        return sb.toString();
549    }
550}