001package gudusoft.gsqlparser.sqlenv; 002 003import gudusoft.gsqlparser.EDbVendor; 004 005import java.util.Arrays; 006import java.util.Objects; 007 008/** 009 * Canonical identity of a single SQL identifier segment under one vendor's identifier 010 * rules (unification plan stage P0c, docs/refactor/identifier_normalization_unification_plan.md). 011 * 012 * <p>Two keys compare equal iff they were produced in the same rule domain — vendor, 013 * object group, the whole effective policy (per-group AND per-category rules plus vendor 014 * flags, including the default collation), the effective rule cell of the category and 015 * collator identity — AND their canonical payloads match. The payload is 016 * the {@link IdentifierService#keyForMap} text for fold-based rules, or the 017 * {@link java.text.CollationKey} bytes (from the same collator the equality path uses) 018 * for collation-based rules. The comparison <em>rule</em> that selected the payload is 019 * deliberately NOT part of equality: under quoted-case-preserving vendors, unquoted 020 * {@code foo} (folded to {@code FOO}) and quoted {@code "FOO"} (preserved as {@code FOO}) 021 * must converge on the same key even though different rules produced them. 022 * 023 * <p><strong>In-process only.</strong> Collation key bytes are only stable within one 024 * JVM/collator configuration; never persist or exchange CanonKeys. Custom 025 * {@link CollatorProvider} subclasses are isolated by class name — two instances of the 026 * same class with different behavior cannot be distinguished. 027 */ 028public final class CanonKey { 029 030 private final EDbVendor vendor; 031 private final IdentifierProfile.ObjectGroup group; 032 private final IdentifierProfile policy; 033 private final IdentifierRules ruleDomain; 034 private final String collatorId; // null for fold-based keys 035 private final String canonText; // fold payload; diagnostic-only for collation keys 036 private final byte[] collationKey; // null for fold-based keys 037 038 CanonKey(EDbVendor vendor, IdentifierProfile.ObjectGroup group, IdentifierProfile policy, 039 IdentifierRules ruleDomain, String collatorId, String canonText, byte[] collationKey) { 040 this.vendor = vendor; 041 this.group = group; 042 this.policy = policy; 043 this.ruleDomain = ruleDomain; 044 this.collatorId = collatorId; 045 this.canonText = canonText; 046 this.collationKey = collationKey; 047 } 048 049 public EDbVendor getVendor() { 050 return vendor; 051 } 052 053 public IdentifierProfile.ObjectGroup getGroup() { 054 return group; 055 } 056 057 /** Canonical text payload. For collation-based keys this is diagnostic only. */ 058 public String getCanonText() { 059 return canonText; 060 } 061 062 public boolean isCollationBased() { 063 return collationKey != null; 064 } 065 066 /** 067 * Whether two canonical keys were produced under the same RULE POLICY — vendor and policy 068 * fingerprint (stage S0R). 069 * 070 * <p>This deliberately says nothing about the collator. Two components of ONE qualified identity 071 * legitimately use different object groups, and a profile may fold one group while collating 072 * another, so requiring the same mechanism split a valid catalog/schema/table/column identity 073 * apart at the column. Policy equality is the part that is always required and is transitive. 074 */ 075 public boolean sameRulePolicyAs(CanonKey other) { 076 // Full effective-strategy equality, not the fingerprint: the fingerprint is a 32-bit hash 077 // and two different per-category strategies can collide in it. It stays the fast path 078 // inside IdentifierProfile.equals. 079 return other != null && vendor == other.vendor 080 && policy.hasSameEffectivePolicyAs(other.policy); 081 } 082 083 /** 084 * The effective rule cell this key's payload was produced under — the key's 085 * <em>rule domain</em> within the profile ({@link IdentifierProfile#ruleDomainOf}). 086 * 087 * <p>Part of equality since per-category configuration exists: two categories of one 088 * {@link IdentifierProfile.ObjectGroup} may now compare differently, so the group alone no 089 * longer says which relation produced the payload. Under a profile that does not split a 090 * group, every category of that group shares this cell and the keys stay interchangeable 091 * exactly as before. 092 * 093 * @return the resolved rule cell; never null 094 */ 095 public IdentifierRules getRuleDomain() { 096 return ruleDomain; 097 } 098 099 /** 100 * The collation domain this key's payload belongs to, or null when the key FOLDS rather than 101 * collates. 102 * 103 * <p>A composite identity may mix fold-based and collation-based components, but every 104 * collation-based component must belong to the SAME collation domain: two different collators 105 * produce incomparable payloads. 106 */ 107 public String getCollatorDomainId() { 108 return collatorId; 109 } 110 111 /** 112 * Whether these two keys may sit in one composite identity: the same rule policy, and — when 113 * both collate — the same collator. 114 * 115 * <p><strong>This relation is NOT transitive.</strong> A fold-based key is compatible with a key 116 * collated by A and also with one collated by B, while A and B are not compatible with each 117 * other. So a composite identity must NOT be validated by comparing every component against the 118 * first one; that loop would accept {@code [fold, A, B]}. Accumulate the first non-null 119 * {@link #getCollatorDomainId()} across all components and compare every other against it. 120 */ 121 public boolean isRuleCompatibleWith(CanonKey other) { 122 if (!sameRulePolicyAs(other)) { 123 return false; 124 } 125 if (collatorId == null || other.collatorId == null) { 126 return true; 127 } 128 return collatorId.equals(other.collatorId); 129 } 130 131 /** A short description of this key's rule domain, for diagnostics only. */ 132 public String describeRuleDomain() { 133 return vendor + "/policy:" + policy.getFingerprint() + "/cell:" + ruleDomain 134 + (isCollationBased() ? "/collator:" + collatorId : "/fold"); 135 } 136 137 @Override 138 public boolean equals(Object o) { 139 if (this == o) return true; 140 if (o == null || getClass() != o.getClass()) return false; 141 CanonKey that = (CanonKey) o; 142 if (vendor != that.vendor || group != that.group 143 || !ruleDomain.equals(that.ruleDomain) 144 || !Objects.equals(collatorId, that.collatorId) 145 || !policy.hasSameEffectivePolicyAs(that.policy)) { 146 return false; 147 } 148 if (collationKey != null || that.collationKey != null) { 149 return Arrays.equals(collationKey, that.collationKey); 150 } 151 return Objects.equals(canonText, that.canonText); 152 } 153 154 @Override 155 public int hashCode() { 156 int result = Objects.hash(vendor, group, policy.getFingerprint(), ruleDomain, collatorId); 157 result = 31 * result + (collationKey != null ? Arrays.hashCode(collationKey) : Objects.hashCode(canonText)); 158 return result; 159 } 160 161 @Override 162 public String toString() { 163 return "CanonKey{" + vendor + "/" + group 164 + (collationKey != null ? "/collation[" + canonText + "]" : "/" + canonText) + "}"; 165 } 166}