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; 018 019import gudusoft.gsqlparser.EDbVendor; 020 021import java.util.*; 022 023/** 024 * Vendor-aware and type-aware name comparison utilities over the CANONICAL 025 * identifier-equality engine (unification plan P0d follow-up). 026 * 027 * <p>Equality delegates to {@link IdentifierService#areEqual} on THIS service's configured 028 * profile and the set/dedup utilities key by {@link CanonKey} from the same service, so every 029 * operation here observes exactly one relation. Built from a bare vendor that relation is the 030 * vendor default ({@code SQLUtil.sameName} / {@code TSQLEnv.compareIdentifier}); built from an 031 * analysis's {@link IdentifierService} it is that analysis's effective configuration. The previous 032 * implementation routed through the SqlNameMatcher framework configured from the legacy 033 * per-type case booleans, which collapsed quoted-case distinctions the canonical rules 034 * preserve (e.g. BigQuery tables, Couchbase).</p> 035 * 036 * @since 3.2.0.3 (Phase 1); canonical since P0d 037 */ 038public class NameService { 039 040 private final EDbVendor vendor; 041 private final IdentifierProfile profile; 042 private final IdentifierService service; 043 044 /** 045 * Creates a new NameService under the vendor's DEFAULT identifier configuration. 046 * 047 * @param vendor the database vendor 048 */ 049 public NameService(EDbVendor vendor) { 050 if (vendor == null) { 051 throw new IllegalArgumentException("vendor cannot be null"); 052 } 053 this.vendor = vendor; 054 this.profile = IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults()); 055 this.service = IdentifierService.defaultServiceFor(vendor); 056 } 057 058 /** 059 * A NameService over ONE analysis's identifier service, so the comparisons and sets built here 060 * observe that analysis's per-category configuration instead of the vendor default. 061 * 062 * <p>A factory rather than a constructor overload on purpose: {@code new NameService(null)} 063 * would otherwise become ambiguous against {@link #NameService(EDbVendor)} and break callers 064 * that already compile. 065 * 066 * @param service the frozen service of the current analysis 067 * @return a name service delegating to that service 068 */ 069 public static NameService forIdentifierService(IdentifierService service) { 070 return new NameService(service); 071 } 072 073 private NameService(IdentifierService service) { 074 if (service == null) { 075 throw new IllegalArgumentException("service cannot be null"); 076 } 077 this.service = service; 078 this.profile = service.getProfile(); 079 this.vendor = this.profile.getVendor(); 080 } 081 082 /** 083 * The identifier service every comparison here delegates to — the single engine, configured 084 * for this environment. 085 * 086 * @return the identifier service 087 */ 088 public IdentifierService getIdentifierService() { 089 return service; 090 } 091 092 /** 093 * The effective identifier configuration behind {@link #getIdentifierService()}. 094 * 095 * @return the identifier profile 096 */ 097 public IdentifierProfile getIdentifierProfile() { 098 return profile; 099 } 100 101 /** 102 * Returns the database vendor associated with this service. 103 * 104 * @return the database vendor 105 */ 106 public EDbVendor getVendor() { 107 return vendor; 108 } 109 110 /** 111 * Checks if two names are equal under the canonical identifier-equality relation 112 * ({@link IdentifierService#areEqual}) for this service’s configuration and object type. 113 * 114 * @param type the SQL object type (table, column, function, etc.) 115 * @param name1 the first name to compare 116 * @param name2 the second name to compare 117 * @return true if the names refer to the same object 118 */ 119 public boolean equals(ESQLDataObjectType type, String name1, String name2) { 120 return service.areEqual(name1, name2, type); 121 } 122 123 /** 124 * Finds the index of a name in a collection under canonical equality. 125 * 126 * <p>This is a case-aware version of {@code List.indexOf()}.</p> 127 * 128 * @param type the SQL object type 129 * @param names the collection of names to search 130 * @param target the name to find 131 * @return the index of the first matching name, or -1 if not found 132 */ 133 public int indexOf(ESQLDataObjectType type, Iterable<String> names, String target) { 134 if (names == null || target == null) { 135 return -1; 136 } 137 138 int index = 0; 139 for (String name : names) { 140 if (equals(type, name, target)) { 141 return index; 142 } 143 index++; 144 } 145 return -1; 146 } 147 148 /** 149 * Creates a distinct copy of a collection, removing canonical duplicates. 150 * 151 * <p>Preserves insertion order and the first spelling seen. For example, for a 152 * case-insensitive domain:</p> 153 * <pre> 154 * ["foo", "FOO", "bar", "Bar"] → ["foo", "bar"] 155 * </pre> 156 * 157 * @param type the SQL object type 158 * @param names the collection of names (may contain duplicates) 159 * @return a new list with duplicates removed, preserving order 160 */ 161 public List<String> distinctCopy(ESQLDataObjectType type, Iterable<String> names) { 162 if (names == null) { 163 return new ArrayList<>(); 164 } 165 166 Set<String> seen = createSet(type); 167 List<String> result = new ArrayList<>(); 168 169 for (String name : names) { 170 if (name != null && seen.add(name)) { 171 result.add(name); 172 } 173 } 174 175 return result; 176 } 177 178 /** 179 * Creates a Set whose membership follows canonical name equality: names are the 180 * same element iff their {@link CanonKey}s are equal. 181 * 182 * @param type the SQL object type 183 * @return a new Set with canonical name equality 184 */ 185 public Set<String> createSet(ESQLDataObjectType type) { 186 return new NameSet(type); 187 } 188 189 /** 190 * Whether the UNQUOTED comparison policy for this object type is case-sensitive. 191 * 192 * <p>Quoted operands may follow a different rule (quoted-case-preserving vendors); 193 * this describes only the unquoted policy from the vendor's identifier rules. A 194 * collation-based cell answers from its collation ({@code _CS_AI}, {@code _BIN2}: yes; 195 * {@code _CI_AS}: no) — see {@link IdentifierService#isCaseSensitive}.</p> 196 * 197 * @param type the object type 198 * @return true if unquoted names compare case-sensitively 199 */ 200 public boolean isCaseSensitive(ESQLDataObjectType type) { 201 return service.isCaseSensitive(type); 202 } 203 204 /** 205 * Internal Set keyed by {@link CanonKey}: membership mirrors canonical equality 206 * exactly, preserving insertion order and the first spelling of each name. 207 */ 208 private class NameSet extends AbstractSet<String> { 209 private final ESQLDataObjectType type; 210 private final Map<CanonKey, String> backingMap; // canonical key -> first spelling 211 212 NameSet(ESQLDataObjectType type) { 213 this.type = type; 214 this.backingMap = new LinkedHashMap<>(); 215 } 216 217 @Override 218 public boolean add(String name) { 219 if (name == null) { 220 throw new NullPointerException("NameSet does not permit null elements"); 221 } 222 return backingMap.putIfAbsent(service.canonKey(name, type), name) == null; 223 } 224 225 @Override 226 public boolean contains(Object o) { 227 if (!(o instanceof String)) { 228 return false; 229 } 230 return backingMap.containsKey(service.canonKey((String) o, type)); 231 } 232 233 @Override 234 public boolean remove(Object o) { 235 if (!(o instanceof String)) { 236 return false; 237 } 238 return backingMap.remove(service.canonKey((String) o, type)) != null; 239 } 240 241 @Override 242 public Iterator<String> iterator() { 243 return backingMap.values().iterator(); 244 } 245 246 @Override 247 public int size() { 248 return backingMap.size(); 249 } 250 251 @Override 252 public void clear() { 253 backingMap.clear(); 254 } 255 } 256}