001package gudusoft.gsqlparser.sqlenv; 002 003import gudusoft.gsqlparser.EDbVendor; 004 005import java.util.ArrayList; 006import java.util.Collections; 007import java.util.List; 008 009/** 010 * Canonical identity of a possibly-qualified SQL name — {@code db.schema.table}, 011 * {@code schema.table.column}, {@code t.c} — for use as a map or set key 012 * (stage S0 of docs/refactor/identifier_identity_display_separation_plan.md). 013 * 014 * <p>It is the qualified counterpart of {@link CanonKey}: one {@code CanonKey} per 015 * hierarchy level, each computed under the rules of <em>that level's</em> object type, plus 016 * the terminal object type itself so that a table named {@code x} and a schema named 017 * {@code x} cannot collide in one index. {@link CanonKey} carries an 018 * {@link IdentifierProfile.ObjectGroup}, which is the right granularity for comparing 019 * identifiers but deliberately coarser than the entity kind — hence the explicit terminal 020 * discriminator here. 021 * 022 * <p><strong>There is no flattened identity string.</strong> The previous approach, 023 * {@link IdentifierService#buildCompositeKey(String, ESQLDataObjectType)}, joins 024 * already-normalized segment text; it cannot represent quote state, and its guard 025 * {@link IdentifierService#canUseCompositeKey(String)} refuses every name containing a 026 * quote character — which is exactly the case that loses lineage (MantisBT 4728). Joining 027 * canonical segments back into a {@code String} would reintroduce both problems, so this 028 * type keeps the segment keys as a list and nothing renders them. 029 * 030 * <h3>Equality</h3> 031 * Two keys are equal iff they have the same terminal object type, the same number of levels, and 032 * level-wise equal {@link CanonKey}s. Since {@code CanonKey} equality IS the canonical 033 * single-segment equality relation (unification-plan stage P0d.1), each LEVEL compares correctly. 034 * 035 * <p><strong>That does not make the whole key agree with 036 * {@link gudusoft.gsqlparser.util.SQLUtil#compareIdentifier}, and it does not make it correct.</strong> 037 * Two things are still decided outside the per-level comparison: how the name is split into 038 * segments, and which object type each position is compared as. The S0 proof test measures the 039 * agreement over every vendor, every mapped object type and every ordered pair of shapes, and 040 * pins each divergence with its cause — but agreement with {@code compareIdentifier} is not 041 * proof of correctness, because both can be wrong together. 042 * 043 * <p>They ARE wrong together in one known case, which this type cannot currently fix: the role of 044 * each position is inferred from the vendor, the terminal type and the arity alone. For a 045 * two-segment column name that guesses {@code table.column}, but BigQuery 046 * {@code customer.name} is a STRUCT field access — base column {@code customer} plus field 047 * {@code name} (see {@code FieldPath} and {@code TestBigQueryNestedColumns}) — and the two read 048 * the first segment under different case rules. Fixing it needs an explicit semantic role per 049 * component, taken from resolution rather than from syntax; that is scheduled before S2 and is 050 * why {@code compareIdentifier} must NOT be made to delegate to the planner first. 051 * 052 * <h3>Provenance</h3> 053 * Build one through the explicitly named factories 054 * {@link IdentifierService#qualifiedCanonKeyFromLexical(String, ESQLDataObjectType)} — 055 * for a name as written in SQL — or 056 * {@link IdentifierService#qualifiedCanonKeyFromStored(java.util.List, ESQLDataObjectType)} — for 057 * a name already in the form a catalog stores, passed as one entry per level. The two are not interchangeable: keying a 058 * Snowflake catalog object from an already-decoded {@code foo} is not the same as keying 059 * the SQL lexical {@code "foo"}. There is deliberately no factory that guesses. 060 * 061 * <p><strong>In-process only</strong>, for the same reason as {@link CanonKey}: collation 062 * payloads are stable only within one JVM and collator configuration. Use 063 * {@link PersistentIdentifierKey} for anything persisted or exchanged. 064 */ 065public final class QualifiedCanonKey { 066 067 private final List<CanonKey> segmentKeys; 068 private final ESQLDataObjectType terminalType; 069 private final int hash; 070 071 QualifiedCanonKey(List<CanonKey> segmentKeys, ESQLDataObjectType terminalType) { 072 this.segmentKeys = Collections.unmodifiableList(new ArrayList<CanonKey>(segmentKeys)); 073 this.terminalType = terminalType; 074 this.hash = 31 * this.segmentKeys.hashCode() + terminalType.hashCode(); 075 } 076 077 /** The per-level canonical keys, outermost container first. Never empty. */ 078 public List<CanonKey> getSegmentKeys() { 079 return segmentKeys; 080 } 081 082 /** The kind of object the whole name denotes — the entity-kind discriminator. */ 083 public ESQLDataObjectType getTerminalType() { 084 return terminalType; 085 } 086 087 /** Number of hierarchy levels this name resolved to. */ 088 public int size() { 089 return segmentKeys.size(); 090 } 091 092 /** The vendor rule domain; all levels share it. */ 093 public EDbVendor getVendor() { 094 return segmentKeys.get(0).getVendor(); 095 } 096 097 /** 098 * The key of the innermost level — the column of {@code t.c}, the table of 099 * {@code s.t}. Useful for indexes that bucket by container and then by leaf name. 100 */ 101 public CanonKey getLeafKey() { 102 return segmentKeys.get(segmentKeys.size() - 1); 103 } 104 105 @Override 106 public boolean equals(Object o) { 107 if (this == o) { 108 return true; 109 } 110 if (o == null || getClass() != o.getClass()) { 111 return false; 112 } 113 QualifiedCanonKey that = (QualifiedCanonKey) o; 114 return terminalType == that.terminalType && segmentKeys.equals(that.segmentKeys); 115 } 116 117 @Override 118 public int hashCode() { 119 return hash; 120 } 121 122 @Override 123 public String toString() { 124 StringBuilder sb = new StringBuilder("QualifiedCanonKey{").append(terminalType).append(' '); 125 for (int i = 0; i < segmentKeys.size(); i++) { 126 if (i > 0) { 127 sb.append(" / "); 128 } 129 sb.append(segmentKeys.get(i)); 130 } 131 return sb.append('}').toString(); 132 } 133}