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; 020import gudusoft.gsqlparser.sqlenv.IdentifierRules.CaseCompare; 021 022import java.util.*; 023 024/** 025 * Vendor-aware and type-aware name comparison utilities over the CANONICAL 026 * identifier-equality engine (unification plan P0d follow-up). 027 * 028 * <p>Equality delegates to {@link IdentifierService#areEqualStatic} and the set/dedup 029 * utilities key by {@link CanonKey}, so every operation here observes exactly the same 030 * relation as {@code SQLUtil.sameName} / {@code TSQLEnv.compareIdentifier}. The previous 031 * implementation routed through the SqlNameMatcher framework configured from the legacy 032 * per-type case booleans, which collapsed quoted-case distinctions the canonical rules 033 * preserve (e.g. BigQuery tables, Couchbase).</p> 034 * 035 * @since 3.2.0.3 (Phase 1); canonical since P0d 036 */ 037public class NameService { 038 039 private final EDbVendor vendor; 040 private final IdentifierProfile profile; 041 042 /** 043 * Creates a new NameService for the specified database vendor. 044 * 045 * @param vendor the database vendor 046 */ 047 public NameService(EDbVendor vendor) { 048 if (vendor == null) { 049 throw new IllegalArgumentException("vendor cannot be null"); 050 } 051 this.vendor = vendor; 052 this.profile = IdentifierProfile.forVendor(vendor, IdentifierProfile.VendorFlags.defaults()); 053 } 054 055 /** 056 * Returns the database vendor associated with this service. 057 * 058 * @return the database vendor 059 */ 060 public EDbVendor getVendor() { 061 return vendor; 062 } 063 064 /** 065 * Checks if two names are equal under the canonical identifier-equality relation 066 * ({@link IdentifierService#areEqualStatic}) for this vendor and object type. 067 * 068 * @param type the SQL object type (table, column, function, etc.) 069 * @param name1 the first name to compare 070 * @param name2 the second name to compare 071 * @return true if the names refer to the same object 072 */ 073 public boolean equals(ESQLDataObjectType type, String name1, String name2) { 074 return IdentifierService.areEqualStatic(vendor, type, name1, name2); 075 } 076 077 /** 078 * Finds the index of a name in a collection under canonical equality. 079 * 080 * <p>This is a case-aware version of {@code List.indexOf()}.</p> 081 * 082 * @param type the SQL object type 083 * @param names the collection of names to search 084 * @param target the name to find 085 * @return the index of the first matching name, or -1 if not found 086 */ 087 public int indexOf(ESQLDataObjectType type, Iterable<String> names, String target) { 088 if (names == null || target == null) { 089 return -1; 090 } 091 092 int index = 0; 093 for (String name : names) { 094 if (equals(type, name, target)) { 095 return index; 096 } 097 index++; 098 } 099 return -1; 100 } 101 102 /** 103 * Creates a distinct copy of a collection, removing canonical duplicates. 104 * 105 * <p>Preserves insertion order and the first spelling seen. For example, for a 106 * case-insensitive domain:</p> 107 * <pre> 108 * ["foo", "FOO", "bar", "Bar"] → ["foo", "bar"] 109 * </pre> 110 * 111 * @param type the SQL object type 112 * @param names the collection of names (may contain duplicates) 113 * @return a new list with duplicates removed, preserving order 114 */ 115 public List<String> distinctCopy(ESQLDataObjectType type, Iterable<String> names) { 116 if (names == null) { 117 return new ArrayList<>(); 118 } 119 120 Set<String> seen = createSet(type); 121 List<String> result = new ArrayList<>(); 122 123 for (String name : names) { 124 if (name != null && seen.add(name)) { 125 result.add(name); 126 } 127 } 128 129 return result; 130 } 131 132 /** 133 * Creates a Set whose membership follows canonical name equality: names are the 134 * same element iff their {@link CanonKey}s are equal. 135 * 136 * @param type the SQL object type 137 * @return a new Set with canonical name equality 138 */ 139 public Set<String> createSet(ESQLDataObjectType type) { 140 return new NameSet(type); 141 } 142 143 /** 144 * Whether the UNQUOTED comparison policy for this object type is case-sensitive. 145 * 146 * <p>Quoted operands may follow a different rule (quoted-case-preserving vendors); 147 * this describes only the unquoted policy from the vendor's identifier rules.</p> 148 * 149 * @param type the object type 150 * @return true if unquoted names compare case-sensitively 151 */ 152 public boolean isCaseSensitive(ESQLDataObjectType type) { 153 return profile.getRules(type).unquotedCompare == CaseCompare.SENSITIVE; 154 } 155 156 /** 157 * Internal Set keyed by {@link CanonKey}: membership mirrors canonical equality 158 * exactly, preserving insertion order and the first spelling of each name. 159 */ 160 private class NameSet extends AbstractSet<String> { 161 private final ESQLDataObjectType type; 162 private final Map<CanonKey, String> backingMap; // canonical key -> first spelling 163 164 NameSet(ESQLDataObjectType type) { 165 this.type = type; 166 this.backingMap = new LinkedHashMap<>(); 167 } 168 169 @Override 170 public boolean add(String name) { 171 if (name == null) { 172 throw new NullPointerException("NameSet does not permit null elements"); 173 } 174 return backingMap.putIfAbsent(IdentifierService.canonKeyStatic(vendor, type, name), name) == null; 175 } 176 177 @Override 178 public boolean contains(Object o) { 179 if (!(o instanceof String)) { 180 return false; 181 } 182 return backingMap.containsKey(IdentifierService.canonKeyStatic(vendor, type, (String) o)); 183 } 184 185 @Override 186 public boolean remove(Object o) { 187 if (!(o instanceof String)) { 188 return false; 189 } 190 return backingMap.remove(IdentifierService.canonKeyStatic(vendor, type, (String) o)) != null; 191 } 192 193 @Override 194 public Iterator<String> iterator() { 195 return backingMap.values().iterator(); 196 } 197 198 @Override 199 public int size() { 200 return backingMap.size(); 201 } 202 203 @Override 204 public void clear() { 205 backingMap.clear(); 206 } 207 } 208}