001package gudusoft.gsqlparser.sqlenv;
002
003
004import gudusoft.gsqlparser.TBaseType;
005import gudusoft.gsqlparser.util.SQLUtil;
006
007import java.util.*;
008
009/**
010 * SQL schema, contains a list of schema objects.
011 */
012public class TSQLSchema extends TSQLObject {
013
014
015    public String getQualifiedName(){
016        return catalog.name+"."+name;
017    }
018
019    private TSQLCatalog catalog;
020
021    public TSQLCatalog getCatalog() {
022        return catalog;
023    }
024
025    // ===== Shadow Introduction: New type-specific indexes (Phase 2) =====
026    // These LinkedHashMaps provide O(1) lookup per object type, replacing the mixed schemaObjectMap
027    private final LinkedHashMap<String, TSQLTable> tables = new LinkedHashMap<>();
028    private final LinkedHashMap<String, TSQLFunction> functions = new LinkedHashMap<>();
029    private final LinkedHashMap<String, TSQLProcedure> procedures = new LinkedHashMap<>();
030    private final LinkedHashMap<String, TSQLTrigger> triggers = new LinkedHashMap<>();
031    private final LinkedHashMap<String, TSQLOraclePackage> oraclePackages = new LinkedHashMap<>();
032    private final LinkedHashMap<String, TSQLSynonyms> synonyms = new LinkedHashMap<>();
033
034    // ===== Phase 3: Bucketed indexes for SQL Server COLLATION_BASED =====
035    // These provide ~25x performance improvement for SQL Server (10,000 objects: 20μs → 800ns)
036    private BucketedIndex bucketedTables;
037    private BucketedIndex bucketedFunctions;
038    private BucketedIndex bucketedProcedures;
039    private BucketedIndex bucketedTriggers;
040    private BucketedIndex bucketedOraclePackages;
041    private BucketedIndex bucketedSynonyms;
042
043    // IdentifierService for consistent key generation (lazy initialization)
044    private IdentifierService identifierService;
045
046    // CollatorProvider for SQL Server (lazy initialization)
047    private CollatorProvider collatorProvider;
048
049    // ===== Phase 6: Schema seal mechanism for thread-safety hardening =====
050    /**
051     * Seal state flag (volatile for thread visibility).
052     */
053    private volatile boolean sealed = false;
054
055    /**
056     * Seal this schema to prevent further modifications (Phase 6).
057     *
058     * <p>See {@link TSQLEnv#seal()} for detailed documentation.
059     */
060    public void seal() {
061        this.sealed = true;
062    }
063
064    /**
065     * Check if this schema is sealed (Phase 6).
066     *
067     * @return true if sealed, false otherwise
068     */
069    public boolean isSealed() {
070        return sealed;
071    }
072
073    /**
074     * Check if schema is not sealed, throw exception if sealed and enforcement is enabled.
075     *
076     * @throws IllegalStateException if sealed and TBaseType.ENFORCE_CATALOG_SEAL is true
077     */
078    private void checkNotSealed() {
079        if (sealed && TBaseType.ENFORCE_CATALOG_SEAL) {
080            throw new IllegalStateException("Cannot modify sealed schema. Call seal() marks schema as read-only.");
081        }
082    }
083
084    /**
085     * Create a new instance of the schema, added to the catalog.
086     *
087     * @param sqlCatalog catalog contains this schema
088     * @param schemaName name of the schema
089     */
090    public TSQLSchema(TSQLCatalog sqlCatalog,String schemaName){
091        super(sqlCatalog.getSqlEnv(), schemaName,ESQLDataObjectType.dotSchema);
092        this.catalog = sqlCatalog;
093        this.catalog.addSchema(this);
094    }
095
096    /**
097     * Get or create IdentifierService (lazy initialization)
098     */
099    private IdentifierService getIdentifierService() {
100        if (identifierService == null) {
101            IdentifierProfile profile = IdentifierProfile.forVendor(
102                this.sqlEnv.getDBVendor(),
103                IdentifierProfile.VendorFlags.defaults()
104            );
105            identifierService = new IdentifierService(profile, null);
106        }
107        return identifierService;
108    }
109
110    /**
111     * Get or create CollatorProvider (lazy initialization, SQL Server only)
112     */
113    private CollatorProvider getCollatorProvider() {
114        if (collatorProvider == null) {
115            collatorProvider = new CollatorProvider();
116        }
117        return collatorProvider;
118    }
119
120    /**
121     * Check if SQL Server and bucketed index is enabled
122     */
123    private boolean shouldUseBucketedIndex() {
124        return TBaseType.USE_BUCKETED_INDEX &&
125               (this.sqlEnv.getDBVendor() == gudusoft.gsqlparser.EDbVendor.dbvmssql ||
126                this.sqlEnv.getDBVendor() == gudusoft.gsqlparser.EDbVendor.dbvazuresql);
127    }
128
129    /**
130     * Get or create bucketed index for a specific object type
131     */
132    private BucketedIndex getBucketedIndex(ESQLDataObjectType objectType) {
133        if (!shouldUseBucketedIndex()) {
134            return null;
135        }
136
137        String collation = getIdentifierService().getProfile().getFlags().defaultCollation;
138        CollatorProvider provider = getCollatorProvider();
139
140        switch (objectType) {
141            case dotTable:
142                if (bucketedTables == null) {
143                    bucketedTables = new BucketedIndex(provider, collation, objectType);
144                }
145                return bucketedTables;
146
147            case dotFunction:
148                if (bucketedFunctions == null) {
149                    bucketedFunctions = new BucketedIndex(provider, collation, objectType);
150                }
151                return bucketedFunctions;
152
153            case dotProcedure:
154                if (bucketedProcedures == null) {
155                    bucketedProcedures = new BucketedIndex(provider, collation, objectType);
156                }
157                return bucketedProcedures;
158
159            case dotTrigger:
160                if (bucketedTriggers == null) {
161                    bucketedTriggers = new BucketedIndex(provider, collation, objectType);
162                }
163                return bucketedTriggers;
164
165            case dotOraclePackage:
166                if (bucketedOraclePackages == null) {
167                    bucketedOraclePackages = new BucketedIndex(provider, collation, objectType);
168                }
169                return bucketedOraclePackages;
170
171            case dotSynonyms:
172                if (bucketedSynonyms == null) {
173                    bucketedSynonyms = new BucketedIndex(provider, collation, objectType);
174                }
175                return bucketedSynonyms;
176
177            default:
178                return null;
179        }
180    }
181
182    public List<TSQLSchemaObject> getSchemaObjectList() {
183        List<TSQLSchemaObject> schemaObjectList = new LinkedList<>();
184        synchronized (schemaObjectMap) {
185            for(String schemaObjectKey: schemaObjectMap.keySet()){
186                TSQLSchemaObject schemaObject = schemaObjectMap.get(schemaObjectKey);
187                if(schemaObject instanceof TSQLProcedure){
188                    if(((TSQLProcedure)schemaObject).isOraclePackageProcedure()){
189                        continue;
190                    }
191                }
192                schemaObjectList.add(schemaObject);
193            }
194        }
195        return schemaObjectList;
196    }
197
198    public List<TSQLSchemaObject> getPackageObjectList(TSQLOraclePackage oraclePackage) {
199        List<TSQLSchemaObject> schemaObjectList = new LinkedList<>();
200        synchronized (schemaObjectMap) {
201            for (String schemaObjectKey : schemaObjectMap.keySet()) {
202                TSQLSchemaObject schemaObject = schemaObjectMap.get(schemaObjectKey);
203                if (schemaObject instanceof TSQLProcedure) {
204                    if (!((TSQLProcedure) schemaObject).isOraclePackageProcedure()) {
205                        continue;
206                    }
207                    TSQLProcedure procedure = (TSQLProcedure) schemaObject;
208                    if (procedure.getOraclePackage() == oraclePackage) {
209                        schemaObjectList.add(schemaObject);
210                    }
211                }
212            }
213        }
214        return schemaObjectList;
215    }
216
217    private Map<String, TSQLSchemaObject> schemaObjectMap = Collections.synchronizedMap(new LinkedHashMap<String, TSQLSchemaObject>( ));
218
219//    public boolean addTable(TSQLTable sqlTable){
220//        return addDataObject(sqlTable);
221//    }
222//
223//    public boolean addProcedure(TSQLProcedure sqlProcedure){
224//        return addDataObject(sqlProcedure);
225//    }
226//
227//    public boolean addFunction(TSQLFunction sqlFunction){
228//        return addDataObject(sqlFunction);
229//    }
230//
231//    public boolean addTrigger(TSQLTrigger sqlTrigger){
232//        return addDataObject(sqlTrigger);
233//    }
234
235    /**
236     * add database object
237     *
238     * <p><strong>Dual-write strategy:</strong> Updates both legacy schemaObjectMap
239     * and new type-specific maps for backward compatibility and performance optimization.
240     *
241     * @param schemaObject schema object to add
242     */
243    protected void addSchemaObject(TSQLSchemaObject schemaObject){
244        checkNotSealed();  // Phase 6: Prevent modification after seal
245        // ===== Legacy path: Update schemaObjectMap (backward compatibility) =====
246        String normalName = SQLUtil.getIdentifierNormalName(this.sqlEnv.getDBVendor(), schemaObject.name, schemaObject.getDataObjectType());
247        if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotFunction)
248        {
249            normalName = normalName+"$function";
250        }
251        else if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotProcedure)
252        {
253            normalName = normalName+"$procedure";
254        }
255        else if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotTrigger)
256        {
257            normalName = normalName+"$trigger";
258        }
259        else if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotOraclePackage)
260        {
261            normalName = normalName+"$package";
262        }
263        schemaObjectMap.put(normalName, schemaObject);
264        this.catalog.getSqlEnv().putSchemaObject(schemaObject.getQualifiedName(),schemaObject);
265
266        // ===== New path: Update type-specific maps (Phase 2 optimization) =====
267        if (TBaseType.USE_HIERARCHICAL_INDEX) {
268            try {
269                String key = getIdentifierService().keyForMap(schemaObject.name, schemaObject.getDataObjectType());
270
271                switch (schemaObject.getDataObjectType()) {
272                    case dotTable:
273                        tables.put(key, (TSQLTable) schemaObject);
274                        break;
275                    case dotFunction:
276                        functions.put(key, (TSQLFunction) schemaObject);
277                        break;
278                    case dotProcedure:
279                        procedures.put(key, (TSQLProcedure) schemaObject);
280                        break;
281                    case dotTrigger:
282                        triggers.put(key, (TSQLTrigger) schemaObject);
283                        break;
284                    case dotOraclePackage:
285                        oraclePackages.put(key, (TSQLOraclePackage) schemaObject);
286                        break;
287                    case dotSynonyms:
288                        synonyms.put(key, (TSQLSynonyms) schemaObject);
289                        break;
290                }
291            } catch (Throwable ignore) {
292                // Silently fail to maintain backward compatibility
293            }
294        }
295
296        // ===== Phase 3: Update bucketed index for SQL Server =====
297        if (shouldUseBucketedIndex()) {
298            try {
299                BucketedIndex bucketedIndex = getBucketedIndex(schemaObject.getDataObjectType());
300                if (bucketedIndex != null) {
301                    bucketedIndex.put(schemaObject.name, schemaObject);
302                }
303            } catch (Throwable ignore) {
304                // Silently fail to maintain backward compatibility
305            }
306        }
307    }
308
309    TSQLSchemaObject findSchemaObject(ESQLDataObjectType dataObjectType, String schemaObjectName) {
310        // Phase 3.5: Use bucketed index for SQL Server (25x faster for 10,000+ objects)
311        if (shouldUseBucketedIndex()) {
312            try {
313                BucketedIndex bucketedIndex = getBucketedIndex(dataObjectType);
314                if (bucketedIndex != null) {
315                    TSQLSchemaObject result = bucketedIndex.get(schemaObjectName);
316                    if (result != null) {
317                        if (TBaseType.LOG_INDEX_HIT_RATE) {
318                            System.out.println("[BucketedIndex] Hit: " + dataObjectType + " - " + schemaObjectName);
319                        }
320                        return result;
321                    }
322                }
323            } catch (Throwable ignore) {
324                // Fall through to legacy path
325            }
326        }
327
328        // Phase 2: Use hierarchical index (type-specific maps) when flag is enabled
329        if (TBaseType.USE_HIERARCHICAL_INDEX) {
330            try {
331                String key = getIdentifierService().keyForMap(schemaObjectName, dataObjectType);
332                TSQLSchemaObject result = null;
333
334                switch (dataObjectType) {
335                    case dotTable:
336                        result = tables.get(key);
337                        break;
338                    case dotFunction:
339                        result = functions.get(key);
340                        break;
341                    case dotProcedure:
342                        result = procedures.get(key);
343                        break;
344                    case dotTrigger:
345                        result = triggers.get(key);
346                        break;
347                    case dotOraclePackage:
348                        result = oraclePackages.get(key);
349                        break;
350                    case dotSynonyms:
351                        result = synonyms.get(key);
352                        break;
353                }
354
355                if (result != null) {
356                    // Log hit rate if enabled
357                    if (TBaseType.LOG_INDEX_HIT_RATE) {
358                        System.out.println("[HierarchicalIndex] Hit: " + dataObjectType + " - " + schemaObjectName);
359                    }
360                    return result;
361                }
362            } catch (Throwable ignore) {
363                // Fall through to legacy path
364            }
365        }
366
367        // Legacy path: use suffix hacks and local map (backward compatibility)
368        String normalName = SQLUtil.getIdentifierNormalName(this.sqlEnv.getDBVendor(), schemaObjectName, dataObjectType);
369        if (dataObjectType == ESQLDataObjectType.dotFunction)
370        {
371            normalName = normalName+"$function";
372        }
373        else if (dataObjectType == ESQLDataObjectType.dotProcedure)
374        {
375            normalName = normalName+"$procedure";
376        }
377        else if (dataObjectType == ESQLDataObjectType.dotTrigger)
378        {
379            normalName = normalName+"$trigger";
380        }
381        else if (dataObjectType == ESQLDataObjectType.dotOraclePackage)
382        {
383            normalName = normalName+"$package";
384        }
385        return schemaObjectMap.get(normalName);
386    }
387
388    /**
389     * create a oracle package belong to this schema
390     *
391     * @param oraclePackageName oracle package name
392     * @return an instance of the oracle package
393     */
394    public TSQLOraclePackage createOraclePackage(String oraclePackageName){
395        return (TSQLOraclePackage)createSchemaObject(oraclePackageName,ESQLDataObjectType.dotOraclePackage);
396    }
397
398    /**
399     * create a procedure belong to this schema
400     *
401     * @param procedureName procedure name
402     * @return an instance of the SQL procedure
403     */
404    public TSQLProcedure createProcedure(String procedureName){
405        return (TSQLProcedure)createSchemaObject(procedureName,ESQLDataObjectType.dotProcedure);
406    }
407
408    public TSQLProcedure createProcedure(String procedureName, ESQLDataObjectType type){
409        return (TSQLProcedure)createSchemaObject(procedureName,type);
410    }
411
412    /**
413     * create a function belong to this schema
414     *
415     * @param functionName function name
416     * @return an instance of the SQL function.
417     */
418    public TSQLFunction createFunction(String functionName){
419        return (TSQLFunction)createSchemaObject(functionName,ESQLDataObjectType.dotFunction);
420    }
421
422    /**
423     * create a trigger belong to this schema
424     *
425     * @param triggerName trigger name
426     * @return an instance of the SQL trigger.
427     */
428    public TSQLTrigger createTrigger(String triggerName){
429        return (TSQLTrigger)createSchemaObject(triggerName,ESQLDataObjectType.dotTrigger);
430    }
431
432    /**
433     * create a synonyms belong to this schema
434     *
435     * @param synonymsName synonyms name
436     * @return an instance of the SQL synonyms
437     */
438    public TSQLSynonyms createSynonyms(String synonymsName){
439        return (TSQLSynonyms)createSchemaObject(synonymsName,ESQLDataObjectType.dotSynonyms);
440    }
441
442    /**
443     * create a table belong to this schema. If table with the same name already exists in the schema
444     * return the existing table.
445     *
446     * @param tableName table name
447     * @return an instance of the SQL table
448     */
449    public TSQLTable createTable(String tableName){
450        return (TSQLTable)createSchemaObject(tableName,ESQLDataObjectType.dotTable);
451    }
452
453    public TSQLTable createTable(String tableName, int priority){
454        return (TSQLTable)createSchemaObject(tableName,ESQLDataObjectType.dotTable, priority);
455    }
456
457        public boolean containsTable(String tableName) {
458                return schemaObjectMap.containsKey(
459                                SQLUtil.getIdentifierNormalName(this.sqlEnv.getDBVendor(), tableName, ESQLDataObjectType.dotTable));
460        }
461
462    public TSQLTable findTable(String tableName) {
463        return (TSQLTable)schemaObjectMap.get(
464                SQLUtil.getIdentifierNormalName(this.sqlEnv.getDBVendor(), tableName, ESQLDataObjectType.dotTable));
465    }
466    
467        public TSQLSchemaObject findSchemaObject(String schemaObjectName) {
468                TSQLSchemaObject object = findSchemaObject(ESQLDataObjectType.dotFunction, schemaObjectName);
469                if (object == null) {
470                        object = findSchemaObject(ESQLDataObjectType.dotProcedure, schemaObjectName);
471                }
472                if (object == null) {
473                        object = findSchemaObject(ESQLDataObjectType.dotTrigger, schemaObjectName);
474                }
475                if (object == null) {
476                        object = findSchemaObject(ESQLDataObjectType.dotOraclePackage, schemaObjectName);
477                }
478                return object;
479        }
480
481    /**
482     *
483     * @param schemaObjectName 该名称不带 catalog, schema 前缀
484     * @param dataObjectType
485     * @return
486     */
487    public TSQLSchemaObject createSchemaObject(String schemaObjectName, ESQLDataObjectType dataObjectType){
488        return createSchemaObject(schemaObjectName, dataObjectType, 0);
489    }
490
491    /**
492     *
493     * @param schemaObjectName 该名称不带 catalog, schema 前缀
494     * @param dataObjectType
495     * @param priority
496     * @return
497     */
498    protected TSQLSchemaObject createSchemaObject(String schemaObjectName, ESQLDataObjectType dataObjectType, int priority){
499        TSQLSchemaObject result = null;
500        TSQLSchemaObject schemaObject = findSchemaObject(dataObjectType, schemaObjectName);
501        if (schemaObject == null){
502            switch (dataObjectType){
503                case dotTable:
504                    result = new TSQLTable(this,schemaObjectName);
505                    break;
506                case dotOraclePackage:
507                    result = new TSQLOraclePackage(this,schemaObjectName);
508                    break;
509                case dotProcedure:
510                    result = new TSQLProcedure(this,schemaObjectName);
511                    break;
512                case dotFunction:
513                    result = new TSQLFunction(this,schemaObjectName);
514                    break;
515                case dotTrigger:
516                    result = new TSQLTrigger(this,schemaObjectName);
517                    break;
518                case dotSynonyms:
519                    result = new TSQLSynonyms(this,schemaObjectName);
520                    break;
521            }
522        }else if (dataObjectType == schemaObject.getDataObjectType()){
523            result = schemaObject;
524        }else{
525            System.out.println("object name conflict:"+getQualifiedName()+"."+schemaObjectName+",type:"+dataObjectType+" VS "+schemaObject.getQualifiedName()+", type:"+schemaObject.getDataObjectType());
526        };
527
528        if (result!=null && priority > result.getPriority()) {
529            result.setPriority(priority);
530        }
531        return result;
532    }
533
534}