001package gudusoft.gsqlparser.sqlenv; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TBaseType; 005import gudusoft.gsqlparser.sqlenv.IdentifierRules.CaseCompare; 006import gudusoft.gsqlparser.sqlenv.IdentifierRules.CaseFold; 007import gudusoft.gsqlparser.util.SQLUtil; 008 009import java.text.Collator; 010import java.util.ArrayList; 011import java.util.EnumMap; 012import java.util.List; 013import java.util.Map; 014 015/** 016 * 标识符服务(Identifier Service) 017 * 018 * <p>提供所有标识符规范化和比较的统一入口。</p> 019 * 020 * <p><strong>为什么需要它(面向入门者):</strong><br> 021 * 不同数据库对名称的大小写和引号行为差别很大(是否区分大小写、是否折叠为大/小写、是否受 collation 影响)。 022 * 如果在业务代码里到处手写 {@code toLowerCase/equalsIgnoreCase},很容易出错且难以维护。 023 * 本服务将“如何折叠与如何比较”的规则集中在一个地方,确保全局一致、可扩展、易测试。</p> 024 * 025 * <p><strong>与其它类的关系:</strong> 026 * <ul> 027 * <li>{@link IdentifierRules}:一张“规则卡片”(策略),描述未引号/带引号的折叠(fold)与比较(compare)。</li> 028 * <li>{@link IdentifierProfile}:一个“厂商档案”,为某个数据库厂商打包不同对象组(表/列/函数)的规则,并携带厂商开关(如 MySQL lower_case_table_names、SQL Server collation)。</li> 029 * <li>IdentifierService:基于 Profile 执行规范化与比较,向外提供统一的 {@code normalize/areEqual/keyForMap} 接口(门面)。</li> 030 * </ul> 031 * 032 * <p><strong>设计理念与收益:</strong> 033 * <ul> 034 * <li>一致性:所有地方都通过同一入口生成 Map 键与做比较,避免行为分裂。</li> 035 * <li>可扩展:新增厂商仅需补一份规则工厂;旧调用不变。</li> 036 * <li>可测试:规则与服务可单测覆盖,验证各厂商/对象类型是否符合预期。</li> 037 * <li>性能友好:{@link #keyForMap} 先做规范化,Map 查找 O(1);SQL Server 经 {@link CollatorProvider} 做 collation 比较。</li> 038 * </ul> 039 * 040 * <p><strong>使用了哪些设计模式:</strong> 041 * <ul> 042 * <li>策略(Strategy):{@link IdentifierRules} 即为可替换的规则策略。</li> 043 * <li>工厂(Factory):{@code IdentifierRules.forOracle()/forPostgreSQL()}、{@code IdentifierProfile.forVendor(...)} 产出预设策略组合。</li> 044 * <li>门面(Facade):本类用少量方法对外隐藏折叠/比较/引号/Collator 细节。</li> 045 * <li>依赖注入(DI):构造时注入 {@link IdentifierProfile} 与可选 {@link CollatorProvider},便于替换与测试。</li> 046 * <li>不可变值对象(Immutable):规则/flags 不可变,线程安全、可缓存。</li> 047 * <li>Provider:{@link CollatorProvider} 解耦 SQL Server 的 collation 依赖。</li> 048 * </ul> 049 * 050 * <p><strong>关键约束(必须遵守):</strong> 051 * <ul> 052 * <li>所有索引键构造必须通过 {@link #keyForMap}</li> 053 * <li>所有标识符比较必须通过 {@link #areEqual}</li> 054 * <li>禁止在业务代码中直接调用 {@code String.toUpperCase/toLowerCase/equalsIgnoreCase}</li> 055 * </ul> 056 * 057 * <p><strong>使用示例:</strong> 058 * <pre> 059 * IdentifierService service = new IdentifierService(profile, collatorProvider); 060 * 061 * // 规范化标识符(用于索引键) 062 * String key = service.normalize("MyTable", ESQLDataObjectType.dotTable); 063 * // Oracle: "MYTABLE", PostgreSQL: "mytable", ClickHouse: "MyTable" 064 * 065 * // 比较两个标识符 066 * boolean eq = service.areEqual("MyTable", "MYTABLE", ESQLDataObjectType.dotTable); 067 * // Oracle: true, ClickHouse: false 068 * 069 * // 构造索引键 070 * String mapKey = service.keyForMap("MyTable", ESQLDataObjectType.dotTable); 071 * </pre> 072 * 073 * @since 3.1.0.9 074 */ 075public class IdentifierService { 076 077 // ===== Static Cache for High-Performance Normalization ===== 078 079 /** 080 * Static cache: one IdentifierService instance per database vendor 081 * 082 * <p>Pre-populated at class loading time for maximum performance. 083 * EnumMap provides O(1) lookup with minimal memory overhead.</p> 084 * 085 * <p>Thread-safe: all cached instances are immutable.</p> 086 */ 087 private static final Map<EDbVendor, IdentifierService> VENDOR_CACHE = new EnumMap<>(EDbVendor.class); 088 089 static { 090 // Pre-populate cache for all vendors at startup 091 for (EDbVendor vendor : EDbVendor.values()) { 092 IdentifierProfile profile = IdentifierProfile.forVendor( 093 vendor, 094 IdentifierProfile.VendorFlags.defaults() 095 ); 096 CollatorProvider collatorProvider = (vendor == EDbVendor.dbvmssql || vendor == EDbVendor.dbvazuresql) 097 ? new CollatorProvider() 098 : null; 099 VENDOR_CACHE.put(vendor, new IdentifierService(profile, collatorProvider)); 100 } 101 } 102 103 /** 104 * High-performance static normalize method with caching 105 * 106 * <p>This method provides a convenient static interface for identifier normalization 107 * while leveraging a pre-populated cache of IdentifierService instances.</p> 108 * 109 * <p><strong>Performance characteristics:</strong> 110 * <ul> 111 * <li>O(1) cache lookup using EnumMap 112 * <li>No object creation - services are pre-created and reused 113 * <li>Thread-safe - all cached instances are immutable 114 * <li>Handles all vendor-specific rules (collation, per-object-type rules, etc.) 115 * </ul> 116 * 117 * <p><strong>Usage example:</strong> 118 * <pre> 119 * String normalized = IdentifierService.normalizeStatic( 120 * EDbVendor.dbvoracle, 121 * ESQLDataObjectType.dotTable, 122 * "MyTable" 123 * ); 124 * // Result: "MYTABLE" 125 * </pre> 126 * 127 * @param dbVendor database vendor 128 * @param objectType object type (table, column, schema, etc.) 129 * @param identifier identifier to normalize (may be quoted) 130 * @return normalized identifier (unquoted and case-folded) 131 */ 132 public static String normalizeStatic(EDbVendor dbVendor, ESQLDataObjectType objectType, String identifier) { 133 if (identifier == null || identifier.isEmpty()) { 134 return identifier; 135 } 136 137 // Get cached instance (O(1) lookup in EnumMap) 138 IdentifierService service = VENDOR_CACHE.get(dbVendor); 139 if (service == null) { 140 // Fallback for unexpected null (should never happen with pre-populated cache) 141 // Create on-demand instance 142 IdentifierProfile profile = IdentifierProfile.forVendor(dbVendor, IdentifierProfile.VendorFlags.defaults()); 143 CollatorProvider collatorProvider = (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) 144 ? new CollatorProvider() 145 : null; 146 service = new IdentifierService(profile, collatorProvider); 147 } 148 149 return service.normalize(identifier, objectType); 150 } 151 152 /** 153 * High-performance static identifier comparison method with caching 154 * 155 * <p>This method provides a convenient static interface for identifier comparison 156 * while leveraging a pre-populated cache of IdentifierService instances.</p> 157 * 158 * <p><strong>Performance characteristics:</strong> 159 * <ul> 160 * <li>O(1) cache lookup using EnumMap 161 * <li>No object creation - services are pre-created and reused 162 * <li>Thread-safe - all cached instances are immutable 163 * <li>Handles all vendor-specific rules (case-sensitive, case-insensitive, collation-based) 164 * </ul> 165 * 166 * <p><strong>Usage example:</strong> 167 * <pre> 168 * boolean equal = IdentifierService.areEqualStatic( 169 * EDbVendor.dbvoracle, 170 * ESQLDataObjectType.dotTable, 171 * "MyTable", 172 * "MYTABLE" 173 * ); 174 * // Result: true (Oracle is case-insensitive for unquoted identifiers) 175 * </pre> 176 * 177 * @param dbVendor database vendor 178 * @param objectType object type (table, column, schema, etc.) 179 * @param ident1 first identifier to compare (may be quoted) 180 * @param ident2 second identifier to compare (may be quoted) 181 * @return true if identifiers are equal according to vendor rules 182 */ 183 public static boolean areEqualStatic(EDbVendor dbVendor, ESQLDataObjectType objectType, String ident1, String ident2) { 184 // Handle null cases 185 if (ident1 == null || ident2 == null) { 186 return ident1 == ident2; 187 } 188 189 // Fast path: reference equality 190 if (ident1 == ident2) { 191 return true; 192 } 193 194 // Get cached instance (O(1) lookup in EnumMap) 195 IdentifierService service = VENDOR_CACHE.get(dbVendor); 196 if (service == null) { 197 // Fallback for unexpected null (should never happen with pre-populated cache) 198 // Create on-demand instance 199 IdentifierProfile profile = IdentifierProfile.forVendor(dbVendor, IdentifierProfile.VendorFlags.defaults()); 200 CollatorProvider collatorProvider = (dbVendor == EDbVendor.dbvmssql || dbVendor == EDbVendor.dbvazuresql) 201 ? new CollatorProvider() 202 : null; 203 service = new IdentifierService(profile, collatorProvider); 204 } 205 206 return service.areEqual(ident1, ident2, objectType); 207 } 208 209 // ===== Instance Fields ===== 210 211 private final IdentifierProfile profile; 212 private final CollatorProvider collatorProvider; // SQL Server 专用(可选) 213 214 // ===== 构造函数 ===== 215 216 /** 217 * 构造标识符服务 218 * 219 * @param profile 厂商标识符配置档案 220 * @param collatorProvider Collator 提供者(SQL Server 专用,可为 null) 221 */ 222 public IdentifierService(IdentifierProfile profile, CollatorProvider collatorProvider) { 223 this.profile = profile; 224 this.collatorProvider = collatorProvider; 225 } 226 227 // ===== 规范化(用于索引键构造) ===== 228 229 /** 230 * 规范化标识符(去引号 + 大小写折叠) 231 * 232 * <p>用于构造索引键,确保相同语义的标识符生成相同的键。 233 * 234 * @param identifier 原始标识符(可能带引号) 235 * @param objectType 对象类型 236 * @return 规范化后的标识符 237 */ 238 public String normalize(String identifier, ESQLDataObjectType objectType) { 239 if (identifier == null || identifier.isEmpty()) { 240 return identifier; 241 } 242 243 IdentifierRules rules = profile.getRules(objectType); 244 boolean isQuoted = isQuoted(identifier); 245 246 // 1. 去引号 247 String unquoted = isQuoted ? removeQuotes(identifier) : identifier; 248 249 // 2. 大小写折叠 250 CaseFold fold = isQuoted ? rules.quotedFold : rules.unquotedFold; 251 return applyFold(unquoted, fold); 252 } 253 254 /** 255 * 应用大小写折叠 256 */ 257 private String applyFold(String str, CaseFold fold) { 258 switch (fold) { 259 case UPPER: 260 return str.toUpperCase(); 261 case LOWER: 262 return str.toLowerCase(); 263 case NONE: 264 default: 265 return str; 266 } 267 } 268 269 // ===== 比较(用于查找匹配) ===== 270 271 /** 272 * 比较两个标识符是否相等 273 * 274 * <p>根据数据库厂商的大小写规则进行比较。 275 * 276 * @param ident1 标识符 1 277 * @param ident2 标识符 2 278 * @param objectType 对象类型 279 * @return true 如果相等 280 */ 281 public boolean areEqual(String ident1, String ident2, ESQLDataObjectType objectType) { 282 if (ident1 == null || ident2 == null) { 283 return ident1 == ident2; 284 } 285 286 // 快速路径:引用相同 287 if (ident1 == ident2) { 288 return true; 289 } 290 291 IdentifierRules rules = profile.getRules(objectType); 292 293 boolean quoted1 = isQuoted(ident1); 294 boolean quoted2 = isQuoted(ident2); 295 296 String unquoted1 = quoted1 ? removeQuotes(ident1) : ident1; 297 String unquoted2 = quoted2 ? removeQuotes(ident2) : ident2; 298 299 // 确定比较规则(优先使用 quoted 规则) 300 CaseCompare compare; 301 if (quoted1 || quoted2) { 302 compare = rules.quotedCompare; 303 // 处理 SAME_AS_UNQUOTED(Presto, Vertica) 304 if (compare == CaseCompare.SAME_AS_UNQUOTED) { 305 compare = rules.unquotedCompare; 306 } 307 } else { 308 compare = rules.unquotedCompare; 309 } 310 311 // 执行比较 312 return compareStrings(unquoted1, unquoted2, compare); 313 } 314 315 /** 316 * 根据比较规则比较两个字符串 317 */ 318 private boolean compareStrings(String str1, String str2, CaseCompare compare) { 319 switch (compare) { 320 case SENSITIVE: 321 return str1.equals(str2); 322 323 case INSENSITIVE: 324 return str1.equalsIgnoreCase(str2); 325 326 case COLLATION_BASED: 327 // SQL Server: 使用 Collator 比较(ThreadLocal 缓存) 328 if (collatorProvider == null) { 329 // 回退:使用不敏感比较 330 return str1.equalsIgnoreCase(str2); 331 } 332 String collation = profile.getFlags().defaultCollation; 333 Collator collator = collatorProvider.getCollator(collation); 334 return collator.compare(str1, str2) == 0; 335 336 default: 337 return false; 338 } 339 } 340 341 // ===== 索引键构造(段级,用于分层索引) ===== 342 343 /** 344 * 为 Map 索引构造键(单个标识符段) 345 * 346 * <p><strong>注意:</strong>此方法仅用于分层索引的单段键,不用于复合键。 347 * 348 * <p>对于 COLLATION_BASED(SQL Server),不做 fold,返回原始标识符 349 * (后续通过桶+Collator 比较)。 350 * 351 * @param identifier 标识符 352 * @param objectType 对象类型 353 * @return 索引键 354 */ 355 public String keyForMap(String identifier, ESQLDataObjectType objectType) { 356 if (identifier == null || identifier.isEmpty()) { 357 return identifier; 358 } 359 360 // 强制单段输入校验(Phase 0: 默认启用) 361 assertSingleSegmentOrThrow(identifier,objectType); 362 363 IdentifierRules rules = profile.getRules(objectType); 364 boolean isQuoted = isQuoted(identifier); 365 CaseCompare compare = isQuoted ? rules.quotedCompare : rules.unquotedCompare; 366 367 // 处理 SAME_AS_UNQUOTED 368 if (compare == CaseCompare.SAME_AS_UNQUOTED) { 369 compare = rules.unquotedCompare; 370 } 371 372 // 对于 COLLATION_BASED,不折叠,返回原始标识符(去引号) 373 if (compare == CaseCompare.COLLATION_BASED) { 374 // return isQuoted ? removeQuotes(identifier) : identifier; 375 identifier = isQuoted ? removeQuotes(identifier) : identifier; 376 } 377 378 // 其他情况:规范化后作为键 379 return normalize(identifier, objectType); 380 } 381 382 // ===== 复合键构造(全限定名级,可选) ===== 383 384 /** 385 * 判断是否可以使用复合键快速路径 386 * 387 * <p>条件(必须全部满足): 388 * <ol> 389 * <li>所有对象组都是 SENSITIVE 390 * <li>输入没有引号字符 391 * <li>没有 COLLATION_BASED 类型 392 * </ol> 393 * 394 * @param qualifiedName 完整限定名(如 "db.schema.table") 395 * @return true 如果可以使用复合键 396 */ 397 public boolean canUseCompositeKey(String qualifiedName) { 398 // 条件 1: 检查是否包含引号字符 399 if (containsQuoteChar(qualifiedName)) { 400 return false; 401 } 402 403 // 条件 2: 检查所有对象组是否全部 SENSITIVE 404 for (IdentifierProfile.ObjectGroup group : IdentifierProfile.ObjectGroup.values()) { 405 IdentifierRules rules = profile.getRulesByGroup(group); 406 if (rules.unquotedCompare != CaseCompare.SENSITIVE) { 407 return false; 408 } 409 } 410 411 return true; 412 } 413 414 /** 415 * 构造复合键(使用长度前缀编码避免冲突) 416 * 417 * <p>格式: {@code len1#segment1|len2#segment2|len3#segment3|objectType} 418 * 419 * <p>例如: {@code "3#db1|6#schema|5#table|dotTable"} 420 * 421 * <p><strong>优势:</strong>避免分隔符冲突(标识符可能包含 '.' 或 '|') 422 * 423 * @param segments 标识符段列表 424 * @param objectType 对象类型 425 * @return 复合键 426 */ 427 public String buildCompositeKey(List<String> segments, ESQLDataObjectType objectType) { 428 StringBuilder sb = new StringBuilder(segments.size() * 20); 429 for (int i = 0; i < segments.size(); i++) { 430 String seg = segments.get(i); 431 if (i > 0) { 432 sb.append('|'); 433 } 434 sb.append(seg.length()).append('#').append(seg); 435 } 436 sb.append('|').append(objectType.name()); 437 return sb.toString(); 438 } 439 440 /** 441 * 构造复合键(从完整限定名) 442 * 443 * @param qualifiedName 完整限定名(如 "db.schema.table") 444 * @param objectType 对象类型 445 * @return 复合键 446 */ 447 public String buildCompositeKey(String qualifiedName, ESQLDataObjectType objectType) { 448 List<String> segments = SQLUtil.parseNames(qualifiedName); 449 return buildCompositeKey(segments, objectType); 450 } 451 452 // ===== 完整限定名处理(多段处理) ===== 453 454 /** 455 * 解析完整限定名为段列表 456 * 457 * <p>包装 {@link SQLUtil#parseNames(String, EDbVendor)} 以支持厂商特定解析: 458 * <ul> 459 * <li>MSSQL: 保留 ".." 以便后续展开 460 * <li>BigQuery: 反引号内的点号仍视为层级分隔 461 * <li>其它: 按 '.' 分段,处理引号包裹的段 462 * </ul> 463 * 464 * @param qualifiedName 完整限定名(如 "db.schema.table" 或 "db..table") 465 * @return 段列表 466 */ 467 public List<String> parseQualifiedName(String qualifiedName) { 468 return SQLUtil.parseNames(qualifiedName, profile.getVendor()); 469 } 470 471 /** 472 * 厂商级预处理(展开特殊语法) 473 * 474 * <p>处理厂商特定语法: 475 * <ul> 476 * <li>MSSQL/Azure SQL: 将 "db..table" 展开为 "db.<global or dbo>.table" 477 * </ul> 478 * 479 * @param segments 原始段列表 480 * @param vendor 数据库厂商 481 * @return 展开后的段列表 482 */ 483 public List<String> expandVendorSpecific(List<String> segments, EDbVendor vendor) { 484 return expandVendorSpecific(segments, vendor, null); 485 } 486 487 /** 488 * 厂商级预处理(展开特殊语法) 489 * 490 * <p>处理厂商特定语法: 491 * <ul> 492 * <li>MSSQL/Azure SQL: 将 "db..table" 展开为 "db.<defaultSchema or dbo>.table" 493 * </ul> 494 * 495 * @param segments 原始段列表 496 * @param vendor 数据库厂商 497 * @param defaultSchema 默认 schema,如果为 null 则使用 "dbo" 498 * @return 展开后的段列表 499 */ 500 public List<String> expandVendorSpecific(List<String> segments, EDbVendor vendor, String defaultSchema) { 501 if (vendor != EDbVendor.dbvmssql && vendor != EDbVendor.dbvazuresql) { 502 return segments; 503 } 504 505 // MSSQL: 展开 ".." 为 ".<defaultSchema or dbo>." 506 // Note: We no longer use ModelBindingManager.getGlobalSchema() here because that is 507 // designed for DataFlowAnalyzer context. Using it here causes test pollution when 508 // tests fail without cleaning up the ThreadLocal state. 509 String schemaToUse = (defaultSchema != null && !defaultSchema.isEmpty()) ? defaultSchema : "dbo"; 510 511 List<String> expanded = new ArrayList<>(); 512 for (int i = 0; i < segments.size(); i++) { 513 String seg = segments.get(i); 514 if (seg.isEmpty() && i > 0 && i < segments.size() - 1) { 515 // 这是 ".." 中的空段 516 expanded.add(schemaToUse); 517 } else { 518 expanded.add(seg); 519 } 520 } 521 return expanded; 522 } 523 524 /** 525 * 规范化单个段(去引号 + 大小写折叠) 526 * 527 * <p>与 {@link #normalize(String, ESQLDataObjectType)} 类似,但语义明确为"单段"处理。 528 * 529 * @param segment 单个段标识符 530 * @param objectType 对象类型 531 * @return 规范化后的段 532 */ 533 public String normalizeSegment(String segment, ESQLDataObjectType objectType) { 534 return normalize(segment, objectType); 535 } 536 537 /** 538 * 规范化完整限定名(等价于 SQLUtil.getIdentifierNormalName) 539 * 540 * <p>处理流程: 541 * <ol> 542 * <li>解析为段列表:{@link #parseQualifiedName(String)} 543 * <li>展开厂商特定语法:{@link #expandVendorSpecific(List, EDbVendor)} 544 * <li>根据 supportCatalog/supportSchema 决定各段类型 545 * <li>逐段规范化:{@link #normalizeSegment(String, ESQLDataObjectType)} 546 * <li>重新拼接为 "catalog.schema.table.column" 格式 547 * </ol> 548 * 549 * @param qualifiedName 完整限定名 550 * @param objectType 最终对象类型(dotTable/dotColumn/dotSchema 等) 551 * @return 规范化后的完整限定名 552 */ 553 public String normalizeQualifiedName(String qualifiedName, ESQLDataObjectType objectType) { 554 if (qualifiedName == null || qualifiedName.isEmpty()) { 555 return qualifiedName; 556 } 557 558 List<String> segments = expandVendorSpecific(parseQualifiedName(qualifiedName), profile.getVendor()); 559 if (segments.isEmpty()) { 560 return qualifiedName; 561 } 562 563 boolean supportCatalog = TSQLEnv.supportCatalog(profile.getVendor()); 564 boolean supportSchema = TSQLEnv.supportSchema(profile.getVendor()); 565 566 StringBuilder builder = new StringBuilder(); 567 568 // 根据 supportCatalog/supportSchema 与 objectType 决定各段类型 569 if (supportCatalog && supportSchema) { 570 if (objectType == ESQLDataObjectType.dotColumn) { 571 if (segments.size() > 4) { 572 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 573 .append(".") 574 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotSchema)) 575 .append(".") 576 .append(normalizeSegment(segments.get(2), ESQLDataObjectType.dotTable)) 577 .append(".") 578 .append(normalizeSegment(SQLUtil.mergeSegments(segments, 3), ESQLDataObjectType.dotColumn)); 579 } else if (segments.size() == 4) { 580 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 581 .append(".") 582 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotSchema)) 583 .append(".") 584 .append(normalizeSegment(segments.get(2), ESQLDataObjectType.dotTable)) 585 .append(".") 586 .append(normalizeSegment(segments.get(3), ESQLDataObjectType.dotColumn)); 587 } else if (segments.size() == 3) { 588 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotSchema)) 589 .append(".") 590 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotTable)) 591 .append(".") 592 .append(normalizeSegment(segments.get(2), ESQLDataObjectType.dotColumn)); 593 } else if (segments.size() == 2) { 594 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotTable)) 595 .append(".") 596 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotColumn)); 597 } else if (segments.size() == 1) { 598 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotColumn)); 599 } 600 } else if (objectType == ESQLDataObjectType.dotTable 601 || objectType == ESQLDataObjectType.dotOraclePackage 602 || objectType == ESQLDataObjectType.dotFunction 603 || objectType == ESQLDataObjectType.dotProcedure 604 || objectType == ESQLDataObjectType.dotTrigger) { 605 if (segments.size() > 3) { 606 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 607 .append(".") 608 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotSchema)) 609 .append(".") 610 .append(normalizeSegment(SQLUtil.mergeSegments(segments, 2), objectType)); 611 } else if (segments.size() == 3) { 612 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 613 .append(".") 614 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotSchema)) 615 .append(".") 616 .append(normalizeSegment(segments.get(2), objectType)); 617 } else if (segments.size() == 2) { 618 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotSchema)) 619 .append(".") 620 .append(normalizeSegment(segments.get(1), objectType)); 621 } else if (segments.size() == 1) { 622 builder.append(normalizeSegment(segments.get(0), objectType)); 623 } 624 } else if (objectType == ESQLDataObjectType.dotSchema) { 625 if (segments.size() > 2) { 626 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 627 .append(".") 628 .append(normalizeSegment(SQLUtil.mergeSegments(segments, 1), objectType)); 629 } else if (segments.size() == 2) { 630 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 631 .append(".") 632 .append(normalizeSegment(segments.get(1), objectType)); 633 } else if (segments.size() == 1) { 634 builder.append(normalizeSegment(segments.get(0), objectType)); 635 } 636 } else if (objectType == ESQLDataObjectType.dotCatalog) { 637 if (segments.size() > 1) { 638 builder.append(normalizeSegment(SQLUtil.mergeSegments(segments, 0), objectType)); 639 } else if (segments.size() == 1) { 640 builder.append(normalizeSegment(segments.get(0), objectType)); 641 } 642 } 643 } else if (supportCatalog) { 644 if (objectType == ESQLDataObjectType.dotColumn) { 645 if (segments.size() > 3) { 646 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 647 .append(".") 648 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotTable)) 649 .append(".") 650 .append(normalizeSegment(SQLUtil.mergeSegments(segments, 2), ESQLDataObjectType.dotColumn)); 651 } else if (segments.size() == 3) { 652 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 653 .append(".") 654 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotTable)) 655 .append(".") 656 .append(normalizeSegment(segments.get(2), ESQLDataObjectType.dotColumn)); 657 } else if (segments.size() == 2) { 658 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotTable)) 659 .append(".") 660 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotColumn)); 661 } else if (segments.size() == 1) { 662 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotColumn)); 663 } 664 } else if (objectType == ESQLDataObjectType.dotTable 665 || objectType == ESQLDataObjectType.dotOraclePackage 666 || objectType == ESQLDataObjectType.dotFunction 667 || objectType == ESQLDataObjectType.dotProcedure 668 || objectType == ESQLDataObjectType.dotTrigger) { 669 if (segments.size() > 2) { 670 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 671 .append(".") 672 .append(normalizeSegment(SQLUtil.mergeSegments(segments, 1), objectType)); 673 } else if (segments.size() == 2) { 674 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)) 675 .append(".") 676 .append(normalizeSegment(segments.get(1), objectType)); 677 } else if (segments.size() == 1) { 678 builder.append(normalizeSegment(segments.get(0), objectType)); 679 } 680 } else if (objectType == ESQLDataObjectType.dotSchema || objectType == ESQLDataObjectType.dotCatalog) { 681 if (segments.size() > 1) { 682 builder.append(normalizeSegment(SQLUtil.mergeSegments(segments, 0), ESQLDataObjectType.dotCatalog)); 683 } else if (segments.size() == 1) { 684 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotCatalog)); 685 } 686 } 687 } else if (supportSchema) { 688 if (objectType == ESQLDataObjectType.dotColumn) { 689 if (segments.size() > 3) { 690 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotSchema)) 691 .append(".") 692 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotTable)) 693 .append(".") 694 .append(normalizeSegment(SQLUtil.mergeSegments(segments, 2), ESQLDataObjectType.dotColumn)); 695 } else if (segments.size() == 3) { 696 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotSchema)) 697 .append(".") 698 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotTable)) 699 .append(".") 700 .append(normalizeSegment(segments.get(2), ESQLDataObjectType.dotColumn)); 701 } else if (segments.size() == 2) { 702 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotTable)) 703 .append(".") 704 .append(normalizeSegment(segments.get(1), ESQLDataObjectType.dotColumn)); 705 } else if (segments.size() == 1) { 706 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotColumn)); 707 } 708 } else if (objectType == ESQLDataObjectType.dotTable 709 || objectType == ESQLDataObjectType.dotOraclePackage 710 || objectType == ESQLDataObjectType.dotFunction 711 || objectType == ESQLDataObjectType.dotProcedure 712 || objectType == ESQLDataObjectType.dotTrigger) { 713 if (segments.size() > 2) { 714 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotSchema)) 715 .append(".") 716 .append(normalizeSegment(SQLUtil.mergeSegments(segments, 1), objectType)); 717 } else if (segments.size() == 2) { 718 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotSchema)) 719 .append(".") 720 .append(normalizeSegment(segments.get(1), objectType)); 721 } else if (segments.size() == 1) { 722 builder.append(normalizeSegment(segments.get(0), objectType)); 723 } 724 } else if (objectType == ESQLDataObjectType.dotSchema || objectType == ESQLDataObjectType.dotCatalog) { 725 if (segments.size() > 1) { 726 builder.append(normalizeSegment(SQLUtil.mergeSegments(segments, 0), ESQLDataObjectType.dotSchema)); 727 } else if (segments.size() == 1) { 728 builder.append(normalizeSegment(segments.get(0), ESQLDataObjectType.dotSchema)); 729 } 730 } 731 } 732 733 return builder.toString(); 734 } 735 736 /** 737 * 生成层级索引所需的段级键列表 738 * 739 * <p>用于分层索引(hierarchical index),确保各层键由 {@link #keyForMap} 统一生成。 740 * 741 * <p><strong>注意:</strong>调用前应先调用 {@link #parseQualifiedName(String)} 和 742 * {@link #expandVendorSpecific(List, EDbVendor)} 完成解析与展开。 743 * 744 * @param qualifiedName 完整限定名 745 * @param partTypes 各段对应的对象类型列表(长度应与段数一致) 746 * @return 段级键列表 747 * @throws IllegalArgumentException 如果段数与类型数不匹配 748 */ 749 public List<String> keysForHierarchy(String qualifiedName, List<ESQLDataObjectType> partTypes) { 750 List<String> segments = expandVendorSpecific(parseQualifiedName(qualifiedName), profile.getVendor()); 751 if (segments.size() != partTypes.size()) { 752 throw new IllegalArgumentException("Segment count (" + segments.size() 753 + ") does not match type count (" + partTypes.size() + ")"); 754 } 755 756 List<String> keys = new ArrayList<>(); 757 for (int i = 0; i < segments.size(); i++) { 758 keys.add(keyForMap(segments.get(i), partTypes.get(i))); 759 } 760 return keys; 761 } 762 763 /** 764 * 断言标识符为单段,否则抛出异常 765 * 766 * <p>用于在 {@link #keyForMap} 入口处确保输入为单段标识符(不包含 '.' 分隔符)。 767 * 768 * <p>如果 {@link TBaseType#ALLOW_MULTI_SEGMENT_IN_KEY} 为 true(兼容模式), 769 * 则仅记录警告日志而不抛异常。 770 * 771 * @param identifier 待检查的标识符 772 * @throws IllegalArgumentException 如果标识符包含多段且未启用兼容模式 773 */ 774 public void assertSingleSegmentOrThrow(String identifier, ESQLDataObjectType objectType) { 775 if (identifier == null || identifier.isEmpty()) { 776 return; 777 } 778 779 List<String> segments = parseQualifiedName(identifier); 780 if (segments.size() > 1) { 781 String message = "keyForMap requires single segment, but got " + segments.size() 782 + " segments: " + identifier; 783 if (TBaseType.ALLOW_MULTI_SEGMENT_IN_KEY) { 784 // 兼容模式:仅记录警告 785 // System.err.println("[WARN] " + message); 786 // System.err.println("[WARN] return by SQLUtil.getIdentifierNormalName:" + SQLUtil.getIdentifierNormalName(this.getProfile().getVendor(), identifier, objectType)); 787 } else { 788 // 严格模式:抛出异常 789 throw new IllegalArgumentException(message); 790 } 791 } 792 } 793 794 // ===== 辅助方法 ===== 795 796 /** 797 * 判断标识符是否被引号包围 798 */ 799 private boolean isQuoted(String identifier) { 800 return TSQLEnv.isDelimitedIdentifier(profile.getVendor(), identifier); 801 } 802 803 /** 804 * 移除标识符的引号 805 */ 806 private String removeQuotes(String identifier) { 807 return TBaseType.getTextWithoutQuoted(identifier); 808 } 809 810 /** 811 * 检查字符串是否包含引号字符 812 * 813 * <p>检查常见引号字符:", ', `, [, ] 814 */ 815 private boolean containsQuoteChar(String str) { 816 for (int i = 0; i < str.length(); i++) { 817 char c = str.charAt(i); 818 if (c == '"' || c == '\'' || c == '`' || c == '[' || c == ']') { 819 return true; 820 } 821 } 822 return false; 823 } 824 825 // ===== Getter 方法 ===== 826 827 /** 828 * 获取标识符配置档案 829 * 830 * @return 配置档案 831 */ 832 public IdentifierProfile getProfile() { 833 return profile; 834 } 835 836 /** 837 * 获取 Collator 提供者 838 * 839 * @return Collator 提供者(可能为 null) 840 */ 841 public CollatorProvider getCollatorProvider() { 842 return collatorProvider; 843 } 844}