001package gudusoft.gsqlparser.resolver2.format; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 005import gudusoft.gsqlparser.sqlenv.IdentifierService; 006 007import java.util.AbstractSet; 008import java.util.ArrayList; 009import java.util.Arrays; 010import java.util.Iterator; 011import java.util.LinkedHashMap; 012import java.util.List; 013import java.util.Map; 014import java.util.TreeMap; 015 016/** 017 * The lines of a {@code Fields:} style listing, de-duplicated so that two DIFFERENT objects 018 * are never folded into one line. 019 * 020 * <p>The listings folded rendered lines case-insensitively. For a case-sensitive name that 021 * deletes output: BigQuery {@code Sales.amount} and {@code SALES.amount} are columns of two 022 * different tables, yet only one line was printed and nothing said a column had gone.</p> 023 * 024 * <p>This set changes exactly one thing: lines that differ only by case AND whose stated 025 * identities differ are kept apart. Byte-identical lines still fold. Everything else folds as it always did — lines still compare case-insensitively, 026 * and a line added without an identity (a star entry, an orphan's {@code missed.} line, a 027 * constraint column) still folds with any line of the same text, because nothing proves it 028 * stands for a separate object. So the set can only ever print MORE lines than before, never 029 * fewer.</p> 030 */ 031public final class IdentityLineSet extends AbstractSet<String> { 032 033 private static final Object OPAQUE = new Object(); 034 035 /** Case-insensitive text -> (identity -> first spelling seen), in arrival order. */ 036 // non-identifier-compare: rendered listing lines, folded exactly as the listings always did 037 private final Map<String, Map<Object, String>> buckets = 038 new TreeMap<String, Map<Object, String>>(String.CASE_INSENSITIVE_ORDER); 039 040 /** Add a line with no stated identity; it folds with any line of the same text. */ 041 @Override 042 public boolean add(String line) { 043 return add(line, null); 044 } 045 046 /** 047 * Add a line for a stated object. 048 * 049 * @param identity what the line stands for, from {@link #fieldIdentity}; null when unknown 050 */ 051 public boolean add(String line, Object identity) { 052 if (line == null) return false; 053 Map<Object, String> bucket = buckets.get(line); 054 if (bucket == null) { 055 bucket = new LinkedHashMap<Object, String>(); 056 buckets.put(line, bucket); 057 } 058 Object key = identity != null ? identity : OPAQUE; 059 if (bucket.containsKey(key)) return false; 060 // Byte-identical text always folds, whatever the identities: two identical lines tell 061 // the reader nothing, and the same object can reach the listing from two places that 062 // state its identity differently. Only lines that DIFFER (by case) are worth keeping 063 // apart, and only when they stand for different objects. 064 if (bucket.containsValue(line)) return false; // exact text, not an identifier compare 065 if (key == OPAQUE && !bucket.isEmpty()) return false; // folds into a stated line 066 if (key != OPAQUE && bucket.size() == 1 && bucket.containsKey(OPAQUE)) { 067 // The unstated line seen earlier is now accounted for by this object. 068 String spelling = bucket.remove(OPAQUE); 069 bucket.put(key, spelling); 070 return false; 071 } 072 bucket.put(key, line); 073 return true; 074 } 075 076 @Override 077 public boolean contains(Object o) { 078 return o instanceof String && buckets.containsKey(o); 079 } 080 081 @Override 082 public Iterator<String> iterator() { 083 List<String> lines = new ArrayList<String>(); 084 for (Map<Object, String> bucket : buckets.values()) { 085 lines.addAll(bucket.values()); 086 } 087 return lines.iterator(); 088 } 089 090 @Override 091 public int size() { 092 int n = 0; 093 for (Map<Object, String> bucket : buckets.values()) n += bucket.size(); 094 return n; 095 } 096 097 @Override 098 public void clear() { 099 buckets.clear(); 100 } 101 102 /** 103 * Identity of a {@code table.column} line, from the SQL spellings and the vendor's rules. 104 * The two parts are keyed under their own object types because a vendor can treat them 105 * differently: BigQuery compares table names case-sensitively and column names not. 106 * 107 * @param tableLexical the table name as written in SQL (may be qualified); null or empty 108 * when the line's prefix is not a named table 109 * @param tableFallback text standing for the prefix when there is no table name 110 * @param columnLexical the column name as written in SQL 111 * @return an identity for {@link #add(String, Object)}, or null when none can be stated 112 */ 113 public static Object fieldIdentity(EDbVendor vendor, String tableLexical, String tableFallback, 114 String columnLexical) { 115 if (vendor == null || columnLexical == null || columnLexical.isEmpty()) return null; 116 try { 117 Object table = (tableLexical != null && !tableLexical.isEmpty()) 118 ? IdentifierService.qualifiedCanonKeyFromLexicalStatic(vendor, 119 ESQLDataObjectType.dotTable, tableLexical) 120 : Arrays.<Object>asList("prefix", tableFallback); 121 Object column = IdentifierService.qualifiedCanonKeyFromLexicalStatic(vendor, 122 ESQLDataObjectType.dotColumn, columnLexical); 123 return Arrays.asList(table, column); 124 } catch (RuntimeException e) { 125 // A spelling the name service rejects: state no identity, fold by text as before. 126 return null; 127 } 128 } 129}