001package gudusoft.gsqlparser.catalog.runtime;
002
003import gudusoft.gsqlparser.EDbVendor;
004
005import java.util.ArrayList;
006import java.util.BitSet;
007import java.util.Collections;
008import java.util.List;
009import java.util.Objects;
010
011/**
012 * Multi-segment qualified name with raw + normalized forms, per-segment quoted flags,
013 * the kind of object it identifies, and the originating vendor.
014 *
015 * <p>This type holds <em>data only</em>. Construction must go through
016 * {@link CatalogIdentifierPolicy#parse} so that case-folding is centralized in
017 * {@code IdentifierService}; the package-private constructor below is the single allowed
018 * factory pathway.</p>
019 *
020 * <p>Plan §7.2 / §9.</p>
021 */
022public final class CatalogQualifiedName {
023
024    private final List<String> rawSegments;
025    private final List<String> normalizedSegments;
026    private final BitSet quotedFlags;
027    private final CatalogObjectKind kind;
028    private final EDbVendor vendor;
029
030    /**
031     * Package-private constructor invoked exclusively by {@link CatalogIdentifierPolicy}.
032     * Defensive copies are taken so callers cannot mutate state after construction.
033     */
034    CatalogQualifiedName(List<String> rawSegments,
035                         List<String> normalizedSegments,
036                         BitSet quotedFlags,
037                         CatalogObjectKind kind,
038                         EDbVendor vendor) {
039        if (rawSegments == null || rawSegments.isEmpty()) {
040            throw new IllegalArgumentException("CatalogQualifiedName: rawSegments must be non-empty");
041        }
042        if (normalizedSegments == null || normalizedSegments.size() != rawSegments.size()) {
043            throw new IllegalArgumentException(
044                "CatalogQualifiedName: normalizedSegments must match rawSegments size");
045        }
046        if (quotedFlags == null) {
047            throw new IllegalArgumentException("CatalogQualifiedName: quotedFlags is required");
048        }
049        if (kind == null) {
050            throw new IllegalArgumentException("CatalogQualifiedName: kind is required");
051        }
052        if (vendor == null) {
053            throw new IllegalArgumentException("CatalogQualifiedName: vendor is required");
054        }
055        this.rawSegments = Collections.unmodifiableList(new ArrayList<String>(rawSegments));
056        this.normalizedSegments = Collections.unmodifiableList(new ArrayList<String>(normalizedSegments));
057        this.quotedFlags = (BitSet) quotedFlags.clone();
058        this.kind = kind;
059        this.vendor = vendor;
060    }
061
062    public List<String> raw() {
063        return rawSegments;
064    }
065
066    public List<String> normalized() {
067        return normalizedSegments;
068    }
069
070    public boolean isQuoted(int segmentIndex) {
071        if (segmentIndex < 0 || segmentIndex >= rawSegments.size()) {
072            throw new IndexOutOfBoundsException(
073                "segment index " + segmentIndex + " out of range [0," + rawSegments.size() + ")");
074        }
075        return quotedFlags.get(segmentIndex);
076    }
077
078    public BitSet quotedFlags() {
079        return (BitSet) quotedFlags.clone();
080    }
081
082    public CatalogObjectKind kind() {
083        return kind;
084    }
085
086    public EDbVendor vendor() {
087        return vendor;
088    }
089
090    public int size() {
091        return rawSegments.size();
092    }
093
094    /** Last segment — the local object name. */
095    public String localName() {
096        return rawSegments.get(rawSegments.size() - 1);
097    }
098
099    /**
100     * Return a copy of this name with a different {@link CatalogObjectKind}. Used by the
101     * resolver's "widened kind matrix" path (plan §9.3): a TABLE lookup needs to be tried
102     * against VIEW and MATERIALIZED_VIEW buckets too. The raw / normalized segments and
103     * quoted flags are preserved bit-for-bit; only the kind changes (which affects
104     * {@code keyForMap} and equality).
105     */
106    public CatalogQualifiedName withKind(CatalogObjectKind newKind) {
107        if (newKind == null) {
108            throw new IllegalArgumentException("CatalogQualifiedName.withKind: newKind is required");
109        }
110        if (newKind == this.kind) {
111            return this;
112        }
113        return new CatalogQualifiedName(rawSegments, normalizedSegments, quotedFlags, newKind, vendor);
114    }
115
116    public String normalizedLocalName() {
117        return normalizedSegments.get(normalizedSegments.size() - 1);
118    }
119
120    @Override
121    public boolean equals(Object o) {
122        if (this == o) return true;
123        if (!(o instanceof CatalogQualifiedName)) return false;
124        CatalogQualifiedName that = (CatalogQualifiedName) o;
125        return rawSegments.equals(that.rawSegments)
126            && normalizedSegments.equals(that.normalizedSegments)
127            && quotedFlags.equals(that.quotedFlags)
128            && kind == that.kind
129            && vendor == that.vendor;
130    }
131
132    @Override
133    public int hashCode() {
134        return Objects.hash(rawSegments, normalizedSegments, quotedFlags, kind, vendor);
135    }
136
137    @Override
138    public String toString() {
139        return "CatalogQualifiedName{kind=" + kind + ", vendor=" + vendor
140            + ", raw=" + rawSegments + ", norm=" + normalizedSegments + '}';
141    }
142}