001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to you under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package gudusoft.gsqlparser.sqlenv.calcite; 018 019import gudusoft.gsqlparser.EDbVendor; 020import gudusoft.gsqlparser.ext.calcite.sqlnamematcher.SqlNameMatcher; 021import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 022import gudusoft.gsqlparser.sqlenv.IdentifierService; 023import gudusoft.gsqlparser.sqlenv.TSQLEnv; 024 025import java.util.*; 026 027/** 028 * Centralized name matching service using Apache Calcite's SqlNameMatcher framework. 029 * 030 * <p>This service provides vendor-aware and type-aware name comparison utilities, 031 * replacing scattered string comparisons throughout GSP with a unified approach.</p> 032 * 033 * <p>Key responsibilities:</p> 034 * <ul> 035 * <li>Name equality checking (case-sensitive or insensitive per vendor/type)</li> 036 * <li>Case-aware collection operations (indexOf, distinct, Set creation)</li> 037 * <li>Bridge between GSP's normalization rules and Calcite's matching framework</li> 038 * </ul> 039 * 040 * <p>Design: This service delegates to {@link NamePolicyFactory} for matcher instances 041 * and preserves {@link TSQLEnv#normalizeIdentifier} as the authority for identifier 042 * quoting and default-case normalization.</p> 043 * 044 * @since 3.2.0.3 (Phase 1) 045 */ 046public class NameService { 047 048 private final EDbVendor vendor; 049 private final NamePolicyFactory factory; 050 051 /** 052 * Creates a new NameService for the specified database vendor. 053 * 054 * @param vendor the database vendor 055 */ 056 public NameService(EDbVendor vendor) { 057 if (vendor == null) { 058 throw new IllegalArgumentException("vendor cannot be null"); 059 } 060 this.vendor = vendor; 061 this.factory = new NamePolicyFactory(vendor); 062 } 063 064 /** 065 * Returns the database vendor associated with this service. 066 * 067 * @return the database vendor 068 */ 069 public EDbVendor getVendor() { 070 return vendor; 071 } 072 073 /** 074 * Checks if two names are equal according to vendor-specific and type-specific rules. 075 * 076 * <p>This method replaces {@code TSQLEnv.compareIdentifier()} with a more robust 077 * implementation backed by Calcite's SqlNameMatcher framework.</p> 078 * 079 * <p>Names are normalized using GSP's existing rules before matching.</p> 080 * 081 * @param type the SQL object type (table, column, function, etc.) 082 * @param name1 the first name to compare 083 * @param name2 the second name to compare 084 * @return true if the names match according to vendor/type rules 085 */ 086 public boolean equals(ESQLDataObjectType type, String name1, String name2) { 087 if (name1 == null || name2 == null) { 088 return name1 == name2; 089 } 090 091 // Normalize identifiers using GSP's existing quoting/case rules 092 String normalized1 = IdentifierService.normalizeStatic(vendor, type, name1); 093 String normalized2 = IdentifierService.normalizeStatic(vendor, type, name2); 094 095 // Use Calcite matcher for case-aware comparison 096 SqlNameMatcher matcher = factory.getMatcherForType(type); 097 return matcher.matches(normalized1, normalized2); 098 } 099 100 /** 101 * Finds the index of a name in a collection, using vendor/type-aware matching. 102 * 103 * <p>This is a case-aware version of {@code List.indexOf()}.</p> 104 * 105 * @param type the SQL object type 106 * @param names the collection of names to search 107 * @param target the name to find 108 * @return the index of the first matching name, or -1 if not found 109 */ 110 public int indexOf(ESQLDataObjectType type, Iterable<String> names, String target) { 111 if (names == null || target == null) { 112 return -1; 113 } 114 115 int index = 0; 116 for (String name : names) { 117 if (equals(type, name, target)) { 118 return index; 119 } 120 index++; 121 } 122 return -1; 123 } 124 125 /** 126 * Creates a distinct copy of a collection, removing duplicates using vendor/type-aware matching. 127 * 128 * <p>Preserves insertion order. For example, with case-insensitive matching:</p> 129 * <pre> 130 * ["foo", "FOO", "bar", "Bar"] → ["foo", "bar"] 131 * </pre> 132 * 133 * @param type the SQL object type 134 * @param names the collection of names (may contain duplicates) 135 * @return a new list with duplicates removed, preserving order 136 */ 137 public List<String> distinctCopy(ESQLDataObjectType type, Iterable<String> names) { 138 if (names == null) { 139 return new ArrayList<>(); 140 } 141 142 Set<String> seen = createSet(type); 143 List<String> result = new ArrayList<>(); 144 145 for (String name : names) { 146 if (name != null) { 147 String normalized = IdentifierService.normalizeStatic(vendor, type, name); 148 if (seen.add(normalized)) { 149 result.add(name); // Keep original name, not normalized 150 } 151 } 152 } 153 154 return result; 155 } 156 157 /** 158 * Creates a Set that uses vendor/type-aware name matching for equality. 159 * 160 * <p>The returned Set will treat names as equal if they match according to 161 * the vendor's case sensitivity rules for the specified object type.</p> 162 * 163 * <p>Implementation note: Uses a wrapper around LinkedHashSet that normalizes 164 * keys before insertion/lookup.</p> 165 * 166 * @param type the SQL object type 167 * @return a new Set with case-aware equality 168 */ 169 public Set<String> createSet(ESQLDataObjectType type) { 170 return new NameSet(type); 171 } 172 173 /** 174 * Checks if a specific object type is case-sensitive for this vendor. 175 * 176 * <p>Convenience method that delegates to {@link NamePolicyFactory}.</p> 177 * 178 * @param type the object type 179 * @return true if case-sensitive, false otherwise 180 */ 181 public boolean isCaseSensitive(ESQLDataObjectType type) { 182 return factory.isCaseSensitive(type); 183 } 184 185 /** 186 * Internal Set implementation that uses vendor/type-aware name matching. 187 * 188 * <p>This wrapper normalizes all keys before delegating to a standard LinkedHashSet, 189 * ensuring that name matching follows vendor-specific case sensitivity rules.</p> 190 */ 191 private class NameSet extends AbstractSet<String> { 192 private final ESQLDataObjectType type; 193 private final Map<String, String> backingMap; // normalized -> original 194 195 NameSet(ESQLDataObjectType type) { 196 this.type = type; 197 this.backingMap = new LinkedHashMap<>(); 198 } 199 200 @Override 201 public boolean add(String name) { 202 if (name == null) { 203 throw new NullPointerException("NameSet does not permit null elements"); 204 } 205 String normalized = IdentifierService.normalizeStatic(vendor, type, name); 206 return backingMap.putIfAbsent(normalized, name) == null; 207 } 208 209 @Override 210 public boolean contains(Object o) { 211 if (!(o instanceof String)) { 212 return false; 213 } 214 String normalized = IdentifierService.normalizeStatic(vendor, type, (String) o); 215 return backingMap.containsKey(normalized); 216 } 217 218 @Override 219 public boolean remove(Object o) { 220 if (!(o instanceof String)) { 221 return false; 222 } 223 String normalized = IdentifierService.normalizeStatic(vendor, type, (String) o); 224 return backingMap.remove(normalized) != null; 225 } 226 227 @Override 228 public Iterator<String> iterator() { 229 return backingMap.values().iterator(); 230 } 231 232 @Override 233 public int size() { 234 return backingMap.size(); 235 } 236 237 @Override 238 public void clear() { 239 backingMap.clear(); 240 } 241 } 242}