001package gudusoft.gsqlparser.sqlenv;
002
003
004import gudusoft.gsqlparser.TBaseType;
005
006import java.util.*;
007
008/**
009 * SQL schema, contains a list of schema objects.
010 */
011public class TSQLSchema extends TSQLObject {
012
013
014    public String getQualifiedName(){
015        return catalog.name+"."+name;
016    }
017
018    private TSQLCatalog catalog;
019
020    public TSQLCatalog getCatalog() {
021        return catalog;
022    }
023
024
025    // IdentifierService for consistent key generation (lazy initialization).
026    // volatile: duplicate (equivalent) instances under a racy lazy init are harmless,
027    // but a partially constructed instance must never be published to another thread.
028    private volatile IdentifierService identifierService;
029
030    // ===== Phase 6: Schema seal mechanism for thread-safety hardening =====
031    /**
032     * Seal state flag (volatile for thread visibility).
033     */
034    private volatile boolean sealed = false;
035
036    /**
037     * Seal this schema to prevent further modifications (Phase 6).
038     *
039     * <p>See  for detailed documentation.
040     */
041    public void seal() {
042        this.sealed = true;
043    }
044
045    /**
046     * Check if this schema is sealed (Phase 6).
047     *
048     * @return true if sealed, false otherwise
049     */
050    public boolean isSealed() {
051        return sealed;
052    }
053
054    /**
055     * Check if schema is not sealed, throw exception if sealed and enforcement is enabled.
056     *
057     * @throws IllegalStateException if sealed and TBaseType.ENFORCE_CATALOG_SEAL is true
058     */
059    private void checkNotSealed() {
060        if (sealed && TBaseType.ENFORCE_CATALOG_SEAL) {
061            throw new IllegalStateException("Cannot modify sealed schema. Call seal() marks schema as read-only.");
062        }
063    }
064
065    /**
066     * Create a new instance of the schema, added to the catalog.
067     *
068     * @param sqlCatalog catalog contains this schema
069     * @param schemaName name of the schema
070     */
071    public TSQLSchema(TSQLCatalog sqlCatalog,String schemaName){
072        super(sqlCatalog.getSqlEnv(), schemaName,ESQLDataObjectType.dotSchema);
073        this.catalog = sqlCatalog;
074        this.catalog.addSchema(this);
075    }
076
077    /**
078     * The identifier service of the environment this object belongs to (lazy initialization).
079     *
080     * <p>Inherited, never rebuilt from the vendor: this object's local indexes are part of the
081     * same catalog the environment's {@link CatalogStore} indexes, so keying them under the
082     * vendor default while the environment ran a per-category configuration would let one half
083     * of a lookup answer a question the other half never asked.
084     */
085    private IdentifierService getIdentifierService() {
086        if (identifierService == null) {
087            if (this.sqlEnv == null) {
088                // Every catalog object is created against an environment; with none there is no
089                // vendor and no configuration to inherit, so say so instead of guessing one.
090                throw new IllegalStateException(getClass().getSimpleName()
091                        + " has no TSQLEnv, so it has no identifier configuration to compare with");
092            }
093            identifierService = this.sqlEnv.getIdentifierService();
094        }
095        return identifierService;
096    }
097
098
099
100    public List<TSQLSchemaObject> getSchemaObjectList() {
101        List<TSQLSchemaObject> schemaObjectList = new LinkedList<>();
102        synchronized (schemaObjectMap) {
103            for(TSQLSchemaObject schemaObject: schemaObjectMap.values()){
104                if(schemaObject instanceof TSQLProcedure){
105                    if(((TSQLProcedure)schemaObject).isOraclePackageProcedure()){
106                        continue;
107                    }
108                }
109                schemaObjectList.add(schemaObject);
110            }
111        }
112        return schemaObjectList;
113    }
114
115    public List<TSQLSchemaObject> getPackageObjectList(TSQLOraclePackage oraclePackage) {
116        List<TSQLSchemaObject> schemaObjectList = new LinkedList<>();
117        synchronized (schemaObjectMap) {
118            for (TSQLSchemaObject schemaObject : schemaObjectMap.values()) {
119                if (schemaObject instanceof TSQLProcedure) {
120                    if (!((TSQLProcedure) schemaObject).isOraclePackageProcedure()) {
121                        continue;
122                    }
123                    TSQLProcedure procedure = (TSQLProcedure) schemaObject;
124                    if (procedure.getOraclePackage() == oraclePackage) {
125                        schemaObjectList.add(schemaObject);
126                    }
127                }
128            }
129        }
130        return schemaObjectList;
131    }
132
133    // Every vendor keys this map with QualifiedCanonKey (one segment plus object
134    // type) built from the environment's own IdentifierService, so the answer here
135    // is the answer the configured IdentifierProfile gives. It used to be a
136    // Snowflake/BigQuery allowlist with legacy String keys for the other 52
137    // vendors, which made TSQLEnv's two searchTable overloads disagree: the String
138    // overload reaches CatalogStore and honors the profile, the TObjectName overload
139    // reaches this map and did not. A configured per-category rule was then honored
140    // or ignored depending on which overload the caller happened to use, and
141    // dlineage uses both (DataFlowAnalyzer passes a TObjectName, ModelFactory and
142    // Table pass a String). The legacy key was also the LOOSER of the two, so the
143    // disagreement surfaced as a confident hit on an object the configuration calls
144    // a different object, which nothing downstream flags.
145    private final Map<Object, TSQLSchemaObject> schemaObjectMap =
146            Collections.synchronizedMap(new LinkedHashMap<Object, TSQLSchemaObject>());
147
148
149    private QualifiedCanonKey schemaKey(String name, ESQLDataObjectType type, IdentifierInputForm form) {
150        // This API receives a LOCAL name: a dot inside it is content, not a new
151        // level. The terminal type also keeps same-named tables/routines separate.
152        CanonKey key = sqlEnv.getIdentifierService().canonKeyOf(name, type, form);
153        return new QualifiedCanonKey(Collections.singletonList(key), type);
154    }
155
156//    public boolean addTable(TSQLTable sqlTable){
157//        return addDataObject(sqlTable);
158//    }
159//
160//    public boolean addProcedure(TSQLProcedure sqlProcedure){
161//        return addDataObject(sqlProcedure);
162//    }
163//
164//    public boolean addFunction(TSQLFunction sqlFunction){
165//        return addDataObject(sqlFunction);
166//    }
167//
168//    public boolean addTrigger(TSQLTrigger sqlTrigger){
169//        return addDataObject(sqlTrigger);
170//    }
171
172    /**
173     * add database object
174     *
175     * <p>One index, one key function. The type-specific maps and the SQL Server bucketed
176     * index that used to be written alongside this one are gone: they keyed themselves
177     * independently of the environment's {@link IdentifierProfile}, so they could only
178     * agree with it by coincidence.
179     *
180     * @param schemaObject schema object to add
181     */
182    protected void addSchemaObject(TSQLSchemaObject schemaObject){
183        // same monitor as createSchemaObject: registration of one object must be
184        // atomic across all indexes (reentrant when called from createSchemaObject
185        // via the schema-object constructors).
186        // Note: env.putSchemaObject (and thus the catalog provider) runs while this
187        // monitor is held — in-tree providers never call back into schema/env
188        // creation; a custom ICatalogProvider that does could deadlock.
189        synchronized (schemaObjectMap) {
190            checkNotSealed();  // Phase 6: checked under the mutation lock (seal() itself takes no monitor, so this narrows, not closes, the seal race)
191            // getName() is the stored spelling this object was registered under, so it
192            // enters as CATALOG_STORED; findSchemaObject takes SQL_LEXICAL because its
193            // argument is a spelling out of a statement. Encoding both ends through the
194            // same service is what keeps the two forms from folding differently.
195            schemaObjectMap.put(schemaKey(schemaObject.getName(), schemaObject.getDataObjectType(),
196                    IdentifierInputForm.CATALOG_STORED), schemaObject);
197            this.catalog.getSqlEnv().putSchemaObject(schemaObject.getQualifiedName(), schemaObject);
198        }
199    }
200
201    TSQLSchemaObject findSchemaObject(ESQLDataObjectType dataObjectType, String schemaObjectName) {
202        // No fallback after a canonical miss. The old key folded more aggressively
203        // than several vendors' real rules, so a retry under it would turn Oracle
204        // "Orders" or Snowflake "Orders" into ORDERS and hand back a DIFFERENT
205        // object while reporting a hit.
206        TSQLSchemaObject result = schemaObjectMap.get(schemaKey(schemaObjectName, dataObjectType,
207                IdentifierInputForm.SQL_LEXICAL));
208        return result;
209    }
210
211
212    /**
213     * create a oracle package belong to this schema
214     *
215     * @param oraclePackageName oracle package name
216     * @return an instance of the oracle package
217     */
218    public TSQLOraclePackage createOraclePackage(String oraclePackageName){
219        return (TSQLOraclePackage)createSchemaObject(oraclePackageName,ESQLDataObjectType.dotOraclePackage);
220    }
221
222    /**
223     * create a procedure belong to this schema
224     *
225     * @param procedureName procedure name
226     * @return an instance of the SQL procedure
227     */
228    public TSQLProcedure createProcedure(String procedureName){
229        return (TSQLProcedure)createSchemaObject(procedureName,ESQLDataObjectType.dotProcedure);
230    }
231
232    public TSQLProcedure createProcedure(String procedureName, ESQLDataObjectType type){
233        return (TSQLProcedure)createSchemaObject(procedureName,type);
234    }
235
236    /**
237     * create a function belong to this schema
238     *
239     * @param functionName function name
240     * @return an instance of the SQL function.
241     */
242    public TSQLFunction createFunction(String functionName){
243        return (TSQLFunction)createSchemaObject(functionName,ESQLDataObjectType.dotFunction);
244    }
245
246    /**
247     * create a trigger belong to this schema
248     *
249     * @param triggerName trigger name
250     * @return an instance of the SQL trigger.
251     */
252    public TSQLTrigger createTrigger(String triggerName){
253        return (TSQLTrigger)createSchemaObject(triggerName,ESQLDataObjectType.dotTrigger);
254    }
255
256    /**
257     * create a synonyms belong to this schema
258     *
259     * @param synonymsName synonyms name
260     * @return an instance of the SQL synonyms
261     */
262    public TSQLSynonyms createSynonyms(String synonymsName){
263        return (TSQLSynonyms)createSchemaObject(synonymsName,ESQLDataObjectType.dotSynonyms);
264    }
265
266    /**
267     * create a synonyms belong to this schema and record its base target.
268     *
269     * <p>If a synonym with the same name already exists it is returned with its
270     * base target updated. Any of {@code sourceDatabase}/{@code sourceSchema} may
271     * be {@code null} to mean "the synonym's own database/schema"; cross-database
272     * base targets are allowed.
273     *
274     * @param synonymsName   synonyms name
275     * @param sourceDatabase base database/catalog, or {@code null}
276     * @param sourceSchema   base schema, or {@code null}
277     * @param sourceName     base object name
278     * @return an instance of the SQL synonyms
279     */
280    public TSQLSynonyms createSynonyms(String synonymsName, String sourceDatabase, String sourceSchema, String sourceName){
281        TSQLSynonyms synonyms = createSynonyms(synonymsName);
282        if (synonyms != null) {
283            synonyms.setBaseTarget(sourceDatabase, sourceSchema, sourceName);
284        }
285        return synonyms;
286    }
287
288    /**
289     * create a table belong to this schema. If table with the same name already exists in the schema
290     * return the existing table.
291     *
292     * @param tableName table name
293     * @return an instance of the SQL table
294     */
295    public TSQLTable createTable(String tableName){
296        return (TSQLTable)createSchemaObject(tableName,ESQLDataObjectType.dotTable);
297    }
298
299    public TSQLTable createTable(String tableName, int priority){
300        return (TSQLTable)createSchemaObject(tableName,ESQLDataObjectType.dotTable, priority);
301    }
302
303    // Use the same identity path as get-or-create: canonical keys for Snowflake
304    // and BigQuery, the bucketed/hierarchical/legacy chain for other vendors.
305    public boolean containsTable(String tableName) {
306        return findSchemaObject(ESQLDataObjectType.dotTable, tableName) != null;
307    }
308
309    public TSQLTable findTable(String tableName) {
310        return (TSQLTable) findSchemaObject(ESQLDataObjectType.dotTable, tableName);
311    }
312
313    public TSQLSynonyms findSynonyms(String synonymsName) {
314        return (TSQLSynonyms) findSchemaObject(ESQLDataObjectType.dotSynonyms, synonymsName);
315    }
316    
317        public TSQLSchemaObject findSchemaObject(String schemaObjectName) {
318                TSQLSchemaObject object = findSchemaObject(ESQLDataObjectType.dotFunction, schemaObjectName);
319                if (object == null) {
320                        object = findSchemaObject(ESQLDataObjectType.dotProcedure, schemaObjectName);
321                }
322                if (object == null) {
323                        object = findSchemaObject(ESQLDataObjectType.dotTrigger, schemaObjectName);
324                }
325                if (object == null) {
326                        object = findSchemaObject(ESQLDataObjectType.dotOraclePackage, schemaObjectName);
327                }
328                if (object == null) {
329                        object = findSchemaObject(ESQLDataObjectType.dotSynonyms, schemaObjectName);
330                }
331                return object;
332        }
333
334    /**
335     *
336     * @param schemaObjectName 该名称不带 catalog, schema 前缀
337     * @param dataObjectType
338     * @return
339     */
340    public TSQLSchemaObject createSchemaObject(String schemaObjectName, ESQLDataObjectType dataObjectType){
341        return createSchemaObject(schemaObjectName, dataObjectType, 0);
342    }
343
344    /**
345     *
346     * @param schemaObjectName 该名称不带 catalog, schema 前缀
347     * @param dataObjectType
348     * @param priority
349     * @return
350     */
351    protected TSQLSchemaObject createSchemaObject(String schemaObjectName, ESQLDataObjectType dataObjectType, int priority){
352        TSQLSchemaObject result = null;
353        // synchronized so concurrent get-or-create of the same object can't produce
354        // duplicate twins (columns added to the losing twin would be invisible).
355        // The schema-object constructors re-enter this monitor via addSchemaObject —
356        // reentrant, keep both on the same lock or the race comes back.
357        synchronized (schemaObjectMap) {
358            TSQLSchemaObject schemaObject = findSchemaObject(dataObjectType, schemaObjectName);
359            if (schemaObject == null){
360                switch (dataObjectType){
361                    case dotTable:
362                        result = new TSQLTable(this,schemaObjectName);
363                        break;
364                    case dotOraclePackage:
365                        result = new TSQLOraclePackage(this,schemaObjectName);
366                        break;
367                    case dotProcedure:
368                        result = new TSQLProcedure(this,schemaObjectName);
369                        break;
370                    case dotFunction:
371                        result = new TSQLFunction(this,schemaObjectName);
372                        break;
373                    case dotTrigger:
374                        result = new TSQLTrigger(this,schemaObjectName);
375                        break;
376                    case dotSynonyms:
377                        result = new TSQLSynonyms(this,schemaObjectName);
378                        break;
379                }
380            }else if (dataObjectType == schemaObject.getDataObjectType()){
381                result = schemaObject;
382            }else{
383                System.out.println("object name conflict:"+getQualifiedName()+"."+schemaObjectName+",type:"+dataObjectType+" VS "+schemaObject.getQualifiedName()+", type:"+schemaObject.getDataObjectType());
384            };
385
386            if (result!=null && priority > result.getPriority()) {
387                result.setPriority(priority);
388            }
389        }
390        return result;
391    }
392
393}