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}