001package gudusoft.gsqlparser.sqlenv;
002
003
004import gudusoft.gsqlparser.TBaseType;
005import gudusoft.gsqlparser.util.SQLUtil;
006
007import java.util.*;
008
009/**
010 * In the most sql-implementation, Catalog is the anonymous of the Database.
011 * So, use this class represents a database.
012 */
013public class TSQLCatalog extends TSQLObject {
014
015    // IdentifierService for consistent key generation (lazy initialization).
016    // volatile: concurrent lazy init may build duplicate (equivalent) instances,
017    // but must never publish a partially constructed one to another thread.
018    private volatile IdentifierService identifierService;
019
020    // ===== Phase 6: Catalog seal mechanism for thread-safety hardening =====
021    /**
022     * Seal state flag (volatile for thread visibility).
023     */
024    private volatile boolean sealed = false;
025
026    /**
027     * Seal this catalog to prevent further modifications (Phase 6).
028     *
029     * <p>See {@link TSQLEnv#seal()} for detailed documentation.
030     */
031    public void seal() {
032        this.sealed = true;
033    }
034
035    /**
036     * Check if this catalog is sealed (Phase 6).
037     *
038     * @return true if sealed, false otherwise
039     */
040    public boolean isSealed() {
041        return sealed;
042    }
043
044    /**
045     * Check if catalog is not sealed, throw exception if sealed and enforcement is enabled.
046     *
047     * @throws IllegalStateException if sealed and TBaseType.ENFORCE_CATALOG_SEAL is true
048     */
049    private void checkNotSealed() {
050        if (sealed && TBaseType.ENFORCE_CATALOG_SEAL) {
051            throw new IllegalStateException("Cannot modify sealed catalog. Call seal() marks catalog as read-only.");
052        }
053    }
054
055    /**
056     * Get or create IdentifierService (lazy initialization)
057     */
058    private IdentifierService getIdentifierService() {
059        if (identifierService == null) {
060            IdentifierProfile profile = IdentifierProfile.forVendor(
061                this.sqlEnv.getDBVendor(),
062                IdentifierProfile.VendorFlags.defaults()
063            );
064            identifierService = new IdentifierService(profile, null);
065        }
066        return identifierService;
067    }
068
069    /**
070     * create a catalog and add to the SQL environment.
071     *
072     * @param sqlEnv SQL environment
073     * @param catalogName catalog name
074     */
075    public TSQLCatalog(TSQLEnv sqlEnv, String catalogName){
076        super(sqlEnv,catalogName,ESQLDataObjectType.dotCatalog);
077        this.sqlEnv.doAddCatalog(this);
078    }
079
080    /**
081     * schema list in this catalog
082     *
083     * @return schema list
084     */
085    public List<TSQLSchema> getSchemaList() {
086        List<TSQLSchema> schemaList = new LinkedList<>();
087        synchronized (schemaMap) {
088            for(String schema: schemaMap.keySet()){
089                schemaList.add(schemaMap.get(schema));
090            }
091        }
092        return schemaList;
093    }
094
095    private Map<String, TSQLSchema> schemaMap = Collections.synchronizedMap(new LinkedHashMap<String, TSQLSchema>( ));
096
097    protected TSQLSchema searchSchema(String schemaName){
098        // Phase 2: Use IdentifierService when hierarchical index is enabled
099        if (TBaseType.USE_HIERARCHICAL_INDEX) {
100            try {
101                String key = getIdentifierService().keyForMap(schemaName, ESQLDataObjectType.dotSchema);
102                TSQLSchema result = schemaMap.get(key);
103                if (result != null) {
104                    if (TBaseType.LOG_INDEX_HIT_RATE) {
105                        System.out.println("[HierarchicalIndex] Catalog hit: schema - " + schemaName);
106                    }
107                    return result;
108                }
109            } catch (Throwable ignore) {
110                // Fall through to legacy path
111            }
112        }
113
114        // Legacy path: use SQLUtil normalization
115        String name = SQLUtil.getIdentifierNormalName(this.sqlEnv.getDBVendor(), schemaName, ESQLDataObjectType.dotSchema);
116        return schemaMap.get(name);
117    }
118
119    protected void addSchema(TSQLSchema sqlSchema) {
120        // same monitor as getSchema: check-then-put must be atomic under
121        // concurrent DDL registration (reentrant when called from getSchema
122        // via the TSQLSchema constructor)
123        synchronized (schemaMap) {
124            checkNotSealed();  // Phase 6: checked under the mutation lock (seal() itself takes no monitor, so this narrows, not closes, the seal race)
125            if (searchSchema(sqlSchema.name) == null) {
126                // ===== Legacy path: Update schemaMap with SQLUtil normalization =====
127                String name = SQLUtil.getIdentifierNormalName(this.sqlEnv.getDBVendor(), sqlSchema.name, ESQLDataObjectType.dotSchema);
128                schemaMap.put(name, sqlSchema);
129
130                // ===== New path: Also write with IdentifierService key =====
131                if (TBaseType.USE_HIERARCHICAL_INDEX) {
132                    try {
133                        String key = getIdentifierService().keyForMap(sqlSchema.name, ESQLDataObjectType.dotSchema);
134                        // Only write if the key is different from legacy key (avoid double-write)
135                        if (!key.equalsIgnoreCase(name)) {
136                            schemaMap.put(key, sqlSchema);
137                        }
138                    } catch (Throwable ignore) {
139                        // Silently fail to maintain backward compatibility
140                    }
141                }
142            }
143        }
144    }
145
146    /**
147     * get a schema instance from the catalog, create a new schema if this schema is not exist and the createIfNotExist parameter is true.
148     *
149     * @param schemaName schema name
150     * @param createIfNotExist create a new schema if this schema is not exist and the createIfNotExist parameter is true.
151     * @return the schema instance
152     */
153    public TSQLSchema getSchema(String schemaName, boolean createIfNotExist){
154        TSQLSchema result = searchSchema(schemaName);
155        if ((result == null)&&(createIfNotExist)){
156            // synchronized so concurrent get-or-create of the same schema can't
157            // produce duplicate twins; the TSQLSchema constructor re-enters this
158            // monitor via addSchema (reentrant)
159            synchronized (schemaMap) {
160                result = searchSchema(schemaName);
161                if (result == null){
162                    result = new TSQLSchema(this,schemaName);
163                }
164            }
165        }
166
167        return result;
168    }
169
170    /**
171     * create a schema under this catalog
172     *
173     * @param schemaName schema name
174     * @return schema instance
175     */
176    public TSQLSchema createSchema(String schemaName){
177        return getSchema(schemaName,true);
178    }
179
180}