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}