001package gudusoft.gsqlparser.sqlenv;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.util.SQLUtil;
005
006import java.util.ArrayList;
007import java.util.Collections;
008import java.util.List;
009
010/**
011 * Assigns a per-segment {@link ESQLDataObjectType} to the segments of a qualified SQL name
012 * (stage S0 of docs/refactor/identifier_identity_display_separation_plan.md).
013 *
014 * <p>A qualified name does not have one object type — {@code db.s.t.c} is a catalog, a
015 * schema, a table and a column, and each segment is compared under the rules of its own
016 * kind. Which position means what depends on the vendor's hierarchy depth, the kind of
017 * object the name as a whole denotes, and how many segments were written. This class is
018 * the single place that decision is expressed.
019 *
020 * <p><strong>It is deliberately a pure function of (vendor, objectType, segments).</strong>
021 * It reads no session state, no {@code ThreadLocal} and no catalog, so the same name always
022 * plans the same way.
023 *
024 * <p><strong>Relation to {@link SQLUtil#compareIdentifier}.</strong> That method carries the
025 * same assignment inline, as nested conditionals around pairwise comparisons. This class
026 * exists so an identity <em>key</em> can be built from the assignment
027 * ({@link QualifiedCanonKey}). It is NOT a copy: it keeps segment boundaries that method
028 * joins away, and it parses vendor-aware where that method does not, so the two diverge on
029 * an enumerated set of shapes where this class is correct and that method is not.
030 * {@code QualifiedCanonKeyEquivalenceProofTest} walks every vendor, every mapped object type
031 * and every ordered pair of shapes, and every divergence is pinned there with its exact
032 * expected values. Stage S2 must make {@code compareIdentifier} delegate here, at which point
033 * those divergences become fixes; S0 changes no production call site.
034 *
035 * <h3>Surplus segments keep their boundaries</h3>
036 * When a name carries more segments than the hierarchy has levels, the surplus segments stay
037 * SEPARATE levels, each typed as the innermost kind: {@code db.s.t.c.sub.sub2} plans as
038 * catalog {@code db}, schema {@code s}, table {@code t}, then three column-typed levels
039 * {@code c}, {@code sub}, {@code sub2}.
040 *
041 * <p>This deliberately differs from {@link SQLUtil#compareIdentifier}, which joins the
042 * surplus back into one dotted string and compares it as a single name. That join loses the
043 * distinction between a path and a name that contains a dot, and an identity key built on it
044 * merges two different objects:
045 *
046 * <pre>
047 * Oracle, dotColumn:   c.d.s.a.f   vs   c.d.s."A.F"
048 *   5 segments [c,d,s,a,f]         vs   4 segments [c,d,s,"A.F"]
049 *   joined leaf text: A.F          vs   A.F          &lt;- collide
050 * </pre>
051 *
052 * The second name is one column whose own text contains a dot — legal on Oracle
053 * ({@code CREATE TABLE t ("A.F" NUMBER)}) — and it is not the field {@code f} of column
054 * {@code a}. Case folding changes letters, never where the boundaries are, so the two can
055 * only be told apart by keeping the segmentation. {@code compareIdentifier} happens to
056 * answer {@code false} for this pair, but only because it compares raw segment counts first
057 * and never reaches its own join; the join does decide pairs of equal arity, and there this
058 * class and that method diverge. Every such divergence is enumerated with exact expected
059 * values in {@code QualifiedCanonKeyEquivalenceProofTest}.
060 */
061public final class QualifiedNameHierarchy {
062
063    private QualifiedNameHierarchy() {
064    }
065
066    /** One planned segment: its text exactly as written, plus the kind it is compared as. */
067    public static final class Segment {
068        private final String text;
069        private final ESQLDataObjectType type;
070
071        Segment(String text, ESQLDataObjectType type) {
072            this.text = text;
073            this.type = type;
074        }
075
076        /** Segment text as written, delimiters and case intact. Never normalized here. */
077        public String getText() {
078            return text;
079        }
080
081        public ESQLDataObjectType getType() {
082            return type;
083        }
084
085        @Override
086        public String toString() {
087            return type + ":" + text;
088        }
089    }
090
091    /**
092     * Splits {@code qualifiedName} into segments the way the identity path must see it.
093     *
094     * <p>Vendor-aware: BigQuery's {@code `project.dataset.table`} becomes three segments.
095     * {@link SQLUtil#compareIdentifier} calls the vendor-blind overload and therefore sees
096     * that shape as ONE segment — a defect of that method, recorded as a pinned divergence
097     * in the S0 proof test and due for repair in S2. Do not "fix" the divergence by making
098     * this method vendor-blind.
099     */
100    public static List<String> split(EDbVendor vendor, String qualifiedName) {
101        return SQLUtil.parseNames(qualifiedName, vendor);
102    }
103
104    /**
105     * Plans the segment types for a qualified name.
106     *
107     * @param vendor     the dialect whose hierarchy depth applies
108     * @param objectType what the name as a whole denotes
109     * @param segments   the already-split segments, in written order
110     * @return exactly one {@link Segment} per input segment — hierarchy types while they
111     *         last, then the innermost type for each surplus segment, with no text joined;
112     *         {@code null} when this (vendor, objectType) pair has no hierarchy mapping —
113     *         see {@link #hasHierarchyMapping}
114     */
115    public static List<Segment> plan(EDbVendor vendor, ESQLDataObjectType objectType, List<String> segments) {
116        if (segments == null || segments.isEmpty() || objectType == null) {
117            return null;
118        }
119
120        boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
121        boolean supportSchema = TSQLEnv.supportSchema(vendor);
122
123        // TSQLEnv.supportCatalog() returns true unconditionally today, so compareIdentifier's
124        // schema-only and neither-level branches are unreachable. Rather than mirror dead
125        // code that cannot be tested, refuse here; the proof test asserts supportCatalog()
126        // holds for every vendor, so a future change to it fails loudly instead of silently
127        // planning names under a hierarchy this class never modelled.
128        if (!supportCatalog) {
129            return null;
130        }
131
132        List<Segment> plan = supportSchema
133                ? planCatalogSchema(objectType, segments)
134                : planCatalogOnly(objectType, segments);
135        return plan == null ? null : Collections.unmodifiableList(plan);
136    }
137
138    /** Vendors with both a catalog and a schema level — every dialect except the four below. */
139    private static List<Segment> planCatalogSchema(ESQLDataObjectType objectType, List<String> segments) {
140        int n = segments.size();
141        if (objectType == ESQLDataObjectType.dotColumn) {
142            if (n >= 4) {
143                return levels(segments,
144                        ESQLDataObjectType.dotCatalog, ESQLDataObjectType.dotSchema,
145                        ESQLDataObjectType.dotTable, ESQLDataObjectType.dotColumn);
146            } else if (n == 3) {
147                return levels(segments,
148                        ESQLDataObjectType.dotSchema, ESQLDataObjectType.dotTable,
149                        ESQLDataObjectType.dotColumn);
150            } else if (n == 2) {
151                return levels(segments, ESQLDataObjectType.dotTable, ESQLDataObjectType.dotColumn);
152            }
153            return levels(segments, ESQLDataObjectType.dotColumn);
154        }
155        if (isTableLevel(objectType)) {
156            if (n >= 3) {
157                return levels(segments,
158                        ESQLDataObjectType.dotCatalog, ESQLDataObjectType.dotSchema, objectType);
159            } else if (n == 2) {
160                return levels(segments, ESQLDataObjectType.dotSchema, objectType);
161            }
162            return levels(segments, objectType);
163        }
164        if (objectType == ESQLDataObjectType.dotSchema) {
165            if (n >= 2) {
166                return levels(segments, ESQLDataObjectType.dotCatalog, ESQLDataObjectType.dotSchema);
167            }
168            return levels(segments, ESQLDataObjectType.dotSchema);
169        }
170        if (objectType == ESQLDataObjectType.dotCatalog) {
171            return levels(segments, ESQLDataObjectType.dotCatalog);
172        }
173        return null;
174    }
175
176    /** MySQL, Teradata, Hive and Impala: one container level, named as a catalog. */
177    private static List<Segment> planCatalogOnly(ESQLDataObjectType objectType, List<String> segments) {
178        int n = segments.size();
179        if (objectType == ESQLDataObjectType.dotColumn) {
180            if (n >= 3) {
181                return levels(segments,
182                        ESQLDataObjectType.dotCatalog, ESQLDataObjectType.dotTable,
183                        ESQLDataObjectType.dotColumn);
184            } else if (n == 2) {
185                return levels(segments, ESQLDataObjectType.dotTable, ESQLDataObjectType.dotColumn);
186            }
187            return levels(segments, ESQLDataObjectType.dotColumn);
188        }
189        if (isTableLevel(objectType)) {
190            if (n >= 2) {
191                return levels(segments, ESQLDataObjectType.dotCatalog, objectType);
192            }
193            return levels(segments, objectType);
194        }
195        // A schema-less dialect compares a schema name in its catalog bucket; that is what
196        // compareIdentifier does, and the flags table has no separate schema rules there.
197        if (objectType == ESQLDataObjectType.dotCatalog || objectType == ESQLDataObjectType.dotSchema) {
198            return levels(segments, ESQLDataObjectType.dotCatalog);
199        }
200        return null;
201    }
202
203    /**
204     * Object types that sit at the table level of the hierarchy — they are schema-qualified
205     * and share the table's position. Kept as one predicate so the two branches above cannot
206     * drift apart.
207     */
208    private static boolean isTableLevel(ESQLDataObjectType objectType) {
209        return objectType == ESQLDataObjectType.dotTable
210                || objectType == ESQLDataObjectType.dotSynonyms
211                || objectType == ESQLDataObjectType.dotOraclePackage
212                || objectType == ESQLDataObjectType.dotFunction
213                || objectType == ESQLDataObjectType.dotProcedure
214                || objectType == ESQLDataObjectType.dotTrigger;
215    }
216
217    /**
218     * One planned level per segment: {@code types[i]} for as long as it lasts, then the
219     * innermost type for every surplus segment. Segment text is never joined, so the plan
220     * always has exactly {@code segments.size()} entries and the name's arity survives into
221     * the key.
222     */
223    private static List<Segment> levels(List<String> segments, ESQLDataObjectType... types) {
224        List<Segment> plan = new ArrayList<Segment>(segments.size());
225        for (int i = 0; i < segments.size(); i++) {
226            plan.add(new Segment(segments.get(i), types[Math.min(i, types.length - 1)]));
227        }
228        return plan;
229    }
230
231    /**
232     * Whether this (vendor, objectType) pair has a hierarchy mapping at all.
233     *
234     * <p>{@code dotUnknown}, {@code dotRoutine}, {@code dotDblink}, {@code dotDataType} and
235     * {@code dotParameter} have none. Note what {@link SQLUtil#compareIdentifier} does for
236     * those: it falls through every branch and returns {@code false} — so it reports two
237     * <em>identical</em> qualified names as different. An identity key cannot model a
238     * non-reflexive relation, so {@link QualifiedCanonKey} refuses to build one instead of
239     * reproducing that. The refusal is the honest answer; the {@code false} is a defect of
240     * {@code compareIdentifier}, tracked for S2.
241     */
242    public static boolean hasHierarchyMapping(EDbVendor vendor, ESQLDataObjectType objectType) {
243        return plan(vendor, objectType, Collections.singletonList("x")) != null;
244    }
245}