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}