001package gudusoft.gsqlparser.resolver2.matcher;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
005import gudusoft.gsqlparser.sqlenv.IdentifierProfile;
006import gudusoft.gsqlparser.sqlenv.IdentifierService;
007import gudusoft.gsqlparser.sqlenv.QualifiedNameHierarchy;
008
009import java.util.List;
010
011/**
012 * Vendor-specific name matcher using IdentifierService.
013 *
014 * <p>This implementation provides database vendor-specific identifier normalization
015 * and comparison by delegating to {@link IdentifierService}.</p>
016 *
017 * <p>Key features:</p>
018 * <ul>
019 *   <li>Vendor-specific case folding (Oracle: UPPER, PostgreSQL: LOWER, etc.)</li>
020 *   <li>Proper handling of quoted vs unquoted identifiers</li>
021 *   <li>Support for different object types (tables, columns, schemas)</li>
022 *   <li>High-performance static caching via IdentifierService</li>
023 * </ul>
024 *
025 * <p>Usage example:</p>
026 * <pre>
027 * // Create matcher for Oracle
028 * VendorNameMatcher matcher = new VendorNameMatcher(EDbVendor.dbvoracle);
029 *
030 * // Normalize column name
031 * String normalized = matcher.normalize("\"Column1\"");  // Returns "Column1" (quoted preserved)
032 * String normalized2 = matcher.normalize("column1");     // Returns "COLUMN1" (unquoted folded)
033 *
034 * // Compare names
035 * boolean match = matcher.matches("COL1", "col1");  // Returns true (both normalize to COL1)
036 * </pre>
037 *
038 * @since 3.1.0.9
039 * @see IdentifierService
040 */
041public class VendorNameMatcher implements INameMatcher {
042
043    private final EDbVendor vendor;
044    private final ESQLDataObjectType defaultObjectType;
045    private final IdentifierService service;
046
047    /**
048     * Create a vendor name matcher with default object type (column) under the vendor's DEFAULT
049     * identifier configuration.
050     *
051     * @param vendor the database vendor
052     */
053    public VendorNameMatcher(EDbVendor vendor) {
054        this(vendor, ESQLDataObjectType.dotColumn);
055    }
056
057    /**
058     * Create a vendor name matcher with specified default object type.
059     *
060     * @param vendor the database vendor
061     * @param defaultObjectType the default object type for normalization/comparison
062     */
063    public VendorNameMatcher(EDbVendor vendor, ESQLDataObjectType defaultObjectType) {
064        this.vendor = vendor != null ? vendor : EDbVendor.dbvoracle;
065        this.defaultObjectType = defaultObjectType != null ? defaultObjectType : ESQLDataObjectType.dotColumn;
066        this.service = IdentifierService.defaultServiceFor(this.vendor);
067    }
068
069    /**
070     * Create a matcher over ONE analysis's identifier service, so resolution observes that
071     * analysis's per-category configuration rather than the vendor default.
072     *
073     * @param service the frozen identifier service of the current analysis
074     * @param defaultObjectType the default object type for normalization/comparison
075     */
076    public VendorNameMatcher(IdentifierService service, ESQLDataObjectType defaultObjectType) {
077        if (service == null) {
078            throw new IllegalArgumentException("VendorNameMatcher: service must not be null");
079        }
080        this.service = service;
081        this.vendor = service.getProfile().getVendor();
082        this.defaultObjectType = defaultObjectType != null ? defaultObjectType : ESQLDataObjectType.dotColumn;
083    }
084
085    /**
086     * The identifier service every comparison here delegates to.
087     *
088     * @return the identifier service
089     */
090    public IdentifierService getIdentifierService() {
091        return service;
092    }
093
094    @Override
095    public boolean matches(String name1, String name2) {
096        return matches(name1, name2, defaultObjectType);
097    }
098
099    /**
100     * Check if two names match according to vendor rules for a specific object type.
101     *
102     * @param name1 first name
103     * @param name2 second name
104     * @param objectType the object type (table, column, etc.)
105     * @return true if names match according to vendor rules
106     */
107    public boolean matches(String name1, String name2, ESQLDataObjectType objectType) {
108        if (name1 == null || name2 == null) {
109            return name1 == name2;
110        }
111        return service.areEqual(name1, name2, objectType);
112    }
113
114    @Override
115    public boolean matchesPattern(String name, String pattern) {
116        if (name == null || pattern == null) {
117            return false;
118        }
119        // Normalize both and do prefix matching
120        String normalizedName = normalize(name);
121        String normalizedPattern = normalize(pattern);
122        return normalizedName.startsWith(normalizedPattern);
123    }
124
125    @Override
126    public String normalize(String name) {
127        return normalize(name, defaultObjectType);
128    }
129
130    /**
131     * Normalize a name according to vendor rules for a specific object type.
132     *
133     * @param name the name to normalize
134     * @param objectType the object type (table, column, etc.)
135     * @return normalized name
136     */
137    public String normalize(String name, ESQLDataObjectType objectType) {
138        if (name == null || name.isEmpty()) {
139            return name;
140        }
141        return service.normalize(name, objectType);
142    }
143
144    /**
145     * Normalize a qualified name (e.g., schema.table.column).
146     *
147     * <p>This method properly normalizes each part of a multi-part identifier
148     * according to vendor rules.</p>
149     *
150     * @param qualifiedName the qualified name
151     * @param objectType the final object type
152     * @return normalized qualified name
153     */
154    public String normalizeQualifiedName(String qualifiedName, ESQLDataObjectType objectType) {
155        if (qualifiedName == null || qualifiedName.isEmpty()) {
156            return qualifiedName;
157        }
158
159        // Split and assign a role to each position with the ONE place that knows the vendor's
160        // hierarchy (QualifiedNameHierarchy), not by hand. The previous loop tracked quote
161        // characters itself and compared every intermediate segment as dotTable, so a catalog or
162        // schema segment was normalized by the table rule. That was invisible while a dialect gave
163        // all three the same rule; configured per category it silently renames a catalog.
164        List<String> segments = QualifiedNameHierarchy.split(vendor, qualifiedName);
165        if (segments == null || segments.isEmpty()) {
166            return qualifiedName;
167        }
168        List<QualifiedNameHierarchy.Segment> plan =
169                QualifiedNameHierarchy.plan(vendor, objectType, segments);
170
171        StringBuilder result = new StringBuilder(qualifiedName.length());
172        if (plan == null) {
173            // No hierarchy mapping for this vendor/type: normalize each segment as the whole
174            // name's own kind rather than inventing a role for it.
175            for (int i = 0; i < segments.size(); i++) {
176                if (i > 0) {
177                    result.append('.');
178                }
179                result.append(normalize(segments.get(i), objectType));
180            }
181            return result.toString();
182        }
183        for (int i = 0; i < plan.size(); i++) {
184            if (i > 0) {
185                result.append('.');
186            }
187            QualifiedNameHierarchy.Segment segment = plan.get(i);
188            result.append(normalize(segment.getText(), segment.getType()));
189        }
190        return result.toString();
191    }
192
193    /**
194     * Whether UNQUOTED names of this matcher's default object category compare case-sensitively
195     * under the configured profile.
196     *
197     * <p>Answered from the profile rather than hard-coded false: the old constant claimed every
198     * dialect was case-insensitive, which is wrong for BigQuery tables and Couchbase by default
199     * and wrong for anything a caller configures. Quoted operands may follow a different rule —
200     * {@link #matches} is the complete relation, this is a one-bit summary of the unquoted branch.
201     */
202    @Override
203    public boolean isCaseSensitive() {
204        return isCaseSensitive(defaultObjectType);
205    }
206
207    /**
208     * Whether UNQUOTED names of one object category compare case-sensitively under the configured
209     * profile.
210     *
211     * @param objectType the object category
212     * @return true when unquoted names of that category compare case-sensitively
213     */
214    public boolean isCaseSensitive(ESQLDataObjectType objectType) {
215        // A collation-based cell answers from its collation (_CS_AI and _BIN2 are case-sensitive
216        // though compared through a collator); the service holds that one rule.
217        return service.isCaseSensitive(objectType);
218    }
219
220    /**
221     * Get the database vendor.
222     *
223     * @return the vendor
224     */
225    public EDbVendor getVendor() {
226        return vendor;
227    }
228
229    /**
230     * Get the default object type.
231     *
232     * @return the default object type
233     */
234    public ESQLDataObjectType getDefaultObjectType() {
235        return defaultObjectType;
236    }
237
238    /**
239     * Create a new matcher with a different default object type.
240     *
241     * @param objectType the new default object type
242     * @return new VendorNameMatcher instance
243     */
244    public VendorNameMatcher withObjectType(ESQLDataObjectType objectType) {
245        return new VendorNameMatcher(service, objectType);
246    }
247
248    @Override
249    public String toString() {
250        return String.format("VendorNameMatcher{vendor=%s, defaultType=%s}", vendor, defaultObjectType);
251    }
252}