001package gudusoft.gsqlparser.sqlenv;
002
003import gudusoft.gsqlparser.EDbVendor;
004
005import java.util.ArrayList;
006import java.util.Collections;
007import java.util.List;
008
009/**
010 * Canonical identity of a possibly-qualified SQL name — {@code db.schema.table},
011 * {@code schema.table.column}, {@code t.c} — for use as a map or set key
012 * (stage S0 of docs/refactor/identifier_identity_display_separation_plan.md).
013 *
014 * <p>It is the qualified counterpart of {@link CanonKey}: one {@code CanonKey} per
015 * hierarchy level, each computed under the rules of <em>that level's</em> object type, plus
016 * the terminal object type itself so that a table named {@code x} and a schema named
017 * {@code x} cannot collide in one index. {@link CanonKey} carries an
018 * {@link IdentifierProfile.ObjectGroup}, which is the right granularity for comparing
019 * identifiers but deliberately coarser than the entity kind — hence the explicit terminal
020 * discriminator here.
021 *
022 * <p><strong>There is no flattened identity string.</strong> The previous approach,
023 * {@link IdentifierService#buildCompositeKey(String, ESQLDataObjectType)}, joins
024 * already-normalized segment text; it cannot represent quote state, and its guard
025 * {@link IdentifierService#canUseCompositeKey(String)} refuses every name containing a
026 * quote character — which is exactly the case that loses lineage (MantisBT 4728). Joining
027 * canonical segments back into a {@code String} would reintroduce both problems, so this
028 * type keeps the segment keys as a list and nothing renders them.
029 *
030 * <h3>Equality</h3>
031 * Two keys are equal iff they have the same terminal object type, the same number of levels, and
032 * level-wise equal {@link CanonKey}s. Since {@code CanonKey} equality IS the canonical
033 * single-segment equality relation (unification-plan stage P0d.1), each LEVEL compares correctly.
034 *
035 * <p><strong>That does not make the whole key agree with
036 * {@link gudusoft.gsqlparser.util.SQLUtil#compareIdentifier}, and it does not make it correct.</strong>
037 * Two things are still decided outside the per-level comparison: how the name is split into
038 * segments, and which object type each position is compared as. The S0 proof test measures the
039 * agreement over every vendor, every mapped object type and every ordered pair of shapes, and
040 * pins each divergence with its cause — but agreement with {@code compareIdentifier} is not
041 * proof of correctness, because both can be wrong together.
042 *
043 * <p>They ARE wrong together in one known case, which this type cannot currently fix: the role of
044 * each position is inferred from the vendor, the terminal type and the arity alone. For a
045 * two-segment column name that guesses {@code table.column}, but BigQuery
046 * {@code customer.name} is a STRUCT field access — base column {@code customer} plus field
047 * {@code name} (see {@code FieldPath} and {@code TestBigQueryNestedColumns}) — and the two read
048 * the first segment under different case rules. Fixing it needs an explicit semantic role per
049 * component, taken from resolution rather than from syntax; that is scheduled before S2 and is
050 * why {@code compareIdentifier} must NOT be made to delegate to the planner first.
051 *
052 * <h3>Provenance</h3>
053 * Build one through the explicitly named factories
054 * {@link IdentifierService#qualifiedCanonKeyFromLexical(String, ESQLDataObjectType)} —
055 * for a name as written in SQL — or
056 * {@link IdentifierService#qualifiedCanonKeyFromStored(java.util.List, ESQLDataObjectType)} — for
057 * a name already in the form a catalog stores, passed as one entry per level. The two are not interchangeable: keying a
058 * Snowflake catalog object from an already-decoded {@code foo} is not the same as keying
059 * the SQL lexical {@code "foo"}. There is deliberately no factory that guesses.
060 *
061 * <p><strong>In-process only</strong>, for the same reason as {@link CanonKey}: collation
062 * payloads are stable only within one JVM and collator configuration. Use
063 * {@link PersistentIdentifierKey} for anything persisted or exchanged.
064 */
065public final class QualifiedCanonKey {
066
067    private final List<CanonKey> segmentKeys;
068    private final ESQLDataObjectType terminalType;
069    private final int hash;
070
071    QualifiedCanonKey(List<CanonKey> segmentKeys, ESQLDataObjectType terminalType) {
072        this.segmentKeys = Collections.unmodifiableList(new ArrayList<CanonKey>(segmentKeys));
073        this.terminalType = terminalType;
074        this.hash = 31 * this.segmentKeys.hashCode() + terminalType.hashCode();
075    }
076
077    /** The per-level canonical keys, outermost container first. Never empty. */
078    public List<CanonKey> getSegmentKeys() {
079        return segmentKeys;
080    }
081
082    /** The kind of object the whole name denotes — the entity-kind discriminator. */
083    public ESQLDataObjectType getTerminalType() {
084        return terminalType;
085    }
086
087    /** Number of hierarchy levels this name resolved to. */
088    public int size() {
089        return segmentKeys.size();
090    }
091
092    /** The vendor rule domain; all levels share it. */
093    public EDbVendor getVendor() {
094        return segmentKeys.get(0).getVendor();
095    }
096
097    /**
098     * The key of the innermost level — the column of {@code t.c}, the table of
099     * {@code s.t}. Useful for indexes that bucket by container and then by leaf name.
100     */
101    public CanonKey getLeafKey() {
102        return segmentKeys.get(segmentKeys.size() - 1);
103    }
104
105    @Override
106    public boolean equals(Object o) {
107        if (this == o) {
108            return true;
109        }
110        if (o == null || getClass() != o.getClass()) {
111            return false;
112        }
113        QualifiedCanonKey that = (QualifiedCanonKey) o;
114        return terminalType == that.terminalType && segmentKeys.equals(that.segmentKeys);
115    }
116
117    @Override
118    public int hashCode() {
119        return hash;
120    }
121
122    @Override
123    public String toString() {
124        StringBuilder sb = new StringBuilder("QualifiedCanonKey{").append(terminalType).append(' ');
125        for (int i = 0; i < segmentKeys.size(); i++) {
126            if (i > 0) {
127                sb.append(" / ");
128            }
129            sb.append(segmentKeys.get(i));
130        }
131        return sb.append('}').toString();
132    }
133}