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}