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