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}