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}