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.sqlenv.ESQLDataObjectType;
021import gudusoft.gsqlparser.sqlenv.IdentifierService;
022import gudusoft.gsqlparser.sqlenv.TSQLEnv;
023import gudusoft.gsqlparser.sqlenv.TSQLSchemaObject;
024
025import java.util.*;
026
027/**
028 * Unified storage abstraction for catalog objects using canonical keys.
029 *
030 * <p>This class replaces GSP's legacy triple-index architecture
031 * (schemaObjectList + objectIndex + tablesByName) with a clean, type-aware
032 * storage system backed by LinkedHashMaps for O(1) lookups.</p>
033 *
034 * <p>Key improvements over legacy storage:</p>
035 * <ul>
036 *   <li>Canonical keys eliminate string suffix hacks ($function, $procedure, etc.)</li>
037 *   <li>Per-type indexes for efficient type-specific lookups</li>
038 *   <li>Vendor-aware case sensitivity via {@link NameService}</li>
039 *   <li>Insertion order preservation for compatibility</li>
040 *   <li>Full and partial name resolution support</li>
041 * </ul>
042 *
043 * <p>Thread safety: This class is NOT thread-safe. External synchronization
044 * is required if accessed from multiple threads.</p>
045 *
046 * @since 3.2.0.3 (Phase 1)
047 */
048public class CatalogStore {
049
050    private final EDbVendor vendor;
051    private final NameService nameService;
052    private final IdentifierService identifierService;
053
054    // Primary storage: canonical key -> object
055    private final Map<QualifiedName, TSQLSchemaObject> objectIndex = new LinkedHashMap<>();
056
057    // Per-type indexes for efficient type-specific lookups
058    private final Map<QualifiedName, TSQLSchemaObject> tableIndex = new LinkedHashMap<>();
059    private final Map<QualifiedName, TSQLSchemaObject> functionIndex = new LinkedHashMap<>();
060    private final Map<QualifiedName, TSQLSchemaObject> procedureIndex = new LinkedHashMap<>();
061    private final Map<QualifiedName, TSQLSchemaObject> packageIndex = new LinkedHashMap<>();
062    private final Map<QualifiedName, TSQLSchemaObject> triggerIndex = new LinkedHashMap<>();
063    private final Map<QualifiedName, TSQLSchemaObject> columnIndex = new LinkedHashMap<>();
064
065    /**
066     * 名称级(不含 catalog/schema)二级索引,用于“未给出完整路径”的部分查找。
067     *
068     * <p><b>为什么 value 是 {@code List<TSQLSchemaObject>}:</b></p>
069     * <ul>
070     *   <li>同名对象可存在于不同 catalog/schema 下:如 {@code db1.dbo.orders}
071     *       与 {@code db1.sales.orders}。</li>
072     *   <li>不同类型也可能同名(表/视图/函数等);可在
073     *       {@link #getByName(String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)}
074     *       中按类型再筛。</li>
075     *   <li>需要先收集全部候选,再依据默认 schema/search_path/上下文判定或报歧义。</li>
076     * </ul>
077     *
078     * <p><b>典型场景:</b></p>
079     * <pre>
080     * -- 1) MSSQL:默认 schema 解析
081     * USE db1;
082     * SELECT * FROM orders;
083     * -- 候选:db1.dbo.orders、db1.sales.orders
084     * -- 解析器按默认 schema(如 dbo)或搜索路径挑选命中
085     *
086     * -- 2) BigQuery:默认 dataset
087     * -- 当前上下文: project.dataset = p.ds
088     * SELECT * FROM `users`;
089     * -- 候选:p.ds.users、p.other_ds.users(若可见)
090     *
091     * -- 3) 歧义诊断
092     * SELECT * FROM orders;
093     * -- 多个候选且规则不能唯一确定时,抛出歧义错误或给出建议
094     * </pre>
095     *
096     * <p><b>备注:</b></p>
097     * <ul>
098     *   <li>键为经 {@link IdentifierService#normalize(String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)}
099     *       规范化后的简单名。</li>
100     *   <li>不用于“按 catalog/schema 分层列举”,此类需求请使用类型索引(如 {@code tableIndex})并基于
101     *       {@link QualifiedName#getCatalog()} / {@link QualifiedName#getSchema()} 过滤。</li>
102     * </ul>
103     */
104    private final Map<String, List<TSQLSchemaObject>> objectsByName = new LinkedHashMap<>();
105
106    /**
107     * Creates a new CatalogStore for the specified database vendor.
108     *
109     * @param vendor the database vendor
110     * @deprecated Use {@link #CatalogStore(EDbVendor, IdentifierService)} instead
111     */
112    @Deprecated
113    public CatalogStore(EDbVendor vendor) {
114        if (vendor == null) {
115            throw new IllegalArgumentException("vendor cannot be null");
116        }
117        this.vendor = vendor;
118        this.nameService = new NameService(vendor);
119        this.identifierService = null;  // Will fall back to TSQLEnv.normalizeIdentifier
120    }
121
122    /**
123     * Creates a new CatalogStore for the specified database vendor with IdentifierService.
124     *
125     * <p>This is the preferred constructor that uses IdentifierService for proper
126     * per-object-type identifier normalization.</p>
127     *
128     * @param vendor the database vendor
129     * @param identifierService the identifier service for normalization
130     * @since 3.2.0.3 (Phase 3.5)
131     */
132    public CatalogStore(EDbVendor vendor, IdentifierService identifierService) {
133        if (vendor == null) {
134            throw new IllegalArgumentException("vendor cannot be null");
135        }
136        if (identifierService == null) {
137            throw new IllegalArgumentException("identifierService cannot be null");
138        }
139        this.vendor = vendor;
140        this.nameService = new NameService(vendor);
141        this.identifierService = identifierService;
142    }
143
144    /**
145     * Returns the database vendor associated with this store.
146     *
147     * @return the database vendor
148     */
149    public EDbVendor getVendor() {
150        return vendor;
151    }
152
153    /**
154     * Returns the name service used by this store.
155     *
156     * @return the name service
157     */
158    public NameService getNameService() {
159        return nameService;
160    }
161
162    /**
163     * Adds or updates an object in the store.
164     *
165     * <p>The object is indexed by its fully-qualified canonical name and also
166     * added to type-specific and name-only indexes for efficient lookups.</p>
167     *
168     * @param object the schema object to add
169     * @return the previous object with the same key, or null if none existed
170     */
171    public TSQLSchemaObject put(TSQLSchemaObject object) {
172        if (object == null) {
173            throw new IllegalArgumentException("object cannot be null");
174        }
175
176        // Create canonical key
177        QualifiedName key = createKey(object);
178
179        // Add to primary index
180        TSQLSchemaObject previous = objectIndex.put(key, object);
181
182        // Add to type-specific index
183        Map<QualifiedName, TSQLSchemaObject> typeIndex = getTypeIndex(object.getDataObjectType());
184        if (typeIndex != null) {
185            typeIndex.put(key, object);
186        }
187
188        // Add to name-only index
189        String normalizedName = normalizeObjectName(object);
190        objectsByName.computeIfAbsent(normalizedName, k -> new ArrayList<>()).add(object);
191
192        return previous;
193    }
194
195    /**
196     * Finds an object by its fully-qualified name and type.
197     *
198     * <p>This is an O(1) lookup using the canonical key index.</p>
199     *
200     * @param catalog the catalog name (may be null)
201     * @param schema the schema name (may be null)
202     * @param objectName the object name (required)
203     * @param type the object type
204     * @return the matching object, or null if not found
205     */
206    public TSQLSchemaObject get(String catalog, String schema, String objectName, ESQLDataObjectType type) {
207        if (objectName == null || type == null) {
208            return null;
209        }
210
211        // Normalize components to match stored keys
212        String normalizedCatalog = normalizeComponent(catalog, type);
213        String normalizedSchema = normalizeComponent(schema, type);
214        String normalizedObject = normalizeComponent(objectName, type);
215
216        QualifiedName key = new QualifiedName(normalizedCatalog, normalizedSchema, normalizedObject, type);
217        return objectIndex.get(key);
218    }
219
220    /**
221     * Finds objects by name only, regardless of catalog/schema.
222     *
223     * <p>This is used for partial name resolution when the full path is not specified.</p>
224     *
225     * @param objectName the object name
226     * @param type the object type (optional - null returns all types)
227     * @return list of matching objects (may be empty, never null)
228     */
229    public List<TSQLSchemaObject> getByName(String objectName, ESQLDataObjectType type) {
230        if (objectName == null) {
231            return Collections.emptyList();
232        }
233
234//        if (type == null){
235//          type = ESQLDataObjectType.dotTable; // Default to table type if none specified
236//        }
237
238        // Use IdentifierService if available, otherwise fall back to legacy method
239        String normalizedName;
240        if (identifierService != null) {
241            normalizedName = identifierService.normalize(objectName, type);
242        } else {
243            normalizedName =  IdentifierService.normalizeStatic(vendor, type, objectName); // TSQLEnv.normalizeIdentifier(vendor, type, objectName);  //
244        }
245        List<TSQLSchemaObject> candidates = objectsByName.get(normalizedName);
246
247        if (candidates == null || candidates.isEmpty()) {
248            return Collections.emptyList();
249        }
250
251        // Filter by type if specified
252        if (type != null) {
253            List<TSQLSchemaObject> filtered = new ArrayList<>();
254            for (TSQLSchemaObject obj : candidates) {
255                if (obj.getDataObjectType() == type) {
256                    filtered.add(obj);
257                }
258            }
259            return filtered;
260        }
261
262        return new ArrayList<>(candidates);
263    }
264
265    /**
266     * Finds all objects of a specific type.
267     *
268     * @param type the object type
269     * @return collection of all objects of that type (may be empty, never null)
270     */
271    public Collection<TSQLSchemaObject> getByType(ESQLDataObjectType type) {
272        if (type == null) {
273            return Collections.emptyList();
274        }
275
276        Map<QualifiedName, TSQLSchemaObject> typeIndex = getTypeIndex(type);
277        if (typeIndex == null) {
278            return Collections.emptyList();
279        }
280
281        return new ArrayList<>(typeIndex.values());
282    }
283
284    /**
285     * Removes an object from the store.
286     *
287     * @param object the object to remove
288     * @return true if the object was removed, false if it was not present
289     */
290    public boolean remove(TSQLSchemaObject object) {
291        if (object == null) {
292            return false;
293        }
294
295        QualifiedName key = createKey(object);
296
297        // Remove from primary index
298        TSQLSchemaObject removed = objectIndex.remove(key);
299
300        if (removed != null) {
301            // Remove from type-specific index
302            Map<QualifiedName, TSQLSchemaObject> typeIndex = getTypeIndex(object.getDataObjectType());
303            if (typeIndex != null) {
304                typeIndex.remove(key);
305            }
306
307            // Remove from name-only index
308            String normalizedName = normalizeObjectName(object);
309            List<TSQLSchemaObject> list = objectsByName.get(normalizedName);
310            if (list != null) {
311                list.remove(object);
312                if (list.isEmpty()) {
313                    objectsByName.remove(normalizedName);
314                }
315            }
316
317            return true;
318        }
319
320        return false;
321    }
322
323    /**
324     * Returns the total number of objects in the store.
325     *
326     * @return the total count
327     */
328    public int size() {
329        return objectIndex.size();
330    }
331
332    /**
333     * Removes all objects from the store.
334     */
335    public void clear() {
336        objectIndex.clear();
337        tableIndex.clear();
338        functionIndex.clear();
339        procedureIndex.clear();
340        packageIndex.clear();
341        triggerIndex.clear();
342        columnIndex.clear();
343        objectsByName.clear();
344    }
345
346    /**
347     * Returns all objects in insertion order.
348     *
349     * @return collection of all objects (may be empty, never null)
350     */
351    public Collection<TSQLSchemaObject> getAll() {
352        return new ArrayList<>(objectIndex.values());
353    }
354
355    /**
356     * Creates a canonical key for an object.
357     *
358     * <p>The key is normalized according to vendor-specific rules.</p>
359     */
360    private QualifiedName createKey(TSQLSchemaObject object) {
361        String catalog = object.getSchema() != null && object.getSchema().getCatalog() != null
362            ? normalizeComponent(object.getSchema().getCatalog().getName(), object.getDataObjectType())
363            : null;
364        String schema = object.getSchema() != null
365            ? normalizeComponent(object.getSchema().getName(), object.getDataObjectType())
366            : null;
367        String name = normalizeComponent(object.getName(), object.getDataObjectType());
368
369        return new QualifiedName(catalog, schema, name, object.getDataObjectType());
370    }
371
372    /**
373     * Normalizes a name component (catalog/schema/object).
374     */
375    private String normalizeComponent(String component, ESQLDataObjectType type) {
376        if (component == null || component.isEmpty()) {
377            return null;
378        }
379        // Use IdentifierService if available, otherwise fall back to legacy method
380        if (identifierService != null) {
381            return identifierService.normalize(component, type);
382        } else {
383            return IdentifierService.normalizeStatic(vendor, type, component);
384        }
385    }
386
387    /**
388     * Normalizes an object's name for the name-only index.
389     */
390    private String normalizeObjectName(TSQLSchemaObject object) {
391        String name = object.getName();
392        if (name == null) {
393            name = "";
394        }
395        // Use IdentifierService if available, otherwise fall back to legacy method
396        if (identifierService != null) {
397            return identifierService.normalize(name, object.getDataObjectType());
398        } else {
399            return IdentifierService.normalizeStatic(vendor, object.getDataObjectType(), name);
400        }
401    }
402
403    /**
404     * Returns the type-specific index for the given object type.
405     */
406    private Map<QualifiedName, TSQLSchemaObject> getTypeIndex(ESQLDataObjectType type) {
407        switch (type) {
408            case dotTable:
409                return tableIndex;
410            case dotFunction:
411                return functionIndex;
412            case dotProcedure:
413                return procedureIndex;
414            case dotOraclePackage:
415                return packageIndex;
416            case dotTrigger:
417                return triggerIndex;
418            case dotColumn:
419                return columnIndex;
420            default:
421                return null;
422        }
423    }
424
425    /**
426     * Immutable qualified name used as a canonical key.
427     *
428     * <p>This value object combines catalog.schema.object + type into a single
429     * key with cached hashCode for efficient map lookups.</p>
430     */
431    public static class QualifiedName {
432        private final String catalog;
433        private final String schema;
434        private final String objectName;
435        private final ESQLDataObjectType type;
436        private final int hashCode;
437
438        public QualifiedName(String catalog, String schema, String objectName, ESQLDataObjectType type) {
439            this.catalog = catalog;
440            this.schema = schema;
441            this.objectName = objectName;
442            this.type = type;
443            this.hashCode = computeHashCode();
444        }
445
446        public String getCatalog() {
447            return catalog;
448        }
449
450        public String getSchema() {
451            return schema;
452        }
453
454        public String getObjectName() {
455            return objectName;
456        }
457
458        public ESQLDataObjectType getType() {
459            return type;
460        }
461
462        @Override
463        public boolean equals(Object obj) {
464            if (this == obj) return true;
465            if (!(obj instanceof QualifiedName)) return false;
466
467            QualifiedName other = (QualifiedName) obj;
468            return Objects.equals(catalog, other.catalog)
469                && Objects.equals(schema, other.schema)
470                && Objects.equals(objectName, other.objectName)
471                && type == other.type;
472        }
473
474        @Override
475        public int hashCode() {
476            return hashCode;
477        }
478
479        private int computeHashCode() {
480            return Objects.hash(catalog, schema, objectName, type);
481        }
482
483        @Override
484        public String toString() {
485            StringBuilder sb = new StringBuilder();
486            if (catalog != null) {
487                sb.append(catalog).append('.');
488            }
489            if (schema != null) {
490                sb.append(schema).append('.');
491            }
492            sb.append(objectName);
493            sb.append(" (").append(type).append(')');
494            return sb.toString();
495        }
496    }
497}