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 <- 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}