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}