001package gudusoft.gsqlparser.sqlenv; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.util.SQLUtil; 005import gudusoft.gsqlparser.util.keywordChecker; 006 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collections; 010import java.util.List; 011import java.util.Map; 012import java.util.Objects; 013 014/** 015 * Generates a SQL name from STORED name segments. 016 * 017 * <p>See docs/refactor/identifier/README.md, "Catalog inputs and name output". 018 * This capability lets existing getters such as 019 * {@code procedure.getName()} can keep their historical output while correct 020 * quoting, escaping and qualification live in one place that actually promises 021 * them.</p> 022 * 023 * <h3>Contract</h3> 024 * <ul> 025 * <li><b>Input is stored names, never SQL text.</b> Each segment is the name as 026 * the database catalog stores it: {@code db name}, not {@code [db name]}. 027 * A stored name whose real content includes delimiter characters 028 * ({@code [db name]} with the brackets being part of the name) is legal 029 * input and is escaped, not reinterpreted.</li> 030 * <li><b>Segments are structural.</b> A stored name containing a dot 031 * ({@code db.part}) is ONE segment. This class never splits a segment and 032 * never joins pre-rendered text.</li> 033 * <li><b>No guessing.</b> The caller states every segment. Nothing is added: 034 * no default schema, no {@code dbo}, no current database. A missing or 035 * empty segment is a failure, not a hint to fill in.</li> 036 * <li><b>A success is verified.</b> Every quoted segment is decoded back and 037 * must reproduce the stored name exactly, and must be a valid spelling for 038 * the vendor. Anything that cannot be proven yields a failure with a 039 * reason instead of a plausible-looking name.</li> 040 * </ul> 041 * 042 * <h3>What is NOT promised</h3> 043 * <p>Validity is at the identifier level: delimiters, escaping, characters and 044 * segment structure. Whether the vendor accepts that many segments in a given 045 * statement position, length limits, and whether the object exists are outside 046 * this class.</p> 047 */ 048public final class SqlNameGenerator { 049 050 /** How to choose between the bare and the delimited spelling of a segment. */ 051 public enum QuotePolicy { 052 /** Delimit every segment. Always correct when the stored name is known. */ 053 ALWAYS, 054 /** 055 * Emit the bare spelling only when it is PROVEN to denote the same object; 056 * otherwise delimit. See {@link SqlNameGenerator#bareIsProvenSafe}. 057 */ 058 WHEN_NEEDED 059 } 060 061 /** Why a name could not be generated. */ 062 public enum Failure { 063 /** No segments were given. */ 064 NO_SEGMENTS, 065 /** A segment, its role or its stored name was null. */ 066 NULL_SEGMENT, 067 /** A segment's stored name was empty; nothing is filled in on the caller's behalf. */ 068 EMPTY_SEGMENT, 069 /** The name needs delimiting but the vendor has no delimited form for that role. */ 070 NO_QUOTED_FORM, 071 /** The name cannot be written as a valid identifier for this vendor at all. */ 072 NOT_REPRESENTABLE, 073 /** The rendered segment did not decode back to the stored name. */ 074 ROUND_TRIP_MISMATCH 075 } 076 077 /** One stored name segment together with its syntactic role. */ 078 public static final class Segment { 079 private final ESQLDataObjectType role; 080 private final String stored; 081 082 private Segment(ESQLDataObjectType role, String stored) { 083 this.role = role; 084 this.stored = stored; 085 } 086 087 public static Segment of(ESQLDataObjectType role, String stored) { 088 return new Segment(role, stored); 089 } 090 091 public static Segment server(String stored) { return of(ESQLDataObjectType.dotServer, stored); } 092 public static Segment catalog(String stored) { return of(ESQLDataObjectType.dotCatalog, stored); } 093 public static Segment schema(String stored) { return of(ESQLDataObjectType.dotSchema, stored); } 094 public static Segment table(String stored) { return of(ESQLDataObjectType.dotTable, stored); } 095 public static Segment column(String stored) { return of(ESQLDataObjectType.dotColumn, stored); } 096 public static Segment function(String stored) { return of(ESQLDataObjectType.dotFunction, stored); } 097 public static Segment procedure(String stored) { return of(ESQLDataObjectType.dotProcedure, stored); } 098 099 public ESQLDataObjectType getRole() { return role; } 100 public String getStored() { return stored; } 101 } 102 103 /** Outcome of a generation: either the SQL text, or the reason there is none. */ 104 public static final class Result { 105 private final String sql; 106 private final Failure failure; 107 private final int failedSegmentIndex; 108 private final String detail; 109 110 private Result(String sql, Failure failure, int failedSegmentIndex, String detail) { 111 this.sql = sql; 112 this.failure = failure; 113 this.failedSegmentIndex = failedSegmentIndex; 114 this.detail = detail; 115 } 116 117 static Result ok(String sql) { 118 return new Result(sql, null, -1, null); 119 } 120 121 static Result fail(Failure failure, int index, String detail) { 122 return new Result(null, failure, index, detail); 123 } 124 125 public boolean isOk() { return failure == null; } 126 127 /** The generated SQL name, or {@code null} when {@link #isOk()} is false. */ 128 public String getSql() { return sql; } 129 130 /** The failure reason, or {@code null} on success. */ 131 public Failure getFailure() { return failure; } 132 133 /** Index of the offending segment, or {@code -1} when not segment-specific. */ 134 public int getFailedSegmentIndex() { return failedSegmentIndex; } 135 136 /** Human-readable explanation of the failure, or {@code null} on success. */ 137 public String getDetail() { return detail; } 138 139 /** The generated SQL name; throws when generation failed. */ 140 public String getSqlOrThrow() { 141 if (!isOk()) { 142 throw new IllegalStateException("SQL name generation failed: " + failure 143 + (failedSegmentIndex >= 0 ? " at segment " + failedSegmentIndex : "") 144 + (detail != null ? " (" + detail + ")" : "")); 145 } 146 return sql; 147 } 148 149 @Override 150 public String toString() { 151 return isOk() ? "Result{sql=" + sql + "}" 152 : "Result{failure=" + failure + ", segment=" + failedSegmentIndex + ", detail=" + detail + "}"; 153 } 154 } 155 156 private SqlNameGenerator() { 157 } 158 159 /** Generate with every segment delimited. */ 160 public static Result generate(EDbVendor vendor, Segment... segments) { 161 return generate(vendor, QuotePolicy.ALWAYS, 162 segments == null ? null : Arrays.asList(segments)); 163 } 164 165 public static Result generate(EDbVendor vendor, QuotePolicy policy, Segment... segments) { 166 return generate(vendor, policy, segments == null ? null : Arrays.asList(segments)); 167 } 168 169 /** 170 * Generate a SQL name. 171 * 172 * @param vendor target dialect (non-null) 173 * @param policy bare-versus-delimited choice (non-null) 174 * @param segments stored name segments, outermost first 175 * @return the SQL name, or the reason none could be produced 176 */ 177 public static Result generate(EDbVendor vendor, QuotePolicy policy, List<Segment> segments) { 178 Objects.requireNonNull(vendor, "vendor"); 179 Objects.requireNonNull(policy, "policy"); 180 if (segments == null || segments.isEmpty()) { 181 return Result.fail(Failure.NO_SEGMENTS, -1, "at least one segment is required"); 182 } 183 184 IdentifierProfile profile = IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults()); 185 List<String> rendered = new ArrayList<String>(segments.size()); 186 187 for (int i = 0; i < segments.size(); i++) { 188 Segment seg = segments.get(i); 189 if (seg == null || seg.role == null || seg.stored == null) { 190 return Result.fail(Failure.NULL_SEGMENT, i, "segment, role and stored name must be non-null"); 191 } 192 if (seg.stored.isEmpty()) { 193 return Result.fail(Failure.EMPTY_SEGMENT, i, 194 "empty stored name; qualification is never filled in on the caller's behalf"); 195 } 196 197 if (policy == QuotePolicy.WHEN_NEEDED && bareIsProvenSafe(vendor, profile, seg)) { 198 rendered.add(seg.stored); 199 continue; 200 } 201 202 if (!IdentifierCodec.hasQuotedForm(profile, seg.role)) { 203 // The vendor (SOQL) has no delimited form, so the bare spelling is the 204 // ONLY spelling. The keyword condition is dropped here on purpose: in 205 // such a dialect an object named after a keyword could never be 206 // referenced, so the stored name existing at all proves the bare word 207 // is usable. Character-level validity is still required. 208 if (IdentifierCodec.isVendorValidSpelling(profile, seg.role, seg.stored)) { 209 rendered.add(seg.stored); 210 continue; 211 } 212 return Result.fail(Failure.NO_QUOTED_FORM, i, 213 "name needs delimiting but " + vendor + " has no delimited form for " + seg.role); 214 } 215 216 String quoted = IdentifierCodec.encodeStored(profile, seg.role, seg.stored); 217 if (!IdentifierCodec.isVendorValidSpelling(profile, seg.role, quoted)) { 218 return Result.fail(Failure.NOT_REPRESENTABLE, i, 219 "no valid " + vendor + " spelling exists for this stored name"); 220 } 221 String decoded; 222 try { 223 decoded = IdentifierCodec.decodeLexical(profile, seg.role, quoted); 224 } catch (RuntimeException e) { 225 return Result.fail(Failure.ROUND_TRIP_MISMATCH, i, "encoded form does not decode: " + e.getMessage()); 226 } 227 // Transport verification of raw text, not an identifier comparison: the 228 // decode must reproduce the stored characters exactly. 229 if (!decoded.equals(seg.stored)) { 230 return Result.fail(Failure.ROUND_TRIP_MISMATCH, i, "encoded form decodes to a different name"); 231 } 232 rendered.add(quoted); 233 } 234 235 StringBuilder sb = new StringBuilder(); 236 for (int i = 0; i < rendered.size(); i++) { 237 if (i > 0) { 238 sb.append('.'); 239 } 240 sb.append(rendered.get(i)); 241 } 242 return Result.ok(sb.toString()); 243 } 244 245 /** 246 * Is the bare spelling PROVEN to denote the same object as the stored name? 247 * 248 * <p>All five must hold; any doubt means "delimit", because the delimited 249 * spelling is always correct and a wrong bare spelling silently names a 250 * different object.</p> 251 * <ol> 252 * <li>The role has a bare spelling at all: a DAX column is always written 253 * {@code [Name]} ({@link IdentifierCodec#requiresDelimiters}).</li> 254 * <li>The stored text does not itself read as a delimited identifier. MSSQL 255 * stored name {@code [db name]} (brackets being real content) written 256 * bare would be parsed as the different object {@code db name}.</li> 257 * <li>The bare spelling is a valid unquoted identifier for the vendor.</li> 258 * <li>The bare spelling resolves to the same object as the delimited one — 259 * Oracle stored {@code Amount} written bare folds to {@code AMOUNT}.</li> 260 * <li>The vendor's keyword list is available AND the word is not on it. With 261 * no keyword list the word cannot be proven safe, so it is delimited.</li> 262 * </ol> 263 */ 264 static boolean bareIsProvenSafe(EDbVendor vendor, IdentifierProfile profile, Segment seg) { 265 if (IdentifierCodec.requiresDelimiters(profile, seg.role)) { 266 // The role has no bare spelling at all (a DAX column is always [Name]). 267 return false; 268 } 269 String bare = seg.stored; 270 if (IdentifierCodec.isQuoted(profile, seg.role, bare)) { 271 return false; 272 } 273 if (!IdentifierCodec.isVendorValidSpelling(profile, seg.role, bare)) { 274 return false; 275 } 276 if (IdentifierCodec.hasQuotedForm(profile, seg.role)) { 277 String quoted = IdentifierCodec.encodeStored(profile, seg.role, bare); 278 if (!SQLUtil.sameName(vendor, seg.role, bare, quoted)) { 279 return false; 280 } 281 } 282 // checkKeywordType splits its input on commas; a valid bare spelling 283 // contains none, so exactly one entry comes back when a list exists. 284 Map<String, keywordChecker.keywordType> kw = keywordChecker.checkKeywordType(bare, vendor); 285 if (kw == null || kw.size() != 1) { 286 return false; 287 } 288 return kw.values().iterator().next() == keywordChecker.keywordType.not_a_keyword; 289 } 290 291 /** 292 * Re-spell ONE identifier that was read from SQL text, as valid SQL naming the same object. 293 * 294 * <p>This is the entry point for a name that HAS a source spelling, as opposed to 295 * {@link #generate}, which starts from a stored name. The difference matters for case:</p> 296 * <ul> 297 * <li><b>Written without delimiters.</b> The script parsed, so the bare word is already 298 * proven to be valid SQL for this object, and re-emitting it bare cannot name anything 299 * else. It is returned as written. It is deliberately NOT case-folded: that would need 300 * the catalog's real stored case, and the name service's fold is a comparison key, not 301 * a storage rule (Teradata is modeled with PostgreSQL's lower-casing although it stores 302 * names as created). Folding here turned {@code DB51_CAD} into {@code db51_cad}.</li> 303 * <li><b>Written with delimiters.</b> Decoding gives the exact stored text, and 304 * {@link #generate} decides whether the delimiters can be dropped.</li> 305 * </ul> 306 * 307 * <p>So two references to one object may still re-spell differently ({@code Sales} and 308 * {@code SALES} on Oracle). Making a LISTING show one of them is the job of identity 309 * grouping plus a display-name policy, which can consult the catalog; it is not something a 310 * single spelling can decide.</p> 311 */ 312 public static Result respell(EDbVendor vendor, QuotePolicy policy, ESQLDataObjectType role, String lexical) { 313 Objects.requireNonNull(vendor, "vendor"); 314 Objects.requireNonNull(policy, "policy"); 315 Objects.requireNonNull(role, "role"); 316 if (lexical == null) return Result.fail(Failure.NULL_SEGMENT, 0, "lexical spelling is null"); 317 if (lexical.isEmpty()) return Result.fail(Failure.EMPTY_SEGMENT, 0, "lexical spelling is empty"); 318 319 IdentifierProfile profile = IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults()); 320 boolean quoted = IdentifierCodec.isQuoted(profile, role, lexical); 321 322 if (!quoted && IdentifierCodec.isVendorValidSpelling(profile, role, lexical)) { 323 if (policy == QuotePolicy.WHEN_NEEDED || !IdentifierCodec.hasQuotedForm(profile, role)) { 324 return Result.ok(lexical); 325 } 326 // ALWAYS: a delimited spelling is wanted, and it must be PROVEN to name the same 327 // object as the bare word. Try the text as written first (right wherever case is 328 // not observable through quoting), then the name service's fold (right for Oracle). 329 String asWritten = IdentifierCodec.encodeStored(profile, role, lexical); 330 if (SQLUtil.sameName(vendor, role, lexical, asWritten)) { 331 return Result.ok(asWritten); 332 } 333 try { 334 String folded = new IdentifierService(profile, null).normalizeSegment(lexical, role); 335 if (folded != null && !folded.isEmpty()) { 336 String foldedQuoted = IdentifierCodec.encodeStored(profile, role, folded); 337 if (SQLUtil.sameName(vendor, role, lexical, foldedQuoted)) { 338 return Result.ok(foldedQuoted); 339 } 340 } 341 } catch (RuntimeException e) { 342 // fall through 343 } 344 return Result.ok(lexical); // no provable delimited form; the bare word is still valid 345 } 346 347 if (!quoted) { 348 // Neither a valid bare word nor a delimited spelling this vendor recognizes: a DAX 349 // string-named column ("Name"), a composite listing label. It is not an identifier 350 // this class can vouch for, so it is refused rather than wrapped in delimiters. 351 return Result.fail(Failure.NOT_REPRESENTABLE, 0, "not an identifier spelling for " + vendor); 352 } 353 String stored; 354 try { 355 stored = IdentifierCodec.decodeLexical(profile, role, lexical); 356 } catch (RuntimeException e) { 357 return Result.fail(Failure.NOT_REPRESENTABLE, 0, "malformed delimited spelling: " + e.getMessage()); 358 } 359 return generate(vendor, policy, Segment.of(role, stored)); 360 } 361 362 /** Convenience for callers holding an immutable segment list. */ 363 public static Result generate(EDbVendor vendor, List<Segment> segments) { 364 return generate(vendor, QuotePolicy.ALWAYS, 365 segments == null ? null : Collections.unmodifiableList(segments)); 366 } 367}