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}