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 maps provide O(1) lookup per object type, replacing the mixed schemaObjectMap.
027    // ConcurrentHashMap: name resolution reads them from other threads while DDL
028    // registration writes (shared-TSQLEnv concurrent analyzers); only keyed get/put,
029    // no iteration, so insertion order is not relied on.
030    private final java.util.concurrent.ConcurrentHashMap<String, TSQLTable> tables = new java.util.concurrent.ConcurrentHashMap<>();
031    private final java.util.concurrent.ConcurrentHashMap<String, TSQLFunction> functions = new java.util.concurrent.ConcurrentHashMap<>();
032    private final java.util.concurrent.ConcurrentHashMap<String, TSQLProcedure> procedures = new java.util.concurrent.ConcurrentHashMap<>();
033    private final java.util.concurrent.ConcurrentHashMap<String, TSQLTrigger> triggers = new java.util.concurrent.ConcurrentHashMap<>();
034    private final java.util.concurrent.ConcurrentHashMap<String, TSQLOraclePackage> oraclePackages = new java.util.concurrent.ConcurrentHashMap<>();
035    private final java.util.concurrent.ConcurrentHashMap<String, TSQLSynonyms> synonyms = new java.util.concurrent.ConcurrentHashMap<>();
036
037    // ===== Phase 3: Bucketed indexes for SQL Server COLLATION_BASED =====
038    // These provide ~25x performance improvement for SQL Server (10,000 objects: 20μs → 800ns).
039    // ConcurrentHashMap keyed by object type: lazy creation must be atomic — two
040    // threads racing the old per-field lazy init could each build an index and one
041    // set of writes would be lost.
042    private final java.util.concurrent.ConcurrentHashMap<ESQLDataObjectType, BucketedIndex> bucketedIndexes = new java.util.concurrent.ConcurrentHashMap<>();
043
044    // IdentifierService for consistent key generation (lazy initialization).
045    // volatile: duplicate (equivalent) instances under a racy lazy init are harmless,
046    // but a partially constructed instance must never be published to another thread.
047    private volatile IdentifierService identifierService;
048
049    // CollatorProvider for SQL Server (lazy initialization, volatile for safe publication)
050    private volatile CollatorProvider collatorProvider;
051
052    // ===== Phase 6: Schema seal mechanism for thread-safety hardening =====
053    /**
054     * Seal state flag (volatile for thread visibility).
055     */
056    private volatile boolean sealed = false;
057
058    /**
059     * Seal this schema to prevent further modifications (Phase 6).
060     *
061     * <p>See  for detailed documentation.
062     */
063    public void seal() {
064        this.sealed = true;
065    }
066
067    /**
068     * Check if this schema is sealed (Phase 6).
069     *
070     * @return true if sealed, false otherwise
071     */
072    public boolean isSealed() {
073        return sealed;
074    }
075
076    /**
077     * Check if schema is not sealed, throw exception if sealed and enforcement is enabled.
078     *
079     * @throws IllegalStateException if sealed and TBaseType.ENFORCE_CATALOG_SEAL is true
080     */
081    private void checkNotSealed() {
082        if (sealed && TBaseType.ENFORCE_CATALOG_SEAL) {
083            throw new IllegalStateException("Cannot modify sealed schema. Call seal() marks schema as read-only.");
084        }
085    }
086
087    /**
088     * Create a new instance of the schema, added to the catalog.
089     *
090     * @param sqlCatalog catalog contains this schema
091     * @param schemaName name of the schema
092     */
093    public TSQLSchema(TSQLCatalog sqlCatalog,String schemaName){
094        super(sqlCatalog.getSqlEnv(), schemaName,ESQLDataObjectType.dotSchema);
095        this.catalog = sqlCatalog;
096        this.catalog.addSchema(this);
097    }
098
099    /**
100     * Get or create IdentifierService (lazy initialization)
101     */
102    private IdentifierService getIdentifierService() {
103        if (identifierService == null) {
104            IdentifierProfile profile = IdentifierProfile.forVendor(
105                this.sqlEnv.getDBVendor(),
106                IdentifierProfile.VendorFlags.defaults()
107            );
108            identifierService = new IdentifierService(profile, null);
109        }
110        return identifierService;
111    }
112
113    /**
114     * Get or create CollatorProvider (lazy initialization, SQL Server only)
115     */
116    private CollatorProvider getCollatorProvider() {
117        if (collatorProvider == null) {
118            collatorProvider = new CollatorProvider();
119        }
120        return collatorProvider;
121    }
122
123    /**
124     * Check if SQL Server and bucketed index is enabled
125     */
126    private boolean shouldUseBucketedIndex() {
127        return TBaseType.USE_BUCKETED_INDEX &&
128               (this.sqlEnv.getDBVendor() == gudusoft.gsqlparser.EDbVendor.dbvmssql ||
129                this.sqlEnv.getDBVendor() == gudusoft.gsqlparser.EDbVendor.dbvazuresql);
130    }
131
132    /**
133     * Slice S3: cache of whether the hierarchical type-specific map is globally
134     * legacy-compatible for a given {@code (vendor, objectType)} pair.
135     *
136     * <p>Key format: {@code "<EDbVendor.name()>|<ESQLDataObjectType.name()>"}.
137     * Both the IdentifierProfile (built from {@link IdentifierProfile#forVendor})
138     * and the legacy {@link TSQLEnv} per-vendor case-sensitivity maps are static
139     * relative to the (vendor, type) tuple, so the answer is stable for the
140     * JVM lifetime and safe to cache process-wide.
141     */
142    private static final java.util.concurrent.ConcurrentHashMap<String, Boolean>
143        HIERARCHICAL_COMPAT_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
144
145    /**
146     * Slice S3: detect whether the hierarchical type-specific map can be safely
147     * consulted for the entire {@code (vendor, objectType)} pair.
148     *
149     * <p>The hierarchical map keys come from {@link IdentifierService#keyForMap}.
150     * The legacy {@code schemaObjectMap} keys come from
151     * {@link gudusoft.gsqlparser.util.SQLUtil#getIdentifierNormalName}.
152     *
153     * <p>For several vendor/type combinations the two key functions diverge:
154     * <ul>
155     *   <li>SQL Server / Azure SQL with {@code COLLATION_BASED} rules
156     *       ({@code IdentifierRules.forSQLServer}: {@code CaseFold.NONE}) when
157     *       {@link TSQLEnv#tableCollationCaseSensitive} reports {@code false}
158     *       (legacy adds a {@code toUpperCase} pass).</li>
159     *   <li>BigQuery tables ({@code IdentifierRules.forBigQueryTable}:
160     *       {@code CaseFold.NONE / SENSITIVE}) where
161     *       {@link TSQLEnv#tableCollationCaseSensitive} is {@code false}.</li>
162     *   <li>Hive/Teradata routines and any vendor whose {@link IdentifierRules}
163     *       differ from the legacy {@link TSQLEnv} per-type case-sensitivity
164     *       map.</li>
165     * </ul>
166     *
167     * <p>When the two diverge, distinct case-different writes land under
168     * different keys in hierarchical (both retained) but collapse to the same
169     * key in legacy (last write wins). A subsequent lookup that happens to
170     * use a probe input where the two functions <em>locally</em> agree (e.g.
171     * an already-uppercase {@code "FOO"} on BigQuery) would still hit the
172     * stale earlier hierarchical entry while legacy holds the authoritative
173     * later write. So input-local parity is insufficient — the gate must be
174     * vendor/type-global.
175     *
176     * <p>We probe once with a case-mixed ASCII input and cache the result.
177     * If the two key functions agree on the probe they agree on every input
178     * (they only differ in fold direction; a probe that survives both folds
179     * is sufficient). When they diverge, hierarchical is skipped permanently
180     * for that {@code (vendor, type)} and the lookup falls through to legacy.
181     *
182     * <p>This is intentionally conservative; once the strict-mode switch from
183     * plan §3.3 lands (slice S7) the gate can flip to "always trust
184     * hierarchical" and accept the (more correct) behavior change.
185     */
186    private boolean isHierarchicalCompatibleForType(ESQLDataObjectType objectType) {
187        try {
188            gudusoft.gsqlparser.EDbVendor vendor = this.sqlEnv.getDBVendor();
189            String cacheKey = vendor.name() + "|" + objectType.name();
190            Boolean cached = HIERARCHICAL_COMPAT_CACHE.get(cacheKey);
191            if (cached != null) {
192                return cached;
193            }
194            // Probe both unquoted and quoted representative inputs because
195            // IdentifierRules has independent {@code unquotedFold/Compare} and
196            // {@code quotedFold/Compare} axes; a vendor/type can match for one
197            // representation and diverge for the other (e.g. Snowflake where
198            // unquoted folds to UPPER on both sides but quoted preserves case
199            // in keyForMap while legacy still appends toUpperCase via
200            // tableCollationCaseSensitive=false).
201            String unquotedProbe = "ProbeXyz";
202            String quotedProbe = quoteIdentifierForProbe(vendor, unquotedProbe);
203            IdentifierService svc = getIdentifierService();
204            boolean compat = probeMatches(svc, vendor, unquotedProbe, objectType)
205                && probeMatches(svc, vendor, quotedProbe, objectType);
206            HIERARCHICAL_COMPAT_CACHE.put(cacheKey, compat);
207            return compat;
208        } catch (Throwable ignore) {
209            return false;
210        }
211    }
212
213    private boolean probeMatches(IdentifierService svc,
214                                 gudusoft.gsqlparser.EDbVendor vendor,
215                                 String probe,
216                                 ESQLDataObjectType objectType) {
217        String hierarchicalKey = svc.keyForMap(probe, objectType);
218        String legacyKey = SQLUtil.getIdentifierNormalName(vendor, probe, objectType);
219        return hierarchicalKey != null && hierarchicalKey.equals(legacyKey);
220    }
221
222    /**
223     * Wrap {@code name} in the vendor's quoted-identifier delimiter so the
224     * probe exercises {@code IdentifierRules.quotedFold/quotedCompare} and the
225     * legacy {@link gudusoft.gsqlparser.util.SQLUtil#normalizeIdentifier}
226     * branch that strips quotes before folding. Mirrors the dispatch in
227     * {@link TSQLEnv#isDelimitedIdentifier(gudusoft.gsqlparser.EDbVendor, String)}.
228     */
229    private static String quoteIdentifierForProbe(gudusoft.gsqlparser.EDbVendor vendor, String name) {
230        switch (vendor) {
231            case dbvmssql:
232            case dbvazuresql:
233                return "[" + name + "]";
234            case dbvmysql:
235            case dbvbigquery:
236            case dbvcouchbase:
237            case dbvhive:
238            case dbvimpala:
239                return "`" + name + "`";
240            case dbvdax:
241                return "'" + name + "'";
242            default:
243                return "\"" + name + "\"";
244        }
245    }
246
247    /**
248     * Get or create bucketed index for a specific object type
249     */
250    private BucketedIndex getBucketedIndex(ESQLDataObjectType objectType) {
251        if (!shouldUseBucketedIndex()) {
252            return null;
253        }
254
255        switch (objectType) {
256            case dotTable:
257            case dotFunction:
258            case dotProcedure:
259            case dotTrigger:
260            case dotOraclePackage:
261            case dotSynonyms:
262                // atomic lazy creation; a racy per-field init could build two
263                // indexes for the same type and lose one side's writes
264                return bucketedIndexes.computeIfAbsent(objectType, type ->
265                        new BucketedIndex(getCollatorProvider(),
266                                getIdentifierService().getProfile().getFlags().defaultCollation, type));
267            default:
268                return null;
269        }
270    }
271
272    public List<TSQLSchemaObject> getSchemaObjectList() {
273        List<TSQLSchemaObject> schemaObjectList = new LinkedList<>();
274        synchronized (schemaObjectMap) {
275            for(String schemaObjectKey: schemaObjectMap.keySet()){
276                TSQLSchemaObject schemaObject = schemaObjectMap.get(schemaObjectKey);
277                if(schemaObject instanceof TSQLProcedure){
278                    if(((TSQLProcedure)schemaObject).isOraclePackageProcedure()){
279                        continue;
280                    }
281                }
282                schemaObjectList.add(schemaObject);
283            }
284        }
285        return schemaObjectList;
286    }
287
288    public List<TSQLSchemaObject> getPackageObjectList(TSQLOraclePackage oraclePackage) {
289        List<TSQLSchemaObject> schemaObjectList = new LinkedList<>();
290        synchronized (schemaObjectMap) {
291            for (String schemaObjectKey : schemaObjectMap.keySet()) {
292                TSQLSchemaObject schemaObject = schemaObjectMap.get(schemaObjectKey);
293                if (schemaObject instanceof TSQLProcedure) {
294                    if (!((TSQLProcedure) schemaObject).isOraclePackageProcedure()) {
295                        continue;
296                    }
297                    TSQLProcedure procedure = (TSQLProcedure) schemaObject;
298                    if (procedure.getOraclePackage() == oraclePackage) {
299                        schemaObjectList.add(schemaObject);
300                    }
301                }
302            }
303        }
304        return schemaObjectList;
305    }
306
307    private Map<String, TSQLSchemaObject> schemaObjectMap = Collections.synchronizedMap(new LinkedHashMap<String, TSQLSchemaObject>( ));
308
309//    public boolean addTable(TSQLTable sqlTable){
310//        return addDataObject(sqlTable);
311//    }
312//
313//    public boolean addProcedure(TSQLProcedure sqlProcedure){
314//        return addDataObject(sqlProcedure);
315//    }
316//
317//    public boolean addFunction(TSQLFunction sqlFunction){
318//        return addDataObject(sqlFunction);
319//    }
320//
321//    public boolean addTrigger(TSQLTrigger sqlTrigger){
322//        return addDataObject(sqlTrigger);
323//    }
324
325    /**
326     * add database object
327     *
328     * <p><strong>Dual-write strategy:</strong> Updates both legacy schemaObjectMap
329     * and new type-specific maps for backward compatibility and performance optimization.
330     *
331     * @param schemaObject schema object to add
332     */
333    protected void addSchemaObject(TSQLSchemaObject schemaObject){
334        // same monitor as createSchemaObject: registration of one object must be
335        // atomic across all indexes (reentrant when called from createSchemaObject
336        // via the schema-object constructors).
337        // Note: env.putSchemaObject (and thus the catalog provider) runs while this
338        // monitor is held — in-tree providers never call back into schema/env
339        // creation; a custom ICatalogProvider that does could deadlock.
340        synchronized (schemaObjectMap) {
341            checkNotSealed();  // Phase 6: checked under the mutation lock (seal() itself takes no monitor, so this narrows, not closes, the seal race)
342            // ===== Legacy path: Update schemaObjectMap (backward compatibility) =====
343            String normalName = SQLUtil.getIdentifierNormalName(this.sqlEnv.getDBVendor(), schemaObject.name, schemaObject.getDataObjectType());
344            if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotFunction)
345            {
346                normalName = normalName+"$function";
347            }
348            else if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotProcedure)
349            {
350                normalName = normalName+"$procedure";
351            }
352            else if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotTrigger)
353            {
354                normalName = normalName+"$trigger";
355            }
356            else if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotOraclePackage)
357            {
358                normalName = normalName+"$package";
359            }
360            else if (schemaObject.getDataObjectType() == ESQLDataObjectType.dotSynonyms)
361            {
362                normalName = normalName+"$synonyms";
363            }
364            schemaObjectMap.put(normalName, schemaObject);
365            this.catalog.getSqlEnv().putSchemaObject(schemaObject.getQualifiedName(),schemaObject);
366
367            // ===== New path: Update type-specific maps (Phase 2 optimization) =====
368            if (TBaseType.USE_HIERARCHICAL_INDEX) {
369                try {
370                    String key = getIdentifierService().keyForMap(schemaObject.name, schemaObject.getDataObjectType());
371
372                    switch (schemaObject.getDataObjectType()) {
373                        case dotTable:
374                            tables.put(key, (TSQLTable) schemaObject);
375                            break;
376                        case dotFunction:
377                            functions.put(key, (TSQLFunction) schemaObject);
378                            break;
379                        case dotProcedure:
380                            procedures.put(key, (TSQLProcedure) schemaObject);
381                            break;
382                        case dotTrigger:
383                            triggers.put(key, (TSQLTrigger) schemaObject);
384                            break;
385                        case dotOraclePackage:
386                            oraclePackages.put(key, (TSQLOraclePackage) schemaObject);
387                            break;
388                        case dotSynonyms:
389                            synonyms.put(key, (TSQLSynonyms) schemaObject);
390                            break;
391                    }
392                } catch (Throwable ignore) {
393                    // Silently fail to maintain backward compatibility
394                }
395            }
396
397            // ===== Phase 3: Update bucketed index for SQL Server =====
398            if (shouldUseBucketedIndex()) {
399                try {
400                    BucketedIndex bucketedIndex = getBucketedIndex(schemaObject.getDataObjectType());
401                    if (bucketedIndex != null) {
402                        bucketedIndex.put(schemaObject.name, schemaObject);
403                    }
404                } catch (Throwable ignore) {
405                    // Silently fail to maintain backward compatibility
406                }
407            }
408        }
409    }
410
411    TSQLSchemaObject findSchemaObject(ESQLDataObjectType dataObjectType, String schemaObjectName) {
412        // Phase 3.5: Use bucketed index for SQL Server (25x faster for 10,000+ objects)
413        if (shouldUseBucketedIndex()) {
414            try {
415                BucketedIndex bucketedIndex = getBucketedIndex(dataObjectType);
416                if (bucketedIndex != null) {
417                    TSQLSchemaObject result = bucketedIndex.get(schemaObjectName);
418                    if (result != null) {
419                        if (TBaseType.LOG_INDEX_HIT_RATE) {
420                            System.out.println("[BucketedIndex] Hit: " + dataObjectType + " - " + schemaObjectName);
421                        }
422                        return result;
423                    }
424                }
425            } catch (Throwable ignore) {
426                // Fall through to legacy path
427            }
428        }
429
430        // Phase 2: Use hierarchical index (type-specific maps) when flag is enabled.
431        //
432        // Slice S3: Only consult hierarchical when its IdentifierService.keyForMap
433        // key function is globally compatible with legacy
434        // SQLUtil.getIdentifierNormalName for this {@code (vendor, type)} pair.
435        // See {@link #isHierarchicalCompatibleForType} for the rationale; in
436        // particular, an input-local parity check is insufficient because a
437        // case-different earlier write may already have populated hierarchical
438        // under a key that legacy collapsed (e.g. BigQuery: writes "FOO" then
439        // "foo"; legacy retains only the last; hierarchical retains both;
440        // querying "FOO" would still return the stale earlier hierarchical
441        // entry). Falling through to legacy preserves write/read symmetry.
442        if (TBaseType.USE_HIERARCHICAL_INDEX
443                && isHierarchicalCompatibleForType(dataObjectType)) {
444            try {
445                String key = getIdentifierService().keyForMap(schemaObjectName, dataObjectType);
446                TSQLSchemaObject result = null;
447
448                switch (dataObjectType) {
449                    case dotTable:
450                        result = tables.get(key);
451                        break;
452                    case dotFunction:
453                        result = functions.get(key);
454                        break;
455                    case dotProcedure:
456                        result = procedures.get(key);
457                        break;
458                    case dotTrigger:
459                        result = triggers.get(key);
460                        break;
461                    case dotOraclePackage:
462                        result = oraclePackages.get(key);
463                        break;
464                    case dotSynonyms:
465                        result = synonyms.get(key);
466                        break;
467                }
468
469                if (result != null) {
470                    // Log hit rate if enabled
471                    if (TBaseType.LOG_INDEX_HIT_RATE) {
472                        System.out.println("[HierarchicalIndex] Hit: " + dataObjectType + " - " + schemaObjectName);
473                    }
474                    return result;
475                }
476            } catch (Throwable ignore) {
477                // Fall through to legacy path
478            }
479        }
480
481        // Legacy path: use suffix hacks and local map (backward compatibility)
482        String normalName = SQLUtil.getIdentifierNormalName(this.sqlEnv.getDBVendor(), schemaObjectName, dataObjectType);
483        if (dataObjectType == ESQLDataObjectType.dotFunction)
484        {
485            normalName = normalName+"$function";
486        }
487        else if (dataObjectType == ESQLDataObjectType.dotProcedure)
488        {
489            normalName = normalName+"$procedure";
490        }
491        else if (dataObjectType == ESQLDataObjectType.dotTrigger)
492        {
493            normalName = normalName+"$trigger";
494        }
495        else if (dataObjectType == ESQLDataObjectType.dotOraclePackage)
496        {
497            normalName = normalName+"$package";
498        }
499        else if (dataObjectType == ESQLDataObjectType.dotSynonyms)
500        {
501            normalName = normalName+"$synonyms";
502        }
503        return schemaObjectMap.get(normalName);
504    }
505
506    /**
507     * create a oracle package belong to this schema
508     *
509     * @param oraclePackageName oracle package name
510     * @return an instance of the oracle package
511     */
512    public TSQLOraclePackage createOraclePackage(String oraclePackageName){
513        return (TSQLOraclePackage)createSchemaObject(oraclePackageName,ESQLDataObjectType.dotOraclePackage);
514    }
515
516    /**
517     * create a procedure belong to this schema
518     *
519     * @param procedureName procedure name
520     * @return an instance of the SQL procedure
521     */
522    public TSQLProcedure createProcedure(String procedureName){
523        return (TSQLProcedure)createSchemaObject(procedureName,ESQLDataObjectType.dotProcedure);
524    }
525
526    public TSQLProcedure createProcedure(String procedureName, ESQLDataObjectType type){
527        return (TSQLProcedure)createSchemaObject(procedureName,type);
528    }
529
530    /**
531     * create a function belong to this schema
532     *
533     * @param functionName function name
534     * @return an instance of the SQL function.
535     */
536    public TSQLFunction createFunction(String functionName){
537        return (TSQLFunction)createSchemaObject(functionName,ESQLDataObjectType.dotFunction);
538    }
539
540    /**
541     * create a trigger belong to this schema
542     *
543     * @param triggerName trigger name
544     * @return an instance of the SQL trigger.
545     */
546    public TSQLTrigger createTrigger(String triggerName){
547        return (TSQLTrigger)createSchemaObject(triggerName,ESQLDataObjectType.dotTrigger);
548    }
549
550    /**
551     * create a synonyms belong to this schema
552     *
553     * @param synonymsName synonyms name
554     * @return an instance of the SQL synonyms
555     */
556    public TSQLSynonyms createSynonyms(String synonymsName){
557        return (TSQLSynonyms)createSchemaObject(synonymsName,ESQLDataObjectType.dotSynonyms);
558    }
559
560    /**
561     * create a synonyms belong to this schema and record its base target.
562     *
563     * <p>If a synonym with the same name already exists it is returned with its
564     * base target updated. Any of {@code sourceDatabase}/{@code sourceSchema} may
565     * be {@code null} to mean "the synonym's own database/schema"; cross-database
566     * base targets are allowed.
567     *
568     * @param synonymsName   synonyms name
569     * @param sourceDatabase base database/catalog, or {@code null}
570     * @param sourceSchema   base schema, or {@code null}
571     * @param sourceName     base object name
572     * @return an instance of the SQL synonyms
573     */
574    public TSQLSynonyms createSynonyms(String synonymsName, String sourceDatabase, String sourceSchema, String sourceName){
575        TSQLSynonyms synonyms = createSynonyms(synonymsName);
576        if (synonyms != null) {
577            synonyms.setBaseTarget(sourceDatabase, sourceSchema, sourceName);
578        }
579        return synonyms;
580    }
581
582    /**
583     * create a table belong to this schema. If table with the same name already exists in the schema
584     * return the existing table.
585     *
586     * @param tableName table name
587     * @return an instance of the SQL table
588     */
589    public TSQLTable createTable(String tableName){
590        return (TSQLTable)createSchemaObject(tableName,ESQLDataObjectType.dotTable);
591    }
592
593    public TSQLTable createTable(String tableName, int priority){
594        return (TSQLTable)createSchemaObject(tableName,ESQLDataObjectType.dotTable, priority);
595    }
596
597    // Slice S3: route public lookups through findSchemaObject so they go through
598    // the bucketed → hierarchical (IdentifierService.keyForMap) → legacy fallback
599    // chain, instead of bypassing both indexes via raw schemaObjectMap.
600    public boolean containsTable(String tableName) {
601        return findSchemaObject(ESQLDataObjectType.dotTable, tableName) != null;
602    }
603
604    public TSQLTable findTable(String tableName) {
605        return (TSQLTable) findSchemaObject(ESQLDataObjectType.dotTable, tableName);
606    }
607
608    public TSQLSynonyms findSynonyms(String synonymsName) {
609        return (TSQLSynonyms) findSchemaObject(ESQLDataObjectType.dotSynonyms, synonymsName);
610    }
611    
612        public TSQLSchemaObject findSchemaObject(String schemaObjectName) {
613                TSQLSchemaObject object = findSchemaObject(ESQLDataObjectType.dotFunction, schemaObjectName);
614                if (object == null) {
615                        object = findSchemaObject(ESQLDataObjectType.dotProcedure, schemaObjectName);
616                }
617                if (object == null) {
618                        object = findSchemaObject(ESQLDataObjectType.dotTrigger, schemaObjectName);
619                }
620                if (object == null) {
621                        object = findSchemaObject(ESQLDataObjectType.dotOraclePackage, schemaObjectName);
622                }
623                if (object == null) {
624                        object = findSchemaObject(ESQLDataObjectType.dotSynonyms, schemaObjectName);
625                }
626                return object;
627        }
628
629    /**
630     *
631     * @param schemaObjectName 该名称不带 catalog, schema 前缀
632     * @param dataObjectType
633     * @return
634     */
635    public TSQLSchemaObject createSchemaObject(String schemaObjectName, ESQLDataObjectType dataObjectType){
636        return createSchemaObject(schemaObjectName, dataObjectType, 0);
637    }
638
639    /**
640     *
641     * @param schemaObjectName 该名称不带 catalog, schema 前缀
642     * @param dataObjectType
643     * @param priority
644     * @return
645     */
646    protected TSQLSchemaObject createSchemaObject(String schemaObjectName, ESQLDataObjectType dataObjectType, int priority){
647        TSQLSchemaObject result = null;
648        // synchronized so concurrent get-or-create of the same object can't produce
649        // duplicate twins (columns added to the losing twin would be invisible).
650        // The schema-object constructors re-enter this monitor via addSchemaObject —
651        // reentrant, keep both on the same lock or the race comes back.
652        synchronized (schemaObjectMap) {
653            TSQLSchemaObject schemaObject = findSchemaObject(dataObjectType, schemaObjectName);
654            if (schemaObject == null){
655                switch (dataObjectType){
656                    case dotTable:
657                        result = new TSQLTable(this,schemaObjectName);
658                        break;
659                    case dotOraclePackage:
660                        result = new TSQLOraclePackage(this,schemaObjectName);
661                        break;
662                    case dotProcedure:
663                        result = new TSQLProcedure(this,schemaObjectName);
664                        break;
665                    case dotFunction:
666                        result = new TSQLFunction(this,schemaObjectName);
667                        break;
668                    case dotTrigger:
669                        result = new TSQLTrigger(this,schemaObjectName);
670                        break;
671                    case dotSynonyms:
672                        result = new TSQLSynonyms(this,schemaObjectName);
673                        break;
674                }
675            }else if (dataObjectType == schemaObject.getDataObjectType()){
676                result = schemaObject;
677            }else{
678                System.out.println("object name conflict:"+getQualifiedName()+"."+schemaObjectName+",type:"+dataObjectType+" VS "+schemaObject.getQualifiedName()+", type:"+schemaObject.getDataObjectType());
679            };
680
681            if (result!=null && priority > result.getPriority()) {
682                result.setPriority(priority);
683            }
684        }
685        return result;
686    }
687
688}