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}