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