001package gudusoft.gsqlparser.catalog.runtime;
002
003import gudusoft.gsqlparser.sqlenv.IdentifierComponentRole;
004import gudusoft.gsqlparser.sqlenv.IdentifierInputForm;
005
006import java.util.ArrayList;
007import java.util.Collections;
008import java.util.List;
009
010/**
011 * A <strong>request</strong> to find a catalog object — explicitly NOT an identity (stage S0R of
012 * docs/refactor/identifier_identity_display_separation_plan.md).
013 *
014 * <h3>Why a lookup is a different type from a key</h3>
015 * {@code t}, {@code s.t} and {@code db..t} are all normal things to write, and none of them is a
016 * complete identity: the first two leave levels unstated, and the third states that a level was
017 * written but left empty. Only after defaults and the search path supply the missing context does a
018 * {@link CatalogObjectKey} exist. Collapsing the two ideas into one type is what let {@code db..t}
019 * be keyed as {@code db.dbo.t}, which silently binds the wrong object whenever the session default
020 * schema is not {@code dbo}.
021 *
022 * <h3>Three states, never two</h3>
023 * <ul>
024 *   <li>{@link SlotState#ABSENT} — the dialect or declared layout genuinely has no such level.
025 *       MySQL has no schema level; there is nothing missing.</li>
026 *   <li>{@link SlotState#OMITTED} — the level EXISTS and the caller did not name it. {@code db..t}
027 *       omits its schema; a bare {@code t} omits both. A resolver must supply it before an identity
028 *       can be built, and {@link CatalogObjectKey} refuses to contain one.</li>
029 *   <li>{@link SlotState#PRESENT} — named, with its text and the provenance of that text.</li>
030 * </ul>
031 *
032 * Conflating ABSENT with OMITTED loses the only signal that tells "this dialect has no schemas"
033 * apart from "the user did not say which schema", and an empty string conflates both with a level
034 * named by the empty name.
035 */
036public final class CatalogLookupName {
037
038    /** Whether a hierarchy level is inapplicable, unstated, or named. */
039    public enum SlotState {
040        ABSENT,
041        OMITTED,
042        PRESENT
043    }
044
045    /** One level of the request. */
046    public static final class Slot {
047        private final IdentifierComponentRole role;
048        private final SlotState state;
049        private final String text;
050        private final IdentifierInputForm form;
051        private final CatalogObjectKind kind;
052
053        Slot(IdentifierComponentRole role, SlotState state, String text, IdentifierInputForm form,
054             CatalogObjectKind kind) {
055            this.role = role;
056            this.state = state;
057            this.text = text;
058            this.form = form;
059            this.kind = kind;
060        }
061
062        public IdentifierComponentRole getRole() {
063            return role;
064        }
065
066        public SlotState getState() {
067            return state;
068        }
069
070        /** The text, only when {@link #getState()} is PRESENT. */
071        public String getText() {
072            return text;
073        }
074
075        /** Where the text came from, only when PRESENT. */
076        public IdentifierInputForm getForm() {
077            return form;
078        }
079
080        /** The entity kind for an OBJECT slot. */
081        public CatalogObjectKind getKind() {
082            return kind;
083        }
084
085        @Override
086        public String toString() {
087            if (state != SlotState.PRESENT) {
088                return role + "=" + state;
089            }
090            return role + (kind == null ? "" : "(" + kind + ")") + "=" + text + "/" + form;
091        }
092    }
093
094    private final List<Slot> slots;
095    private final CatalogObjectKind terminalKind;
096    private final CatalogHierarchy hierarchy;
097    private final CatalogParentConstraint parentConstraint;
098    private final String databaseLink;
099    private final IdentifierInputForm databaseLinkForm;
100
101    private CatalogLookupName(List<Slot> slots, CatalogObjectKind terminalKind,
102                              CatalogHierarchy hierarchy, CatalogParentConstraint parentConstraint,
103                              String databaseLink, IdentifierInputForm databaseLinkForm) {
104        this.slots = Collections.unmodifiableList(new ArrayList<Slot>(slots));
105        this.terminalKind = terminalKind;
106        this.hierarchy = hierarchy;
107        this.parentConstraint = parentConstraint;
108        this.databaseLink = databaseLink;
109        this.databaseLinkForm = databaseLinkForm;
110    }
111
112    /**
113     * A request against a declared namespace topology. The topology is required: whether a missing
114     * schema is an OMISSION or simply inapplicable is what it answers, and no component list can.
115     */
116    public static Builder builder(CatalogHierarchy hierarchy) {
117        return new Builder(hierarchy);
118    }
119
120    /**
121     * Parses a FLAT name under a layout the caller DECLARES, against a topology the caller DECLARES.
122     * The parser verifies the component count against the layout and refuses otherwise; it never
123     * chooses the layout, which is the whole point — choosing is the defect.
124     *
125     * <p>Where the layout has a container OBJECT whose exact kind a flat name cannot carry — a
126     * column's parent may be a table, a view or a materialized view — the FAMILY is recorded
127     * instead, derived from the layout itself. A lookup legitimately knows only the family; only a
128     * resolved {@link CatalogObjectKey} carries the exact kind.
129     *
130     * <p>Prefer {@link #builder(CatalogHierarchy)} with the parts passed separately. This exists for
131     * callers whose input genuinely is one string.
132     *
133     * @throws IllegalArgumentException if the component count does not match the declared layout
134     */
135    public static CatalogLookupName ofDeclaredLayout(List<String> components,
136                                                     CatalogNameLayout layout,
137                                                     IdentifierInputForm form,
138                                                     CatalogObjectKind terminalKind,
139                                                     CatalogHierarchy hierarchy) {
140        if (components == null || layout == null) {
141            throw new IllegalArgumentException("components and layout are both required");
142        }
143        if (components.size() != layout.size()) {
144            throw new IllegalArgumentException("declared layout " + layout + " has " + layout.size()
145                    + " components but the name has " + components.size()
146                    + "; the caller must declare the layout its name actually has, and the parser "
147                    + "will not pick one");
148        }
149
150        if (!layout.permitsTerminalKind(terminalKind)) {
151            throw new IllegalArgumentException("layout " + layout + " does not describe a "
152                    + terminalKind + "; a layout states a SHAPE, and using a child layout to seek a "
153                    + "container object would describe that object as a child of another");
154        }
155        Builder builder = builder(hierarchy);
156        // A level the topology HAS but the declared layout does not write is OMITTED. That is not a
157        // guess: the layout states what was written and the topology states what exists, so the
158        // difference between them is exactly "exists but unnamed".
159        for (IdentifierComponentRole container : hierarchy.containers()) {
160            if (!layout.roles().contains(container)) {
161                builder.omitted(container);
162            }
163        }
164        CatalogParentConstraint containerFamily = containerFamilyOf(layout, terminalKind);
165        if (containerFamily != null) {
166            builder.parentConstraint(containerFamily);
167        }
168        for (int i = 0; i < components.size(); i++) {
169            IdentifierComponentRole role = layout.roleAt(i);
170            String text = components.get(i);
171            if (text == null || text.isEmpty()) {
172                // An empty component in a declared layout is an OMISSION, not a name: `db..t`.
173                builder.omitted(role);
174                continue;
175            }
176            if (role == IdentifierComponentRole.OBJECT) {
177                boolean terminal = i == components.size() - 1;
178                if (terminal) {
179                    builder.object(terminalKind, text, form);
180                } else {
181                    // The container's exact kind is not expressible here; the family above is.
182                    builder.containerObject(text, form);
183                }
184            } else if (role == IdentifierComponentRole.BASE_COLUMN) {
185                builder.baseColumn(text, form);
186            } else {
187                builder.present(role, text, form);
188            }
189        }
190        return builder.build(terminalKind);
191    }
192
193    /**
194     * The family a layout's container object belongs to, or null when the layout has no container
195     * object. Derived from the LAYOUT, which is declared — not from the component count.
196     */
197    private static CatalogParentConstraint containerFamilyOf(CatalogNameLayout layout,
198                                                             CatalogObjectKind terminalKind) {
199        int objects = 0;
200        boolean terminalIsColumn = false;
201        for (int i = 0; i < layout.size(); i++) {
202            if (layout.roleAt(i) == IdentifierComponentRole.OBJECT) {
203                objects++;
204            }
205            if (i == layout.size() - 1
206                    && layout.roleAt(i) == IdentifierComponentRole.BASE_COLUMN) {
207                terminalIsColumn = true;
208            }
209        }
210        if (terminalIsColumn && objects >= 1) {
211            return CatalogParentConstraint.RELATION;
212        }
213        if (objects >= 2) {
214            return layout == CatalogNameLayout.CATALOG_SCHEMA_PACKAGE_MEMBER
215                    ? CatalogParentConstraint.ROUTINE_CONTAINER
216                    : CatalogParentConstraint.RELATION;
217        }
218        return null;
219    }
220
221    /** @see #builder() */
222    public static final class Builder {
223        private final List<Slot> slots = new ArrayList<Slot>();
224        private final CatalogHierarchy hierarchy;
225        private CatalogParentConstraint parentConstraint;
226        private boolean parentConstraintSupplied;
227        private String databaseLink;
228        private IdentifierInputForm databaseLinkForm;
229
230        private Builder(CatalogHierarchy hierarchy) {
231            if (hierarchy == null) {
232                throw new IllegalArgumentException("a lookup must declare its namespace topology: "
233                        + "that is what distinguishes an inapplicable level from an unnamed one");
234            }
235            this.hierarchy = hierarchy;
236        }
237
238        /**
239         * What is known about the parent of the object being sought, when it is a child. A LOOKUP
240         * carries a FAMILY, not an exact kind: parsing {@code schema.relation.column} establishes
241         * that the parent is a relation, not whether it will resolve to a table or a view.
242         */
243        public Builder parentConstraint(CatalogParentConstraint constraint) {
244            if (constraint == null) {
245                throw new IllegalArgumentException("a parent constraint of null says nothing; omit "
246                        + "the call instead");
247            }
248            // Tracked by WHETHER it was supplied, not by its value. Comparing values let a caller
249            // classify twice with the same answer — which is still double classification, and a
250            // null-then-value sequence slipped through entirely.
251            if (parentConstraintSupplied) {
252                throw new IllegalArgumentException("the parent constraint was already supplied as "
253                        + this.parentConstraint + "; stating it twice hides which call the caller "
254                        + "meant");
255            }
256            this.parentConstraint = constraint;
257            this.parentConstraintSupplied = true;
258            return this;
259        }
260
261        /**
262         * Refuses a second slot for a role that names a single level. The normalization step below
263         * reorders slots into hierarchy order, and an earlier version took the FIRST slot of each
264         * role — so a caller that supplied two catalogs silently lost one of them before the
265         * uniqueness validator ever saw it. Rejecting at the point of addition is the only place
266         * where both values are still in hand.
267         */
268        private void refuseDuplicate(IdentifierComponentRole role) {
269            if (role == IdentifierComponentRole.OBJECT) {
270                return;   // a relation plus its child, or a package plus its routine
271            }
272            for (Slot slot : slots) {
273                if (slot.role == role) {
274                    throw new IllegalArgumentException("a second " + role + " slot: "
275                            + "this role names a single level, and keeping one of two values would "
276                            + "discard the other silently");
277                }
278            }
279        }
280
281        /** The level exists and is named. */
282        public Builder present(IdentifierComponentRole role, String text, IdentifierInputForm form) {
283            if (text == null || text.isEmpty()) {
284                throw new IllegalArgumentException("a PRESENT " + role + " needs text; use "
285                        + "omitted(role) when the level exists but was not named, or absent(role) "
286                        + "when the dialect has no such level");
287            }
288            if (form == null) {
289                throw new IllegalArgumentException("a PRESENT slot needs its provenance");
290            }
291            refuseDuplicate(role);
292            slots.add(new Slot(role, SlotState.PRESENT, text, form, null));
293            return this;
294        }
295
296        /** A named object level, with its entity kind. */
297        public Builder object(CatalogObjectKind kind, String text, IdentifierInputForm form) {
298            if (kind == null) {
299                throw new IllegalArgumentException("an object slot needs its kind");
300            }
301            if (text == null || text.isEmpty()) {
302                throw new IllegalArgumentException("a PRESENT object needs text");
303            }
304            slots.add(new Slot(IdentifierComponentRole.OBJECT, SlotState.PRESENT, text, form, kind));
305            return this;
306        }
307
308        /**
309         * A named container object whose exact kind is not yet known — only the family, which the
310         * request carries through {@link #parentConstraint}. This is what a flat
311         * {@code schema.relation.column} can honestly say about its middle component.
312         */
313        public Builder containerObject(String text, IdentifierInputForm form) {
314            if (text == null || text.isEmpty()) {
315                throw new IllegalArgumentException("a PRESENT object needs text");
316            }
317            if (form == null) {
318                throw new IllegalArgumentException("a PRESENT slot needs its provenance");
319            }
320            slots.add(new Slot(IdentifierComponentRole.OBJECT, SlotState.PRESENT, text, form, null));
321            return this;
322        }
323
324        /**
325         * A named column level. It installs the RELATION parent constraint itself: a column is
326         * always a child of a relation, which is semantic knowledge rather than a guess, so making
327         * every caller restate it would be ceremony that some caller eventually forgets.
328         */
329        public Builder baseColumn(String text, IdentifierInputForm form) {
330            present(IdentifierComponentRole.BASE_COLUMN, text, form);
331            if (parentConstraint == null) {
332                // Installed by the type itself, so it does NOT count as the caller having
333                // classified: a caller may still state RELATION explicitly once.
334                parentConstraint = CatalogParentConstraint.RELATION;
335            }
336            return this;
337        }
338
339        /** The level exists but the caller did not name it — a resolver must supply it. */
340        public Builder omitted(IdentifierComponentRole role) {
341            refuseDuplicate(role);
342            slots.add(new Slot(role, SlotState.OMITTED, null, null, null));
343            return this;
344        }
345
346        /** The dialect or layout has no such level at all. */
347        public Builder absent(IdentifierComponentRole role) {
348            refuseDuplicate(role);
349            slots.add(new Slot(role, SlotState.ABSENT, null, null, null));
350            return this;
351        }
352
353        public Builder databaseLink(String text, IdentifierInputForm form) {
354            if (databaseLink != null) {
355                throw new IllegalArgumentException("a name has one database link");
356            }
357            if (text == null || text.isEmpty()) {
358                throw new IllegalArgumentException("a database link with no text is not a link");
359            }
360            if (form == null) {
361                throw new IllegalArgumentException("a database link needs its provenance");
362            }
363            this.databaseLink = text;
364            this.databaseLinkForm = form;
365            return this;
366        }
367
368        public CatalogLookupName build(CatalogObjectKind terminalKind) {
369            if (terminalKind == null) {
370                throw new IllegalArgumentException("a lookup must say what kind it is looking for");
371            }
372            // Levels the topology does not have are marked ABSENT automatically. That is not a
373            // guess: the topology DECLARED them inapplicable, and leaving them unrepresented is
374            // what made a bare `t` report itself fully qualified.
375            // Only levels at or ABOVE the requested terminal belong in the vector. A CATALOG lookup
376            // that also carried a schema slot was claiming to name a schema while saying it sought a
377            // catalog — two different requests wearing one shape.
378            List<Slot> complete = new ArrayList<Slot>();
379            for (IdentifierComponentRole container
380                    : new IdentifierComponentRole[]{IdentifierComponentRole.SERVER,
381                    IdentifierComponentRole.CATALOG, IdentifierComponentRole.SCHEMA}) {
382                if (!CatalogNameGrammar.levelAppliesTo(container, terminalKind)) {
383                    Slot stray = find(container);
384                    if (stray != null) {
385                        throw new IllegalArgumentException("a " + terminalKind + " lookup names a "
386                                + container + ", which sits below what it seeks");
387                    }
388                    continue;
389                }
390                Slot existing = find(container);
391                if (existing != null) {
392                    complete.add(existing);
393                } else if (container != IdentifierComponentRole.SERVER && !hierarchy.has(container)) {
394                    complete.add(new Slot(container, SlotState.ABSENT, null, null, null));
395                }
396            }
397            for (Slot slot : slots) {
398                if (slot.role != IdentifierComponentRole.SERVER
399                        && slot.role != IdentifierComponentRole.CATALOG
400                        && slot.role != IdentifierComponentRole.SCHEMA) {
401                    complete.add(slot);
402                }
403            }
404            CatalogNameGrammar.validateLookup(complete, terminalKind, hierarchy, databaseLink,
405                    databaseLinkForm, parentConstraint);
406            return new CatalogLookupName(complete, terminalKind, hierarchy, parentConstraint,
407                    databaseLink, databaseLinkForm);
408        }
409
410        private Slot find(IdentifierComponentRole role) {
411            for (Slot slot : slots) {
412                if (slot.role == role) {
413                    return slot;
414                }
415            }
416            return null;
417        }
418    }
419
420    /** The slots, outermost first. */
421    public List<Slot> getSlots() {
422        return slots;
423    }
424
425    public CatalogObjectKind getTerminalKind() {
426        return terminalKind;
427    }
428
429    /** The declared namespace topology this request was built against. */
430    public CatalogHierarchy getHierarchy() {
431        return hierarchy;
432    }
433
434    /** What is known about the parent's kind, or null when the request names no child. */
435    public CatalogParentConstraint getParentConstraint() {
436        return parentConstraint;
437    }
438
439    public String getDatabaseLink() {
440        return databaseLink;
441    }
442
443    public IdentifierInputForm getDatabaseLinkForm() {
444        return databaseLinkForm;
445    }
446
447    /**
448     * Whether every level this request needs is named, so an identity could be built without
449     * consulting any default. An omission here is not an error — it is the normal reason a resolver
450     * is involved.
451     */
452    public boolean isFullyQualified() {
453        for (Slot slot : slots) {
454            if (slot.state == SlotState.OMITTED) {
455                return false;
456            }
457        }
458        return true;
459    }
460
461    /**
462     * The slot for a role, or null when this request has no such level.
463     *
464     * @throws IllegalArgumentException for {@link IdentifierComponentRole#OBJECT}, which may appear
465     *         twice — a relation and its index. Returning the first silently would hand a caller
466     *         asking for "the object" the container when it wanted the child. Use
467     *         {@link #getTerminalSlot()}, {@link #getParentObjectSlot()} or {@link #getObjectSlots()}.
468     */
469    public Slot slotOf(IdentifierComponentRole role) {
470        if (role == IdentifierComponentRole.OBJECT) {
471            throw new IllegalArgumentException("OBJECT may appear twice, so asking for 'the' object "
472                    + "slot is ambiguous; use getTerminalSlot(), getParentObjectSlot() or "
473                    + "getObjectSlots()");
474        }
475        for (Slot slot : slots) {
476            if (slot.role == role) {
477                return slot;
478            }
479        }
480        return null;
481    }
482
483    /** The innermost slot — what this request is actually looking for. */
484    public Slot getTerminalSlot() {
485        return slots.get(slots.size() - 1);
486    }
487
488    /** The object slot that contains the terminal, or null when the terminal has no object parent. */
489    public Slot getParentObjectSlot() {
490        Slot parent = null;
491        for (int i = 0; i < slots.size() - 1; i++) {
492            if (slots.get(i).role == IdentifierComponentRole.OBJECT) {
493                parent = slots.get(i);
494            }
495        }
496        return parent;
497    }
498
499    /** Every object slot, outermost first. */
500    public List<Slot> getObjectSlots() {
501        List<Slot> objects = new ArrayList<Slot>(2);
502        for (Slot slot : slots) {
503            if (slot.role == IdentifierComponentRole.OBJECT) {
504                objects.add(slot);
505            }
506        }
507        return Collections.unmodifiableList(objects);
508    }
509
510    @Override
511    public String toString() {
512        StringBuilder sb = new StringBuilder("CatalogLookupName(").append(terminalKind).append(") ");
513        for (int i = 0; i < slots.size(); i++) {
514            if (i > 0) {
515                sb.append(" / ");
516            }
517            sb.append(slots.get(i));
518        }
519        if (databaseLink != null) {
520            sb.append(" @").append(databaseLink);
521        }
522        return sb.toString();
523    }
524}