001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to you under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package gudusoft.gsqlparser.sqlenv; 018 019import gudusoft.gsqlparser.EDbVendor; 020 021import java.util.*; 022 023/** 024 * Unified storage abstraction for catalog objects using canonical keys. 025 * 026 * <p>This class replaces GSP's legacy triple-index architecture 027 * (schemaObjectList + objectIndex + tablesByName) with a clean, type-aware 028 * storage system backed by LinkedHashMaps for O(1) lookups.</p> 029 * 030 * <p>Key improvements over legacy storage:</p> 031 * <ul> 032 * <li>Canonical keys eliminate string suffix hacks ($function, $procedure, etc.)</li> 033 * <li>Per-type indexes for efficient type-specific lookups</li> 034 * <li>Vendor-aware case sensitivity via {@link NameService}</li> 035 * <li>Insertion order preservation for compatibility</li> 036 * <li>Full and partial name resolution support</li> 037 * </ul> 038 * 039 * <p>Thread safety: This class is NOT thread-safe. External synchronization 040 * is required if accessed from multiple threads.</p> 041 * 042 * @since 3.2.0.3 (Phase 1) 043 */ 044public class CatalogStore { 045 046 private final EDbVendor vendor; 047 private final NameService nameService; 048 private final IdentifierService identifierService; 049 050 // Primary storage: canonical key -> object 051 private final Map<QualifiedName, TSQLSchemaObject> objectIndex = new LinkedHashMap<>(); 052 053 // Per-type indexes for efficient type-specific lookups 054 private final Map<QualifiedName, TSQLSchemaObject> tableIndex = new LinkedHashMap<>(); 055 private final Map<QualifiedName, TSQLSchemaObject> functionIndex = new LinkedHashMap<>(); 056 private final Map<QualifiedName, TSQLSchemaObject> procedureIndex = new LinkedHashMap<>(); 057 private final Map<QualifiedName, TSQLSchemaObject> packageIndex = new LinkedHashMap<>(); 058 private final Map<QualifiedName, TSQLSchemaObject> triggerIndex = new LinkedHashMap<>(); 059 private final Map<QualifiedName, TSQLSchemaObject> columnIndex = new LinkedHashMap<>(); 060 061 /** 062 * 名称级(不含 catalog/schema)二级索引,用于“未给出完整路径”的部分查找。 063 * 064 * <p><b>为什么 value 是 {@code List<TSQLSchemaObject>}:</b></p> 065 * <ul> 066 * <li>同名对象可存在于不同 catalog/schema 下:如 {@code db1.dbo.orders} 067 * 与 {@code db1.sales.orders}。</li> 068 * <li>不同类型也可能同名(表/视图/函数等);可在 069 * {@link #getByName(String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)} 070 * 中按类型再筛。</li> 071 * <li>需要先收集全部候选,再依据默认 schema/search_path/上下文判定或报歧义。</li> 072 * </ul> 073 * 074 * <p><b>典型场景:</b></p> 075 * <pre> 076 * -- 1) MSSQL:默认 schema 解析 077 * USE db1; 078 * SELECT * FROM orders; 079 * -- 候选:db1.dbo.orders、db1.sales.orders 080 * -- 解析器按默认 schema(如 dbo)或搜索路径挑选命中 081 * 082 * -- 2) BigQuery:默认 dataset 083 * -- 当前上下文: project.dataset = p.ds 084 * SELECT * FROM `users`; 085 * -- 候选:p.ds.users、p.other_ds.users(若可见) 086 * 087 * -- 3) 歧义诊断 088 * SELECT * FROM orders; 089 * -- 多个候选且规则不能唯一确定时,抛出歧义错误或给出建议 090 * </pre> 091 * 092 * <p><b>备注:</b></p> 093 * <ul> 094 * <li>键为经 {@link IdentifierService#normalize(String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)} 095 * 规范化后的简单名。</li> 096 * <li>不用于“按 catalog/schema 分层列举”,此类需求请使用类型索引(如 {@code tableIndex})并基于 097 * {@link QualifiedName#getCatalog()} / {@link QualifiedName#getSchema()} 过滤。</li> 098 * </ul> 099 */ 100 private final Map<String, List<TSQLSchemaObject>> objectsByName = new LinkedHashMap<>(); 101 102 /** 103 * Creates a new CatalogStore for the specified database vendor. 104 * 105 * @param vendor the database vendor 106 * @deprecated Use {@link #CatalogStore(EDbVendor, IdentifierService)} instead 107 */ 108 @Deprecated 109 public CatalogStore(EDbVendor vendor) { 110 if (vendor == null) { 111 throw new IllegalArgumentException("vendor cannot be null"); 112 } 113 this.vendor = vendor; 114 this.nameService = new NameService(vendor); 115 this.identifierService = null; // Will fall back to TSQLEnv.normalizeIdentifier 116 } 117 118 /** 119 * Creates a new CatalogStore for the specified database vendor with IdentifierService. 120 * 121 * <p>This is the preferred constructor that uses IdentifierService for proper 122 * per-object-type identifier normalization.</p> 123 * 124 * @param vendor the database vendor 125 * @param identifierService the identifier service for normalization 126 * @since 3.2.0.3 (Phase 3.5) 127 */ 128 public CatalogStore(EDbVendor vendor, IdentifierService identifierService) { 129 if (vendor == null) { 130 throw new IllegalArgumentException("vendor cannot be null"); 131 } 132 if (identifierService == null) { 133 throw new IllegalArgumentException("identifierService cannot be null"); 134 } 135 this.vendor = vendor; 136 this.nameService = new NameService(vendor); 137 this.identifierService = identifierService; 138 } 139 140 /** 141 * Returns the database vendor associated with this store. 142 * 143 * @return the database vendor 144 */ 145 public EDbVendor getVendor() { 146 return vendor; 147 } 148 149 /** 150 * Returns the name service used by this store. 151 * 152 * @return the name service 153 */ 154 public NameService getNameService() { 155 return nameService; 156 } 157 158 /** 159 * Adds or updates an object in the store. 160 * 161 * <p>The object is indexed by its fully-qualified canonical name and also 162 * added to type-specific and name-only indexes for efficient lookups.</p> 163 * 164 * @param object the schema object to add 165 * @return the previous object with the same key, or null if none existed 166 */ 167 public TSQLSchemaObject put(TSQLSchemaObject object) { 168 if (object == null) { 169 throw new IllegalArgumentException("object cannot be null"); 170 } 171 172 // Create canonical key 173 QualifiedName key = createKey(object); 174 175 // Add to primary index 176 TSQLSchemaObject previous = objectIndex.put(key, object); 177 178 // Add to type-specific index 179 Map<QualifiedName, TSQLSchemaObject> typeIndex = getTypeIndex(object.getDataObjectType()); 180 if (typeIndex != null) { 181 typeIndex.put(key, object); 182 } 183 184 // Add to name-only index 185 String normalizedName = normalizeObjectName(object); 186 objectsByName.computeIfAbsent(normalizedName, k -> new ArrayList<>()).add(object); 187 188 return previous; 189 } 190 191 /** 192 * Finds an object by its fully-qualified name and type. 193 * 194 * <p>This is an O(1) lookup using the canonical key index.</p> 195 * 196 * @param catalog the catalog name (may be null) 197 * @param schema the schema name (may be null) 198 * @param objectName the object name (required) 199 * @param type the object type 200 * @return the matching object, or null if not found 201 */ 202 public TSQLSchemaObject get(String catalog, String schema, String objectName, ESQLDataObjectType type) { 203 if (objectName == null || type == null) { 204 return null; 205 } 206 207 // Normalize components to match stored keys 208 String normalizedCatalog = normalizeComponent(catalog, type); 209 String normalizedSchema = normalizeComponent(schema, type); 210 String normalizedObject = normalizeComponent(objectName, type); 211 212 QualifiedName key = new QualifiedName(normalizedCatalog, normalizedSchema, normalizedObject, type); 213 return objectIndex.get(key); 214 } 215 216 /** 217 * Finds objects by name only, regardless of catalog/schema. 218 * 219 * <p>This is used for partial name resolution when the full path is not specified.</p> 220 * 221 * @param objectName the object name 222 * @param type the object type (optional - null returns all types) 223 * @return list of matching objects (may be empty, never null) 224 */ 225 public List<TSQLSchemaObject> getByName(String objectName, ESQLDataObjectType type) { 226 if (objectName == null) { 227 return Collections.emptyList(); 228 } 229 230// if (type == null){ 231// type = ESQLDataObjectType.dotTable; // Default to table type if none specified 232// } 233 234 // Use IdentifierService if available, otherwise fall back to legacy method 235 String normalizedName; 236 if (identifierService != null) { 237 normalizedName = identifierService.normalize(objectName, type); 238 } else { 239 normalizedName = IdentifierService.normalizeStatic(vendor, type, objectName); // TSQLEnv.normalizeIdentifier(vendor, type, objectName); // 240 } 241 List<TSQLSchemaObject> candidates = objectsByName.get(normalizedName); 242 243 if (candidates == null || candidates.isEmpty()) { 244 return Collections.emptyList(); 245 } 246 247 // Filter by type if specified 248 if (type != null) { 249 List<TSQLSchemaObject> filtered = new ArrayList<>(); 250 for (TSQLSchemaObject obj : candidates) { 251 if (obj.getDataObjectType() == type) { 252 filtered.add(obj); 253 } 254 } 255 return filtered; 256 } 257 258 return new ArrayList<>(candidates); 259 } 260 261 /** 262 * Finds all objects of a specific type. 263 * 264 * @param type the object type 265 * @return collection of all objects of that type (may be empty, never null) 266 */ 267 public Collection<TSQLSchemaObject> getByType(ESQLDataObjectType type) { 268 if (type == null) { 269 return Collections.emptyList(); 270 } 271 272 Map<QualifiedName, TSQLSchemaObject> typeIndex = getTypeIndex(type); 273 if (typeIndex == null) { 274 return Collections.emptyList(); 275 } 276 277 return new ArrayList<>(typeIndex.values()); 278 } 279 280 /** 281 * Removes an object from the store. 282 * 283 * @param object the object to remove 284 * @return true if the object was removed, false if it was not present 285 */ 286 public boolean remove(TSQLSchemaObject object) { 287 if (object == null) { 288 return false; 289 } 290 291 QualifiedName key = createKey(object); 292 293 // Remove from primary index 294 TSQLSchemaObject removed = objectIndex.remove(key); 295 296 if (removed != null) { 297 // Remove from type-specific index 298 Map<QualifiedName, TSQLSchemaObject> typeIndex = getTypeIndex(object.getDataObjectType()); 299 if (typeIndex != null) { 300 typeIndex.remove(key); 301 } 302 303 // Remove from name-only index 304 String normalizedName = normalizeObjectName(object); 305 List<TSQLSchemaObject> list = objectsByName.get(normalizedName); 306 if (list != null) { 307 list.remove(object); 308 if (list.isEmpty()) { 309 objectsByName.remove(normalizedName); 310 } 311 } 312 313 return true; 314 } 315 316 return false; 317 } 318 319 /** 320 * Returns the total number of objects in the store. 321 * 322 * @return the total count 323 */ 324 public int size() { 325 return objectIndex.size(); 326 } 327 328 /** 329 * Removes all objects from the store. 330 */ 331 public void clear() { 332 objectIndex.clear(); 333 tableIndex.clear(); 334 functionIndex.clear(); 335 procedureIndex.clear(); 336 packageIndex.clear(); 337 triggerIndex.clear(); 338 columnIndex.clear(); 339 objectsByName.clear(); 340 } 341 342 /** 343 * Returns all objects in insertion order. 344 * 345 * @return collection of all objects (may be empty, never null) 346 */ 347 public Collection<TSQLSchemaObject> getAll() { 348 return new ArrayList<>(objectIndex.values()); 349 } 350 351 /** 352 * Creates a canonical key for an object. 353 * 354 * <p>The key is normalized according to vendor-specific rules.</p> 355 */ 356 private QualifiedName createKey(TSQLSchemaObject object) { 357 String catalog = object.getSchema() != null && object.getSchema().getCatalog() != null 358 ? normalizeComponent(object.getSchema().getCatalog().getName(), object.getDataObjectType()) 359 : null; 360 String schema = object.getSchema() != null 361 ? normalizeComponent(object.getSchema().getName(), object.getDataObjectType()) 362 : null; 363 String name = normalizeComponent(object.getName(), object.getDataObjectType()); 364 365 return new QualifiedName(catalog, schema, name, object.getDataObjectType()); 366 } 367 368 /** 369 * Normalizes a name component (catalog/schema/object). 370 */ 371 private String normalizeComponent(String component, ESQLDataObjectType type) { 372 if (component == null || component.isEmpty()) { 373 return null; 374 } 375 // Use IdentifierService if available, otherwise fall back to legacy method 376 if (identifierService != null) { 377 return identifierService.normalize(component, type); 378 } else { 379 return IdentifierService.normalizeStatic(vendor, type, component); 380 } 381 } 382 383 /** 384 * Normalizes an object's name for the name-only index. 385 */ 386 private String normalizeObjectName(TSQLSchemaObject object) { 387 String name = object.getName(); 388 if (name == null) { 389 name = ""; 390 } 391 // Use IdentifierService if available, otherwise fall back to legacy method 392 if (identifierService != null) { 393 return identifierService.normalize(name, object.getDataObjectType()); 394 } else { 395 return IdentifierService.normalizeStatic(vendor, object.getDataObjectType(), name); 396 } 397 } 398 399 /** 400 * Returns the type-specific index for the given object type. 401 */ 402 private Map<QualifiedName, TSQLSchemaObject> getTypeIndex(ESQLDataObjectType type) { 403 switch (type) { 404 case dotTable: 405 return tableIndex; 406 case dotFunction: 407 return functionIndex; 408 case dotProcedure: 409 return procedureIndex; 410 case dotOraclePackage: 411 return packageIndex; 412 case dotTrigger: 413 return triggerIndex; 414 case dotColumn: 415 return columnIndex; 416 default: 417 return null; 418 } 419 } 420 421 /** 422 * Immutable qualified name used as a canonical key. 423 * 424 * <p>This value object combines catalog.schema.object + type into a single 425 * key with cached hashCode for efficient map lookups.</p> 426 */ 427 public static class QualifiedName { 428 private final String catalog; 429 private final String schema; 430 private final String objectName; 431 private final ESQLDataObjectType type; 432 private final int hashCode; 433 434 public QualifiedName(String catalog, String schema, String objectName, ESQLDataObjectType type) { 435 this.catalog = catalog; 436 this.schema = schema; 437 this.objectName = objectName; 438 this.type = type; 439 this.hashCode = computeHashCode(); 440 } 441 442 public String getCatalog() { 443 return catalog; 444 } 445 446 public String getSchema() { 447 return schema; 448 } 449 450 public String getObjectName() { 451 return objectName; 452 } 453 454 public ESQLDataObjectType getType() { 455 return type; 456 } 457 458 @Override 459 public boolean equals(Object obj) { 460 if (this == obj) return true; 461 if (!(obj instanceof QualifiedName)) return false; 462 463 QualifiedName other = (QualifiedName) obj; 464 return Objects.equals(catalog, other.catalog) 465 && Objects.equals(schema, other.schema) 466 && Objects.equals(objectName, other.objectName) 467 && type == other.type; 468 } 469 470 @Override 471 public int hashCode() { 472 return hashCode; 473 } 474 475 private int computeHashCode() { 476 return Objects.hash(catalog, schema, objectName, type); 477 } 478 479 @Override 480 public String toString() { 481 StringBuilder sb = new StringBuilder(); 482 if (catalog != null) { 483 sb.append(catalog).append('.'); 484 } 485 if (schema != null) { 486 sb.append(schema).append('.'); 487 } 488 sb.append(objectName); 489 sb.append(" (").append(type).append(')'); 490 return sb.toString(); 491 } 492 } 493}