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}