001package gudusoft.gsqlparser.resolver2.format;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.nodes.TObjectName;
005import gudusoft.gsqlparser.nodes.TTable;
006import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
007import gudusoft.gsqlparser.sqlenv.IdentifierCodec;
008import gudusoft.gsqlparser.sqlenv.IdentifierProfile;
009import gudusoft.gsqlparser.sqlenv.IdentifierService;
010import gudusoft.gsqlparser.sqlenv.SqlNameGenerator;
011import gudusoft.gsqlparser.util.SQLUtil;
012
013import java.util.List;
014
015/**
016 * Normalizes identifier names for display output.
017 *
018 * <p>This class provides display-focused normalization that is separate from
019 * the matching/equality normalization used during name resolution. The key
020 * principle is:</p>
021 *
022 * <ul>
023 *   <li><b>Matching normalization</b> (in {@code IdentifierService}): applies case folding
024 *       and other transformations to create a canonical key for comparison</li>
025 *   <li><b>Display normalization</b> (this class): strips delimiters but preserves
026 *       the original case as written by the user</li>
027 * </ul>
028 *
029 * <h3>Usage Example:</h3>
030 * <pre>
031 * DisplayNameNormalizer normalizer = new DisplayNameNormalizer(EDbVendor.dbvbigquery);
032 * normalizer.setMode(DisplayNameMode.DISPLAY);
033 *
034 * // Strip backticks, preserve case
035 * String display = normalizer.normalizeTableName("`kalyan-DB`.`test-schema`.`t1`");
036 * // Result: "kalyan-DB.test-schema.t1"
037 * </pre>
038 *
039 * @see DisplayNameMode
040 * @see DisplayNamePolicy
041 */
042public class DisplayNameNormalizer {
043
044    private final EDbVendor vendor;
045    private DisplayNameMode mode = DisplayNameMode.DISPLAY;
046    private boolean stripDelimiters = true;
047
048    /** How much delimiting {@link DisplayNameMode#SQL_RENDER} applies; see {@link #setQuotePolicy}. */
049    private SqlNameGenerator.QuotePolicy quotePolicy = SqlNameGenerator.QuotePolicy.WHEN_NEEDED;
050
051    /**
052     * Vendor identifier profile, built lazily and reused. This is what makes the
053     * delimiter rules come from the unified name service instead of this class.
054     */
055    private IdentifierProfile profileCache;
056
057    /** Name service used for CANONICAL folding, built lazily and reused. */
058    private IdentifierService serviceCache;
059
060    private IdentifierProfile profile() {
061        if (profileCache == null) {
062            profileCache = IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults());
063        }
064        return profileCache;
065    }
066
067    private IdentifierService service() {
068        if (serviceCache == null) {
069            // Collator provider is SQL Server specific and nullable; display folding
070            // does not need collation, which is a comparison concern.
071            serviceCache = new IdentifierService(profile(), null);
072        }
073        return serviceCache;
074    }
075
076    /**
077     * Create a normalizer for the specified database vendor.
078     *
079     * @param vendor the database vendor (determines delimiter style)
080     */
081    public DisplayNameNormalizer(EDbVendor vendor) {
082        this.vendor = vendor != null ? vendor : EDbVendor.dbvansi;
083    }
084
085    /**
086     * Create a normalizer with default vendor (ANSI).
087     */
088    public DisplayNameNormalizer() {
089        this(EDbVendor.dbvansi);
090    }
091
092    /**
093     * Get the current display mode.
094     */
095    public DisplayNameMode getMode() {
096        return mode;
097    }
098
099    /**
100     * Set the display mode.
101     *
102     * @param mode the display mode
103     * @return this normalizer for chaining
104     */
105    public DisplayNameNormalizer setMode(DisplayNameMode mode) {
106        this.mode = mode != null ? mode : DisplayNameMode.DISPLAY;
107        return this;
108    }
109
110    public SqlNameGenerator.QuotePolicy getQuotePolicy() {
111        return quotePolicy;
112    }
113
114    /**
115     * Choose the spelling {@link DisplayNameMode#SQL_RENDER} generates. "Valid SQL that names
116     * this object" is a constraint, not a value: {@code dbo} and {@code [dbo]} both satisfy it.
117     * This parameter is what makes the answer unique.
118     *
119     * <ul>
120     *   <li>{@code WHEN_NEEDED} (default): delimit only where the bare word would be invalid,
121     *       would fold to another object, or is a keyword — {@code dbo.SalesOrder},
122     *       {@code [Order ID]}.</li>
123     *   <li>{@code ALWAYS}: delimit every segment — {@code [dbo].[SalesOrder]}.</li>
124     * </ul>
125     */
126    public DisplayNameNormalizer setQuotePolicy(SqlNameGenerator.QuotePolicy quotePolicy) {
127        this.quotePolicy = quotePolicy != null ? quotePolicy : SqlNameGenerator.QuotePolicy.WHEN_NEEDED;
128        return this;
129    }
130
131    /**
132     * Check if delimiters should be stripped.
133     */
134    public boolean isStripDelimiters() {
135        return stripDelimiters;
136    }
137
138    /**
139     * Set whether to strip delimiters.
140     *
141     * @param stripDelimiters true to strip delimiters
142     * @return this normalizer for chaining
143     */
144    public DisplayNameNormalizer setStripDelimiters(boolean stripDelimiters) {
145        this.stripDelimiters = stripDelimiters;
146        return this;
147    }
148
149    /**
150     * Normalize a table name for display.
151     *
152     * @param table the table object
153     * @return the normalized display name
154     */
155    public String normalizeTableName(TTable table) {
156        if (table == null) {
157            return null;
158        }
159
160        // Try to get the full name from the table
161        String fullName = table.getFullName();
162        if (fullName != null && !fullName.isEmpty()) {
163            return normalizeQualifiedName(fullName, ESQLDataObjectType.dotTable);
164        }
165
166        // Fallback to table name
167        String name = table.getName();
168        return name != null ? normalizeIdentifier(name, ESQLDataObjectType.dotTable) : null;
169    }
170
171    /**
172     * Normalize an object name (table or column) for display.
173     *
174     * @param objectName the object name node
175     * @return the normalized display name
176     */
177    public String normalizeObjectName(TObjectName objectName) {
178        if (objectName == null) {
179            return null;
180        }
181
182        // Use toString() which gives the full representation
183        String name = objectName.toString();
184        return normalizeQualifiedName(name);
185    }
186
187    /**
188     * Normalize a column name for display.
189     *
190     * @param columnName the column name (may include table prefix)
191     * @return the normalized display name
192     */
193    public String normalizeColumnName(String columnName) {
194        if (columnName == null || columnName.isEmpty()) {
195            return columnName;
196        }
197        return normalizeIdentifier(columnName, ESQLDataObjectType.dotColumn);
198    }
199
200    /**
201     * Normalize a qualified name (e.g., schema.table.column) for display.
202     *
203     * <p>This method handles multi-part names by normalizing each segment
204     * separately while preserving the dot separators.</p>
205     *
206     * @param qualifiedName the qualified name
207     * @return the normalized display name
208     */
209    public String normalizeQualifiedName(String qualifiedName) {
210        return normalizeQualifiedName(qualifiedName, ESQLDataObjectType.dotColumn);
211    }
212
213    /**
214     * Normalize a qualified name for display, stating the syntactic role of its
215     * segments so delimiter rules come from the vendor profile.
216     *
217     * <p>Every segment is decoded with the same {@code objectType}, which is what
218     * this class has always done. Per-segment role typing (catalog / schema / leaf)
219     * is a separate change: it would alter output for vendors whose quote rules
220     * differ by role, so it needs its own measurement.</p>
221     *
222     * @param qualifiedName the qualified name
223     * @param objectType syntactic role of the identifier segments
224     * @return the normalized display name
225     */
226    public String normalizeQualifiedName(String qualifiedName, ESQLDataObjectType objectType) {
227        if (qualifiedName == null || qualifiedName.isEmpty()) {
228            return qualifiedName;
229        }
230
231        // Segmentation belongs to the name service, not to this class: it knows
232        // which dots are separators and which sit inside a delimited segment, and
233        // it carries the vendor quirks (MSSQL "db..table", BigQuery backticks).
234        List<String> parts = SQLUtil.parseNames(qualifiedName, vendor);
235        if (parts == null || parts.isEmpty()) {
236            return normalizeIdentifier(qualifiedName, objectType);
237        }
238
239        // The name service may hand back segments with their delimiters already removed
240        // (BigQuery writes `proj.ds.tbl` inside ONE pair of backticks). Those are stored names,
241        // not SQL spellings, and SQL_RENDER has to write them back from that starting point.
242        boolean storedSegments = false;
243        if (mode == DisplayNameMode.SQL_RENDER) {
244            StringBuilder rejoined = new StringBuilder();
245            for (int i = 0; i < parts.size(); i++) {
246                if (i > 0) rejoined.append(".");
247                rejoined.append(parts.get(i));
248            }
249            storedSegments = !rejoined.toString().equals(qualifiedName);   // exact text, not an identifier compare
250        }
251
252        StringBuilder result = new StringBuilder();
253        for (int i = 0; i < parts.size(); i++) {
254            if (i > 0) {
255                result.append(".");
256            }
257            result.append(storedSegments
258                    ? renderStoredAsSql(parts.get(i), objectType)
259                    : normalizeIdentifier(parts.get(i), objectType));
260        }
261
262        return result.toString();
263    }
264
265    /**
266     * Normalize a single identifier (without dots) for display.
267     *
268     * @param identifier the identifier
269     * @return the normalized identifier
270     */
271    public String normalizeIdentifier(String identifier) {
272        return normalizeIdentifier(identifier, ESQLDataObjectType.dotColumn);
273    }
274
275    /**
276     * Normalize a single identifier (without dots) for display, stating its
277     * syntactic role so delimiter rules come from the vendor profile.
278     *
279     * @param identifier the identifier
280     * @param objectType syntactic role of the identifier
281     * @return the normalized identifier
282     */
283    public String normalizeIdentifier(String identifier, ESQLDataObjectType objectType) {
284        if (identifier == null || identifier.isEmpty()) {
285            return identifier;
286        }
287
288        switch (mode) {
289            case SQL_RENDER:
290                return renderAsSql(identifier, objectType);
291
292            case CANONICAL:
293                // Undelimit AND apply the vendor's case folding, via the name service.
294                // This is deliberately NOT a rendered CanonKey: a comparison key and a
295                // display string are two different contracts, and nothing may re-derive
296                // identity by comparing what this returns.
297                return canonicalize(identifier, objectType);
298
299            case DISPLAY:
300            default:
301                // Strip delimiters, preserve case
302                return stripDelimiters ? stripAllDelimiters(identifier, objectType) : identifier;
303        }
304    }
305
306    /**
307     * Strip all SQL delimiters from an identifier.
308     *
309     * <p>Handles multiple delimiter styles:</p>
310     * <ul>
311     *   <li>Double quotes: {@code "name"} → {@code name}</li>
312     *   <li>Backticks: {@code `name`} → {@code name}</li>
313     *   <li>Square brackets: {@code [name]} → {@code name}</li>
314     *   <li>Single quotes (for some vendors): {@code 'name'} → {@code name}</li>
315     * </ul>
316     *
317     * @param identifier the identifier (may be quoted)
318     * @return the identifier without delimiters
319     */
320    /**
321     * Write a name as valid SQL for the vendor: GENERATED from the stored name, not echoed from
322     * the reference.
323     *
324     * <p>Echoing kept whatever delimiters the author happened to type, so one object had as many
325     * SQL_RENDER spellings as the script had ways of writing it ({@code dbo} and {@code [dbo]}).
326     * {@link SqlNameGenerator#respell} instead decides the delimiters by rule, governed by
327     * {@link #setQuotePolicy}: a delimited name is decoded to its exact stored text and
328     * re-delimited only if it must be.</p>
329     *
330     * <p>A name written WITHOUT delimiters is returned as written. It is already valid SQL for
331     * that object, and folding its case would need the catalog's stored case, which SQL text
332     * does not reveal. So {@code Sales} and {@code SALES} may both come back for one Oracle
333     * table; a listing shows one of them through identity grouping and a
334     * {@link DisplayNamePolicy}.</p>
335     */
336    /** Write a STORED name (no source spelling) as SQL; shown as given when none can be generated. */
337    private String renderStoredAsSql(String stored, ESQLDataObjectType objectType) {
338        if (stored == null || stored.isEmpty()) return stored;
339        SqlNameGenerator.Result r = SqlNameGenerator.generate(vendor, quotePolicy,
340                SqlNameGenerator.Segment.of(objectType, stored));
341        return r.isOk() ? r.getSql() : stored;
342    }
343
344    private String renderAsSql(String identifier, ESQLDataObjectType objectType) {
345        // A bare asterisk in SQL text can only be the wildcard: a column that is really named
346        // "*" cannot be written without delimiters. This reads the LEXICAL input, so it does not
347        // confuse the two the way a rendered name would.
348        if ("*".equals(identifier)) {
349            return identifier;
350        }
351        try {
352            SqlNameGenerator.Result r = SqlNameGenerator.respell(vendor, quotePolicy, objectType, identifier);
353            if (r.isOk()) {
354                return r.getSql();
355            }
356        } catch (RuntimeException e) {
357            // Malformed spelling: fall through.
358        }
359        // Nothing valid could be generated. Show the reference as written rather than drop it.
360        return identifier;
361    }
362
363    /**
364     * Render an identifier in the vendor's canonical form: delimiters removed and
365     * case folded by the vendor's own rule (Oracle {@code MyTable} -> {@code MYTABLE},
366     * while {@code "MyTable"} keeps its case because quoting is part of identity).
367     *
368     * <p>The folding rule belongs to {@link IdentifierService}; this class only asks
369     * for it. The result is a DISPLAY string, not a comparison key — use
370     * {@code SQLUtil.sameName} / {@code canonKey} to compare names, never this.</p>
371     */
372    private String canonicalize(String identifier, ESQLDataObjectType objectType) {
373        try {
374            String folded = service().normalizeSegment(identifier, objectType);
375            if (folded != null && !folded.isEmpty()) {
376                return folded;
377            }
378        } catch (RuntimeException e) {
379            // Malformed spelling: fall back to the undelimited form rather than
380            // dropping the name from the listing.
381        }
382        return stripDelimiters ? stripAllDelimiters(identifier, objectType) : identifier;
383    }
384
385    public String stripAllDelimiters(String identifier) {
386        return stripAllDelimiters(identifier, ESQLDataObjectType.dotColumn);
387    }
388
389    /**
390     * Strip SQL delimiters from an identifier, using the vendor's real quote rules.
391     *
392     * <p>The decision of what counts as a delimiter belongs to the unified name
393     * service, not to the display layer, so this delegates to
394     * {@link IdentifierCodec#decodeLexical}. Two display-specific rules are layered
395     * on top of it, and both exist to keep output byte-identical to the behavior
396     * this class had before it delegated:</p>
397     *
398     * <ul>
399     *   <li><b>Never fail on a malformed spelling.</b> {@code decodeLexical} throws
400     *       {@code MalformedIdentifierException} for input such as {@code "unclosed}.
401     *       A display layer must not abort a whole Tables/Fields listing because one
402     *       name is malformed, so a throw falls through to the legacy strip.</li>
403     *   <li><b>Never display an empty name.</b> {@code decodeLexical} decodes
404     *       {@code ""}, {@code []} and {@code ''} to the empty string. Rendering a
405     *       column as nothing is worse than rendering its raw spelling, so an empty
406     *       decode falls through as well.</li>
407     * </ul>
408     *
409     * <p><b>Known gap, deliberately preserved.</b> The legacy fallback also strips
410     * delimiters the vendor does not own (backticks for Oracle, brackets for MySQL,
411     * single quotes for everyone). Routing those through the profile instead would
412     * be the semantically correct behavior, but it changes output for 14 of 15
413     * vendors measured, so it is a separate decision with its own golden review —
414     * not something to slip in under a refactor.</p>
415     *
416     * @param identifier the identifier (may be quoted)
417     * @param objectType syntactic role of the identifier (quote rules can depend on it)
418     * @return the identifier without delimiters
419     */
420    public String stripAllDelimiters(String identifier, ESQLDataObjectType objectType) {
421        if (identifier == null || identifier.isEmpty()) {
422            return identifier;
423        }
424
425        try {
426            String decoded = IdentifierCodec.decodeLexical(profile(), objectType, identifier);
427            // An empty decode means the spelling was an empty delimited name such as
428            // "" or []. Rendering a column as nothing is worse than rendering what
429            // was written, so show the spelling instead. This is a display policy
430            // applied to the codec's answer, not a second opinion about delimiters.
431            return decoded.isEmpty() ? identifier : decoded;
432        } catch (RuntimeException e) {
433            // Malformed spelling (an unterminated delimiter). A display layer must
434            // not abort a whole listing over one bad name, so show it as written.
435            return identifier;
436        }
437    }
438
439
440
441
442    /**
443     * Get a display name for a table, handling various TTable configurations.
444     *
445     * @param table the table
446     * @return the display name
447     */
448    public String getTableDisplayName(TTable table) {
449        if (table == null) {
450            return null;
451        }
452
453        // Build the full qualified name from parts if available
454        StringBuilder sb = new StringBuilder();
455
456        // Server/catalog
457        String server = table.getPrefixServer();
458        if (server != null && !server.isEmpty()) {
459            sb.append(normalizeIdentifier(server, ESQLDataObjectType.dotServer));
460            sb.append(".");
461        }
462
463        // Database
464        String database = table.getPrefixDatabase();
465        if (database != null && !database.isEmpty()) {
466            sb.append(normalizeIdentifier(database, ESQLDataObjectType.dotCatalog));
467            sb.append(".");
468        }
469
470        // Schema
471        String schema = table.getPrefixSchema();
472        if (schema != null && !schema.isEmpty()) {
473            sb.append(normalizeIdentifier(schema, ESQLDataObjectType.dotSchema));
474            sb.append(".");
475        }
476
477        // Table name
478        String tableName = table.getName();
479        if (tableName != null && !tableName.isEmpty()) {
480            sb.append(normalizeIdentifier(tableName, ESQLDataObjectType.dotTable));
481        }
482
483        String result = sb.toString();
484
485        // If we got nothing useful, fall back to getFullName
486        if (result.isEmpty() || result.equals(".")) {
487            String fullName = table.getFullName();
488            if (fullName != null) {
489                return normalizeQualifiedName(fullName, ESQLDataObjectType.dotTable);
490            }
491        }
492
493        return result.isEmpty() ? null : result;
494    }
495
496    @Override
497    public String toString() {
498        return String.format("DisplayNameNormalizer{vendor=%s, mode=%s, stripDelimiters=%s}",
499                vendor, mode, stripDelimiters);
500    }
501}