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.&lt;global or dbo&gt;.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.&lt;defaultSchema or dbo&gt;.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}