001package gudusoft.gsqlparser.dlineage.util;
002
003import gudusoft.gsqlparser.dlineage.dataflow.model.xml.table;
004import gudusoft.gsqlparser.sqlenv.QualifiedCanonKey;
005
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.LinkedHashSet;
010import java.util.List;
011import java.util.Map;
012import java.util.Set;
013
014/**
015 * Merge keys of the physical tables and views of one output merge.
016 *
017 * <p>Two tables are one table exactly when their identities under the analysis's identifier rules agree,
018 * whatever their legacy folded names. A table carries its identity when its producer minted one (a table
019 * analyzed from SQL, a catalog table read by {@code SQLEnvMetadataAnalyzer}), or vouches that its published
020 * name is a SQL spelling (the SQLDep, Grabit and sqlflow readers), in which case the name read under this
021 * merge's rules is its identity. A table from a serialized document carries none: its published name is
022 * then the only evidence, and a weak one, since a stored name is published bare and reads as another
023 * name.</p>
024 *
025 * <p>Decided after every table of the merge is known, and independently of the order they arrive in:</p>
026 * <ul>
027 * <li>a table carrying an identity is keyed by that identity;</li>
028 * <li>a table carrying none joins the one identity it can be shown to be: the identity of a table that
029 * published exactly the same name (the table reloaded, or its copy), else the identity its own published
030 * name reads as, if some table carries it; under a configured identifier profile its published name is its
031 * identity, as the configured-profile merge always read it;</li>
032 * <li>otherwise it keeps the legacy merge when no table of its legacy bucket has an identity, and stays
033 * apart when one does, rather than attaching to a guess.</li>
034 * </ul>
035 *
036 * <p>An identity is keyed by the first legacy bucket it was seen in: the bucket's first identity by the
037 * legacy key itself, as before identities existed, so a merge that groups tables as it always did also
038 * orders its output as it always did; another identity of the bucket by the legacy key and its position.</p>
039 */
040public final class PhysicalTableMergeKeys {
041
042    private final boolean configuredRules;
043    private final Set<QualifiedCanonKey> carried = new HashSet<QualifiedCanonKey>();
044    /** The carried identities each published name was seen with, across the merge. */
045    private final Map<String, Set<QualifiedCanonKey>> carriedByPublishedName =
046            new HashMap<String, Set<QualifiedCanonKey>>();
047    private final Map<QualifiedCanonKey, String> firstBucket = new HashMap<QualifiedCanonKey, String>();
048    private final Map<String, List<QualifiedCanonKey>> identitiesByBucket = new HashMap<String, List<QualifiedCanonKey>>();
049
050    /**
051     * @param configuredRules whether the analysis runs under identifier rules of its own, rather than its
052     *        vendor's defaults
053     */
054    public PhysicalTableMergeKeys(boolean configuredRules) {
055        this.configuredRules = configuredRules;
056    }
057
058    /**
059     * The identity a table carries: the one its producer minted, else its published name read under this
060     * analysis's rules when the producer vouches the name is a SQL spelling. Null for a table that carries
061     * none.
062     */
063    public static QualifiedCanonKey carriedIdentity(table table, String legacyKey, String qualifiedTableName) {
064        if (table.getMergeIdentity() != null) {
065            return table.getMergeIdentity();
066        }
067        return table.isPublishedNameSqlSpelling()
068                ? DlineageUtil.publishedTableIdentity(qualifiedTableName, legacyKey) : null;
069    }
070
071    /**
072     * First pass for one physical table or view: {@link #add} for the table under its own name and, for a merged
073     * table that carries an identity, under each exact name the carried tables merged into it published
074     * ({@code table.getCarriedPublishedNames()}), so a merge in stages matches a table that carries no identity
075     * on the evidence one merge of all the inputs has.
076     */
077    public void addTable(table table, String legacyKey, String qualifiedTableName) {
078        QualifiedCanonKey identity = carriedIdentity(table, legacyKey, qualifiedTableName);
079        add(legacyKey, identity, qualifiedTableName);
080        if (identity == null || table.getCarriedPublishedNames() == null) {
081            return;
082        }
083        for (Map.Entry<String, String> name : table.getCarriedPublishedNames().entrySet()) {
084            add(name.getValue(), identity, name.getKey());
085        }
086    }
087
088    /** First pass: one call per physical table or view of the merge, or per name {@link #addTable} registers. */
089    public void add(String legacyKey, QualifiedCanonKey carriedIdentity, String qualifiedTableName) {
090        if (legacyKey == null) {
091            return;
092        }
093        QualifiedCanonKey identity = carriedIdentity;
094        if (identity != null) {
095            carried.add(identity);
096            if (qualifiedTableName != null) {
097                Set<QualifiedCanonKey> identities = carriedByPublishedName.get(qualifiedTableName);
098                if (identities == null) {
099                    identities = new LinkedHashSet<QualifiedCanonKey>();
100                    carriedByPublishedName.put(qualifiedTableName, identities);
101                }
102                identities.add(identity);
103            }
104        } else if (configuredRules) {
105            identity = DlineageUtil.publishedTableIdentity(qualifiedTableName, legacyKey);
106        }
107        if (identity == null) {
108            return;
109        }
110        if (!firstBucket.containsKey(identity)) {
111            firstBucket.put(identity, legacyKey);
112        }
113        List<QualifiedCanonKey> identities = identitiesByBucket.get(legacyKey);
114        if (identities == null) {
115            identities = new ArrayList<QualifiedCanonKey>();
116            identitiesByBucket.put(legacyKey, identities);
117        }
118        if (!identities.contains(identity)) {
119            identities.add(identity);
120        }
121    }
122
123    /** Second pass: the merge key of a table given to {@link #add}. */
124    public String keyOf(String legacyKey, QualifiedCanonKey carriedIdentity, String qualifiedTableName) {
125        if (legacyKey == null) {
126            return null;
127        }
128        QualifiedCanonKey identity = carriedIdentity != null ? carriedIdentity
129                : uncarriedIdentity(legacyKey, qualifiedTableName);
130        String bucket = identity == null ? null : firstBucket.get(identity);
131        if (bucket != null) {
132            int index = identitiesByBucket.get(bucket).indexOf(identity);
133            return index == 0 ? bucket : bucket + "\u0001" + index;
134        }
135        return identitiesByBucket.containsKey(legacyKey) ? legacyKey + "\u0002" + qualifiedTableName : legacyKey;
136    }
137
138    private QualifiedCanonKey uncarriedIdentity(String legacyKey, String qualifiedTableName) {
139        Set<QualifiedCanonKey> byName = qualifiedTableName == null ? null : carriedByPublishedName.get(qualifiedTableName);
140        if (byName != null) {
141            return byName.size() == 1 ? byName.iterator().next() : null;
142        }
143        QualifiedCanonKey read = DlineageUtil.publishedTableIdentity(qualifiedTableName, legacyKey);
144        if (read == null) {
145            return null;
146        }
147        return carried.contains(read) || configuredRules ? read : null;
148    }
149}