001package gudusoft.gsqlparser.resolver2.format;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
005import gudusoft.gsqlparser.nodes.TObjectName;
006import gudusoft.gsqlparser.sqlenv.IdentifierService;
007import gudusoft.gsqlparser.sqlenv.SqlNameGenerator;
008import gudusoft.gsqlparser.sqlenv.TSQLEnv;
009import gudusoft.gsqlparser.sqlenv.TSQLTable;
010import gudusoft.gsqlparser.util.SQLUtil;
011
012import java.util.ArrayList;
013import java.util.Collections;
014import java.util.Comparator;
015import java.util.LinkedHashMap;
016import java.util.List;
017import java.util.Map;
018
019/**
020 * Collects the names a listing will print and de-duplicates them by OBJECT IDENTITY, then
021 * picks which spelling to print with a {@link DisplayNamePolicy}.
022 *
023 * <p>The listings used to de-duplicate rendered strings case-insensitively. Rendered text has
024 * already lost what decides identity, so that was wrong in both directions:</p>
025 * <ul>
026 *   <li>it MERGED distinct objects. BigQuery table names are case-sensitive, so
027 *       {@code Sales} and {@code SALES} are two tables, yet only one reached the list —
028 *       a silently missing table;</li>
029 *   <li>it SPLIT one object. Oracle {@code Sales}, {@code SALES} and {@code "SALES"} are one
030 *       table, yet the quoted spelling printed as a second row.</li>
031 * </ul>
032 *
033 * <p>Identity comes from the unified name service ({@link IdentifierService}), per vendor and
034 * per object type — BigQuery tables compare case-sensitively while its columns do not, so the
035 * caller states the type and nothing here guesses it from text.</p>
036 */
037public final class IdentityNameList {
038
039    private static final class Occurrence {
040        final String rendered;
041        final boolean definitionSite;
042        final String catalogRendered;
043
044        Occurrence(String rendered, boolean definitionSite, String catalogRendered) {
045            this.rendered = rendered;
046            this.definitionSite = definitionSite;
047            this.catalogRendered = catalogRendered;
048        }
049    }
050
051    private final EDbVendor vendor;
052    private final DisplayNamePolicy policy;
053    /** Insertion-ordered so "first occurrence" means source order. */
054    private final Map<Object, List<Occurrence>> groups = new LinkedHashMap<Object, List<Occurrence>>();
055
056    public IdentityNameList(EDbVendor vendor, DisplayNamePolicy policy) {
057        this.vendor = vendor != null ? vendor : EDbVendor.dbvansi;
058        this.policy = policy != null ? policy : DisplayNamePolicy.PREFER_DEFINITION_SITE;
059    }
060
061    public void clear() {
062        groups.clear();
063    }
064
065    public boolean isEmpty() {
066        return groups.isEmpty();
067    }
068
069    /**
070     * Record one occurrence of a name.
071     *
072     * @param rendered        the text the listing would print for this occurrence, decorations
073     *                        included
074     * @param lexical         the name as written in SQL (may be qualified); identity is derived
075     *                        from this, never from {@code rendered}
076     * @param objectType      syntactic role of {@code lexical}
077     * @param decoration      any suffix the listing appended ({@code "(tetSelect)"}); two
078     *                        occurrences of one object under different decorations are
079     *                        different lines. May be null.
080     * @param definitionSite  true when this occurrence is where the object is defined
081     * @param catalogRendered how the listing would print the catalog's stored name for this
082     *                        object, or null when no catalog metadata is known
083     */
084    public void add(String rendered, String lexical, ESQLDataObjectType objectType,
085                    String decoration, boolean definitionSite, String catalogRendered) {
086        if (rendered == null) return;
087        Object identity = identityOf(lexical, objectType, rendered);
088        Object key = java.util.Arrays.asList(identity, decoration == null ? "" : decoration);
089        List<Occurrence> list = groups.get(key);
090        if (list == null) {
091            list = new ArrayList<Occurrence>();
092            groups.put(key, list);
093        }
094        list.add(new Occurrence(rendered, definitionSite, catalogRendered));
095    }
096
097    /**
098     * How a listing prints the catalog's stored name for a table, or null when there is no
099     * catalog entry. One rule shared by every writer, so the metadata rung of the policy cannot
100     * differ between them.
101     *
102     * <p>Only an unqualified reference is answered: a stored leaf name says nothing about how
103     * the qualifiers should be spelled, and none are invented. Under
104     * {@link DisplayNameMode#SQL_RENDER} the stored name is written back as SQL by
105     * {@link SqlNameGenerator}, delimited only when it must be.</p>
106     */
107    public static String renderCatalogTableName(EDbVendor vendor, TSQLEnv sqlEnv, TObjectName tableName,
108                                                DisplayNameMode mode, SqlNameGenerator.QuotePolicy quotePolicy) {
109        if (vendor == null || sqlEnv == null || tableName == null) return null;
110        String lexical = tableName.toString();
111        if (lexical == null || lexical.isEmpty()) return null;
112        TSQLTable found;
113        try {
114            if (SQLUtil.parseNames(lexical, vendor).size() != 1) return null;
115            found = sqlEnv.searchTable(tableName);
116        } catch (RuntimeException e) {
117            return null;
118        }
119        if (found == null || found.getName() == null || found.getName().isEmpty()) return null;
120        if (mode != DisplayNameMode.SQL_RENDER) return found.getName();
121        SqlNameGenerator.Result r = SqlNameGenerator.generate(vendor,
122                quotePolicy != null ? quotePolicy : SqlNameGenerator.QuotePolicy.WHEN_NEEDED,
123                SqlNameGenerator.Segment.table(found.getName()));
124        return r.isOk() ? r.getSql() : null;
125    }
126
127    /**
128     * Record a line whose identity the caller cannot state (a synthesized or composite line).
129     * It is de-duplicated only against an identical line, so it can never swallow a named
130     * object and never be swallowed by one.
131     */
132    public void addOpaque(String rendered) {
133        if (rendered == null) return;
134        Object key = java.util.Arrays.asList("opaque", rendered);
135        List<Occurrence> list = groups.get(key);
136        if (list == null) {
137            list = new ArrayList<Occurrence>();
138            groups.put(key, list);
139        }
140        list.add(new Occurrence(rendered, false, null));
141    }
142
143    private Object identityOf(String lexical, ESQLDataObjectType objectType, String rendered) {
144        if (lexical != null && !lexical.isEmpty() && objectType != null) {
145            try {
146                return IdentifierService.qualifiedCanonKeyFromLexicalStatic(vendor, objectType, lexical);
147            } catch (RuntimeException e) {
148                // A spelling the name service rejects still has to be listed. Fall through to
149                // an exact-text identity: it can only merge with a byte-identical spelling.
150            }
151        }
152        return java.util.Arrays.asList("text", lexical != null ? lexical : rendered);
153    }
154
155    /**
156     * The lines to print: exactly ONE per object in every mode, sorted case-insensitively as
157     * the listings always were.
158     *
159     * <p>A {@link DisplayNameMode} decides HOW a name is written and a
160     * {@link DisplayNamePolicy} decides WHICH occurrence supplies the spelling; the two are
161     * orthogonal and no mode is exempt from the policy. {@link DisplayNameMode#SQL_RENDER}
162     * therefore prints the policy-chosen occurrence with its SQL delimiters kept, rather than
163     * one row per spelling: a listing row stands for an object, and how an object with several
164     * spellings is shown is the policy's question, not the mode's.</p>
165     *
166     * @param mode accepted for symmetry with the writers; the rendering was already applied
167     *             when each occurrence was added
168     */
169    public List<String> resolve(DisplayNameMode mode) {
170        List<String> out = new ArrayList<String>();
171        for (List<Occurrence> occurrences : groups.values()) {
172            out.add(choose(occurrences));
173        }
174        Collections.sort(out, new Comparator<String>() {
175            public int compare(String a, String b) {
176                // non-identifier-compare: presentation order of already de-duplicated lines
177                int c = String.CASE_INSENSITIVE_ORDER.compare(a, b);
178                return c != 0 ? c : a.compareTo(b);
179            }
180        });
181        return out;
182    }
183
184    /**
185     * Apply the policy's fallback chain: definition site, then catalog metadata, then first
186     * occurrence. Metadata sits in the middle because without a definition site the remaining
187     * spellings are arbitrary references, and "first occurrence" alone would make the printed
188     * name depend on statement order.
189     */
190    private String choose(List<Occurrence> occurrences) {
191        Occurrence first = occurrences.get(0);
192        // The policy chooses AMONG spellings. When every occurrence is spelled the same there
193        // is nothing to choose, and the spelling is printed as written — so a table that is
194        // only ever referenced one way prints exactly as it always did, metadata or not.
195        boolean oneSpelling = true;
196        for (Occurrence o : occurrences) {
197            if (!o.rendered.equals(first.rendered)) {   // exact text, not an identifier compare
198                oneSpelling = false;
199                break;
200            }
201        }
202        if (oneSpelling || policy == DisplayNamePolicy.PREFER_FIRST_OCCURRENCE) {
203            return first.rendered;
204        }
205        Occurrence definition = null;
206        String catalog = null;
207        for (Occurrence o : occurrences) {
208            if (definition == null && o.definitionSite) definition = o;
209            if (catalog == null && o.catalogRendered != null) catalog = o.catalogRendered;
210        }
211        if (policy == DisplayNamePolicy.PREFER_METADATA) {
212            if (catalog != null) return catalog;
213            return definition != null ? definition.rendered : first.rendered;
214        }
215        // PREFER_DEFINITION_SITE
216        if (definition != null) return definition.rendered;
217        if (catalog != null) return catalog;
218        return first.rendered;
219    }
220}