001package gudusoft.gsqlparser.sqlenv;
002
003import gudusoft.gsqlparser.EDbVendor;
004
005import java.util.Collections;
006import java.util.EnumMap;
007import java.util.EnumSet;
008import java.util.Map;
009import java.util.Objects;
010import java.util.concurrent.ConcurrentHashMap;
011
012/**
013 * 厂商标识符配置档案(Vendor Identifier Profile)
014 *
015 * <p>封装每个数据库厂商的完整标识符配置,包括:
016 * <ul>
017 * <li>按对象组(NAME_GROUP, COLUMN_GROUP, ROUTINE_GROUP)的标识符规则
018 * <li>按对象类别(catalog/project、schema、table、column…)的独立覆盖,见
019 *     {@link Builder#withObjectRules(ESQLDataObjectType, IdentifierRules)}
020 * <li>Vendor-specific flags(如 MySQL lower_case_table_names, SQL Server collation)
021 * </li>
022 *
023 * <p>设计目标:
024 * <ul>
025 * <li>集中管理所有 vendor-specific 配置
026 * <li>统一注入路径,避免配置散落在各处
027 * <li>支持缓存失效(通过 fingerprint)
028 * </ul>
029 *
030 * <p>使用示例:
031 * <pre>
032 * // 创建 Oracle profile(使用默认 flags)
033 * IdentifierProfile oracleProfile = IdentifierProfile.forVendor(
034 *     EDbVendor.dbvoracle,
035 *     VendorFlags.defaults()
036 * );
037 *
038 * // 创建 MySQL profile(指定 lower_case_table_names)
039 * IdentifierProfile mysqlProfile = IdentifierProfile.forVendor(
040 *     EDbVendor.dbvmysql,
041 *     new VendorFlags(1, null, false, false)  // lower_case_table_names = 1
042 * );
043 *
044 * // 查询规则
045 * IdentifierRules tableRules = oracleProfile.getRules(ESQLDataObjectType.dotTable);
046 * </pre>
047 *
048 * @since 3.1.0.9
049 */
050public final class IdentifierProfile {
051
052    private final EDbVendor vendor;
053
054    // ===== 按对象组的规则 =====
055    private final EnumMap<ObjectGroup, IdentifierRules> rulesByGroup;
056
057    /**
058     * Explicit per-object-category overrides, the finest granularity
059     * ({@link Builder#withObjectRules}). Empty for a plain vendor profile.
060     *
061     * <p>Kept separately from {@link #effectiveRules} so {@link #toBuilder()} can hand the
062     * overrides back as overrides rather than as a new seed — re-seeding with
063     * {@link Builder#withFlags} must not silently freeze an override into a vendor default.
064     */
065    private final EnumMap<ESQLDataObjectType, IdentifierRules> rulesByType;
066
067    /**
068     * The groups whose rule a caller set ({@link Builder#withRules}) rather than the vendor seed.
069     * A caller's group rule outranks the vendor's own per-category rules
070     * ({@link #defaultTypeRulesFor}), so this has to be remembered to resolve a category.
071     */
072    private final EnumSet<ObjectGroup> overriddenGroups;
073
074    /**
075     * The resolved rule of EVERY object type — the profile's complete effective strategy.
076     *
077     * <p>Selection order (design §5.1): explicit object-category override &gt; group override
078     * or vendor-seeded group rule. Precomputed for two reasons: {@link #getRules} is on the
079     * per-compare hot path, and equality/fingerprint have to compare the whole strategy, not
080     * the three group slots (two profiles built by different routes — a group override versus
081     * an override on every type of that group — are the same strategy and must compare equal).
082     */
083    private final EnumMap<ESQLDataObjectType, IdentifierRules> effectiveRules;
084
085    // ===== Vendor-specific flags =====
086    private final VendorFlags flags;
087
088    // ===== 对象组定义 =====
089
090    /**
091     * 对象组(用于区分不同对象类型的标识符规则)
092     *
093     * <p>某些数据库对不同对象类型使用不同的大小写规则:
094     * <ul>
095     * <li>BigQuery: 表名敏感,列名不敏感
096     * <li>MySQL: 表名根据 lower_case_table_names,列名始终不敏感
097     * </ul>
098     */
099    public enum ObjectGroup {
100        /**
101         * 名称组(catalog, schema, table, view, procedure, trigger)
102         */
103        NAME_GROUP,
104
105        /**
106         * 列组(column)
107         */
108        COLUMN_GROUP,
109
110        /**
111         * 函数组(function)
112         * <p>某些数据库函数名规则与表名不同
113         */
114        ROUTINE_GROUP
115    }
116
117    // ===== Vendor flags 封装 =====
118
119    /**
120     * Vendor-specific flags(厂商特定配置)
121     *
122     * <p>封装各数据库厂商的特殊配置参数
123     */
124    public static class VendorFlags {
125        /**
126         * MySQL: lower_case_table_names 系统变量(0, 1, 2)
127         * <ul>
128         * <li>0: 大小写敏感(Unix/Linux)
129         * <li>1: 存储为小写,比较不敏感(Windows)
130         * <li>2: 存储保留原样,比较不敏感(macOS)
131         * </ul>
132         */
133        public final int mysqlLowerCaseTableNames;
134
135        /**
136         * SQL Server / Azure SQL: 默认 collation 名称
137         * <p>例如: "SQL_Latin1_General_CP1_CI_AS"
138         */
139        public final String defaultCollation;
140
141        /**
142         * Redshift: enable_case_sensitive_identifier 参数
143         * <p>默认 false(与 PostgreSQL 一致)
144         */
145        public final boolean redshiftEnableCaseSensitive;
146
147        /**
148         * Snowflake: QUOTED_IDENTIFIERS_IGNORE_CASE 会话参数
149         * <p>默认 false(quoted 标识符大小写敏感)
150         */
151        public final boolean snowflakeQuotedIdentifiersIgnoreCase;
152
153        /**
154         * MySQL 家族: sql_mode 中的 ANSI_QUOTES 选项
155         *
156         * <p>为 true 时,双引号 {@code "} 界定标识符(而非字符串字面量)。
157         * 只影响 {@link IdentifierCodec} 的引号识别/编解码,不影响大小写
158         * 折叠/比较规则(fold/compare 与 ANSI_QUOTES 无关)。
159         * <p>默认 false(MySQL 出厂默认,{@code "} 是字符串定界符)。
160         */
161        public final boolean mysqlAnsiQuotes;
162
163        /**
164         * 构造 vendor flags(ansiQuotes 取默认值 false)
165         *
166         * @param mysqlLowerCaseTableNames MySQL lower_case_table_names (0, 1, 2)
167         * @param defaultCollation SQL Server collation 名称
168         * @param redshiftEnableCaseSensitive Redshift case sensitive 开关
169         * @param snowflakeQuotedIdentifiersIgnoreCase Snowflake quoted ignore case 开关
170         */
171        public VendorFlags(int mysqlLowerCaseTableNames,
172                          String defaultCollation,
173                          boolean redshiftEnableCaseSensitive,
174                          boolean snowflakeQuotedIdentifiersIgnoreCase) {
175            this(mysqlLowerCaseTableNames, defaultCollation, redshiftEnableCaseSensitive,
176                 snowflakeQuotedIdentifiersIgnoreCase, false);
177        }
178
179        /**
180         * 构造 vendor flags(完整参数)
181         *
182         * @param mysqlLowerCaseTableNames MySQL lower_case_table_names (0, 1, 2)
183         * @param defaultCollation SQL Server collation 名称
184         * @param redshiftEnableCaseSensitive Redshift case sensitive 开关
185         * @param snowflakeQuotedIdentifiersIgnoreCase Snowflake quoted ignore case 开关
186         * @param mysqlAnsiQuotes MySQL 家族 sql_mode ANSI_QUOTES 开关
187         */
188        public VendorFlags(int mysqlLowerCaseTableNames,
189                          String defaultCollation,
190                          boolean redshiftEnableCaseSensitive,
191                          boolean snowflakeQuotedIdentifiersIgnoreCase,
192                          boolean mysqlAnsiQuotes) {
193            this.mysqlLowerCaseTableNames = mysqlLowerCaseTableNames;
194            this.defaultCollation = defaultCollation;
195            this.redshiftEnableCaseSensitive = redshiftEnableCaseSensitive;
196            this.snowflakeQuotedIdentifiersIgnoreCase = snowflakeQuotedIdentifiersIgnoreCase;
197            this.mysqlAnsiQuotes = mysqlAnsiQuotes;
198        }
199
200        /**
201         * 默认 flags(用于大部分数据库)
202         */
203        public static VendorFlags defaults() {
204            return new VendorFlags(
205                // MySQL lower_case_table_names = 1 (P0d.2): the unconfigured default keeps
206                // the legacy case-INSENSITIVE table equality, and mode 1's LOWER fold keeps
207                // keyForMap bucket keys coherent with that relation (mode 2 would bucket
208                // case-variants apart while areEqual equates them). Mode 0 (Linux
209                // case-sensitive) remains available through explicit configuration.
210                1,
211                "SQL_Latin1_General_CP1_CI_AS",  // SQL Server 默认 collation
212                false,                        // Redshift case sensitive = false
213                false                         // Snowflake quoted ignore case = false
214            );
215        }
216
217        @Override
218        public boolean equals(Object o) {
219            if (this == o) return true;
220            if (o == null || getClass() != o.getClass()) return false;
221            VendorFlags that = (VendorFlags) o;
222            return mysqlLowerCaseTableNames == that.mysqlLowerCaseTableNames &&
223                   redshiftEnableCaseSensitive == that.redshiftEnableCaseSensitive &&
224                   snowflakeQuotedIdentifiersIgnoreCase == that.snowflakeQuotedIdentifiersIgnoreCase &&
225                   mysqlAnsiQuotes == that.mysqlAnsiQuotes &&
226                   Objects.equals(defaultCollation, that.defaultCollation);
227        }
228
229        @Override
230        public int hashCode() {
231            return Objects.hash(mysqlLowerCaseTableNames, defaultCollation,
232                              redshiftEnableCaseSensitive, snowflakeQuotedIdentifiersIgnoreCase,
233                              mysqlAnsiQuotes);
234        }
235    }
236
237    // ===== 构造函数 =====
238
239    /**
240     * 构造标识符配置档案
241     *
242     * @param vendor 数据库厂商
243     * @param rulesByGroup 按对象组的规则
244     * @param flags vendor-specific flags
245     */
246    private IdentifierProfile(EDbVendor vendor,
247                             EnumMap<ObjectGroup, IdentifierRules> rulesByGroup,
248                             EnumSet<ObjectGroup> overriddenGroups,
249                             EnumMap<ESQLDataObjectType, IdentifierRules> rulesByType,
250                             VendorFlags flags) {
251        this.vendor = Objects.requireNonNull(vendor, "vendor");
252        this.rulesByGroup = new EnumMap<>(Objects.requireNonNull(rulesByGroup, "rulesByGroup"));
253        this.overriddenGroups = overriddenGroups == null || overriddenGroups.isEmpty()
254            ? EnumSet.noneOf(ObjectGroup.class)
255            : EnumSet.copyOf(overriddenGroups);
256        this.rulesByType = rulesByType == null || rulesByType.isEmpty()
257            ? new EnumMap<ESQLDataObjectType, IdentifierRules>(ESQLDataObjectType.class)
258            : new EnumMap<>(rulesByType);
259        this.flags = Objects.requireNonNull(flags, "flags");
260
261        // Resolve every object type ONCE: explicit category override > a caller's group override
262        // > the vendor's own rule for the category (defaultTypeRulesFor; MySQL indexes and
263        // parameters) > the group's vendor-seeded rule. A type no rule singles out inherits its
264        // group's rule, so a default profile keeps exactly the rules it had before per-category
265        // configuration existed.
266        EnumMap<ESQLDataObjectType, IdentifierRules> vendorTypeRules = defaultTypeRulesFor(vendor, flags);
267        this.effectiveRules = new EnumMap<>(ESQLDataObjectType.class);
268        for (ESQLDataObjectType type : ESQLDataObjectType.values()) {
269            IdentifierRules override = this.rulesByType.get(type);
270            if (override == null && !this.overriddenGroups.contains(mapToGroup(type))) {
271                override = vendorTypeRules.get(type);
272            }
273            if (override == null) {
274                override = this.rulesByGroup.get(mapToGroup(type));
275            }
276            if (override == null) {
277                // A group slot is missing: the seed tables fill all three, so this can only be a
278                // programming error inside this class. Fail loudly instead of comparing under null.
279                throw new IllegalStateException("No identifier rules for " + type
280                        + " (group " + mapToGroup(type) + ") on vendor " + vendor);
281            }
282            this.effectiveRules.put(type, override);
283        }
284
285        // Immutable → compute once; getFingerprint sits on the per-compare hot path
286        // since areEqual/canonKey stamp keys with it (P0d). Since per-category configuration
287        // exists the fingerprint must cover the WHOLE effective strategy: hashing only the three
288        // group slots would let a table-only override share a fingerprint with the vendor default
289        // and make two different policies look like one rule domain.
290        this.fingerprint = Objects.hash(vendor, flags, this.effectiveRules);
291    }
292
293    private final long fingerprint;
294
295    // ===== 工厂方法:根据厂商和 flags 创建 =====
296
297    /**
298     * 为指定厂商创建标识符配置档案
299     *
300     * @param vendor 数据库厂商
301     * @param flags vendor-specific flags
302     * @return 标识符配置档案
303     */
304    public static IdentifierProfile forVendor(EDbVendor vendor, VendorFlags flags) {
305        Objects.requireNonNull(vendor, "vendor");
306        Objects.requireNonNull(flags, "flags");
307        if (DEFAULT_FLAGS.equals(flags)) {
308            // The overwhelmingly common call. Profiles are immutable, so one shared instance per
309            // vendor is safe, and it keeps the per-type resolution below off hot paths that build
310            // a profile per AST node (TObjectName.getIdentifierService).
311            IdentifierProfile cached = DEFAULT_FLAG_PROFILES.get(vendor);
312            if (cached == null) {
313                cached = new IdentifierProfile(vendor, defaultRulesFor(vendor, flags), null, null, flags);
314                IdentifierProfile raced = DEFAULT_FLAG_PROFILES.putIfAbsent(vendor, cached);
315                if (raced != null) {
316                    cached = raced;
317                }
318            }
319            return cached;
320        }
321        return new IdentifierProfile(vendor, defaultRulesFor(vendor, flags), null, null, flags);
322    }
323
324    /**
325     * {@code configured} when it is a configuration for {@code vendor}, else that vendor's
326     * default profile.
327     *
328     * <p>For the places that build an object of a dialect an analysis may or may not be configured
329     * for — a metadata file that declares its own {@code dbVendor}, nested SQL in another dialect.
330     * A profile belongs to exactly one vendor, so it is never transplanted onto another: rules
331     * chosen for BigQuery tables say nothing about Oracle's.
332     *
333     * @param configured the analysis's configuration, or null when there is none
334     * @param vendor the vendor the caller actually needs a profile for
335     * @return a profile for {@code vendor}; never null
336     */
337    public static IdentifierProfile forVendorOr(IdentifierProfile configured, EDbVendor vendor) {
338        if (configured != null && configured.getVendor() == vendor) {
339            return configured;
340        }
341        return forVendor(vendor, VendorFlags.defaults());
342    }
343
344    private static final VendorFlags DEFAULT_FLAGS = VendorFlags.defaults();
345
346    private static final ConcurrentHashMap<EDbVendor, IdentifierProfile> DEFAULT_FLAG_PROFILES =
347            new ConcurrentHashMap<EDbVendor, IdentifierProfile>();
348
349    // ===== Builder entry points =====
350
351    /**
352     * Create a builder seeded from default rules for the given vendor
353     * with default VendorFlags.
354     *
355     * @param vendor database vendor
356     * @return a new Builder
357     */
358    public static Builder builder(EDbVendor vendor) {
359        return builder(vendor, VendorFlags.defaults());
360    }
361
362    /**
363     * Create a builder seeded from default rules for the given vendor
364     * and the specified VendorFlags.
365     *
366     * @param vendor database vendor
367     * @param flags vendor-specific flags
368     * @return a new Builder
369     */
370    public static Builder builder(EDbVendor vendor, VendorFlags flags) {
371        Objects.requireNonNull(vendor, "vendor");
372        Objects.requireNonNull(flags, "flags");
373        return new Builder(vendor, flags, defaultRulesFor(vendor, flags));
374    }
375
376    /**
377     * Create a builder pre-populated from this profile's current state.
378     * Enables deriving a variant without re-specifying everything.
379     *
380     * @return a new Builder seeded from this profile
381     */
382    public Builder toBuilder() {
383        Builder builder = new Builder(this.vendor, this.flags, new EnumMap<>(this.rulesByGroup));
384        builder.typeOverrides.putAll(this.rulesByType);
385        // The seed now holds this profile's group overrides; remember which they are, so they keep
386        // outranking the vendor's per-category rules in the derived profile too.
387        builder.seededGroupOverrides.addAll(this.overriddenGroups);
388        return builder;
389    }
390
391    // ===== Internal: single source of truth for default rules =====
392
393    /**
394     * Compute the default per-group rules for the given vendor and flags.
395     * This is the sole source of truth for default rules -- both forVendor()
396     * and the Builder use this method.
397     *
398     * @param vendor database vendor
399     * @param flags vendor-specific flags
400     * @return mutable EnumMap with rules for all three groups
401     */
402    static EnumMap<ObjectGroup, IdentifierRules> defaultRulesFor(EDbVendor vendor, VendorFlags flags) {
403        EnumMap<ObjectGroup, IdentifierRules> rules = new EnumMap<>(ObjectGroup.class);
404
405        switch (vendor) {
406            case dbvoracle: {
407                // Verified on Oracle 23.26 (2026-10-11). The Oracle-shaped forks below keep
408                // forOracle() until their own engines are verified.
409                IdentifierRules rule = IdentifierRules.forOracleNative();
410                rules.put(ObjectGroup.NAME_GROUP, rule);
411                rules.put(ObjectGroup.COLUMN_GROUP, rule);
412                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
413                break;
414            }
415
416            case dbvdameng: {
417                IdentifierRules rule = IdentifierRules.forDameng();
418                rules.put(ObjectGroup.NAME_GROUP, rule);
419                rules.put(ObjectGroup.COLUMN_GROUP, rule);
420                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
421                break;
422            }
423
424            case dbvpostgresql: {
425                // Verified on PostgreSQL 16.15, UTF8 (2026-10-11).
426                IdentifierRules rule = IdentifierRules.forPostgreSQLNative();
427                rules.put(ObjectGroup.NAME_GROUP, rule);
428                rules.put(ObjectGroup.COLUMN_GROUP, rule);
429                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
430                break;
431            }
432
433            case dbvgreenplum:
434            case dbvgaussdb:
435            case dbvedb: {
436                // PostgreSQL forks: not verified on their own engines, so they keep the shape they
437                // shared with PostgreSQL before 2026-10-11.
438                IdentifierRules rule = IdentifierRules.forPostgreSQL();
439                rules.put(ObjectGroup.NAME_GROUP, rule);
440                rules.put(ObjectGroup.COLUMN_GROUP, rule);
441                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
442                break;
443            }
444
445            case dbvduckdb: {
446                IdentifierRules rule = IdentifierRules.forDuckDB();
447                rules.put(ObjectGroup.NAME_GROUP, rule);
448                rules.put(ObjectGroup.COLUMN_GROUP, rule);
449                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
450                break;
451            }
452
453            case dbvsqlite: {
454                IdentifierRules rule = IdentifierRules.forSqlite();
455                rules.put(ObjectGroup.NAME_GROUP, rule);
456                rules.put(ObjectGroup.COLUMN_GROUP, rule);
457                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
458                break;
459            }
460
461            case dbvfirebird: {
462                IdentifierRules rule = IdentifierRules.forFirebird();
463                rules.put(ObjectGroup.NAME_GROUP, rule);
464                rules.put(ObjectGroup.COLUMN_GROUP, rule);
465                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
466                break;
467            }
468
469            case dbvsybase:
470            case dbvsybasease: {
471                IdentifierRules rule = IdentifierRules.forSybase();
472                rules.put(ObjectGroup.NAME_GROUP, rule);
473                rules.put(ObjectGroup.COLUMN_GROUP, rule);
474                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
475                break;
476            }
477
478            case dbvsybaseiq:
479            case dbvsqlanywhere: {
480                IdentifierRules rule = IdentifierRules.forSqlAnywhere();
481                rules.put(ObjectGroup.NAME_GROUP, rule);
482                rules.put(ObjectGroup.COLUMN_GROUP, rule);
483                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
484                break;
485            }
486
487            case dbvaccess: {
488                IdentifierRules rule = IdentifierRules.forAccess();
489                rules.put(ObjectGroup.NAME_GROUP, rule);
490                rules.put(ObjectGroup.COLUMN_GROUP, rule);
491                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
492                break;
493            }
494
495            case dbvdax: {
496                IdentifierRules rule = IdentifierRules.forDax();
497                rules.put(ObjectGroup.NAME_GROUP, rule);
498                rules.put(ObjectGroup.COLUMN_GROUP, rule);
499                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
500                break;
501            }
502
503            case dbvmdx: {
504                IdentifierRules rule = IdentifierRules.forMdx();
505                rules.put(ObjectGroup.NAME_GROUP, rule);
506                rules.put(ObjectGroup.COLUMN_GROUP, rule);
507                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
508                break;
509            }
510
511            case dbvpowerquery: {
512                IdentifierRules rule = IdentifierRules.forPowerQuery();
513                rules.put(ObjectGroup.NAME_GROUP, rule);
514                rules.put(ObjectGroup.COLUMN_GROUP, rule);
515                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
516                break;
517            }
518
519            case dbvansi:
520            case dbvgeneric:
521            case dbvodbc: {
522                IdentifierRules rule = IdentifierRules.forAnsi();
523                rules.put(ObjectGroup.NAME_GROUP, rule);
524                rules.put(ObjectGroup.COLUMN_GROUP, rule);
525                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
526                break;
527            }
528
529            case dbvredshift: {
530                IdentifierRules rule = flags.redshiftEnableCaseSensitive
531                    ? IdentifierRules.forCouchbase()
532                    : IdentifierRules.forRedshift();
533                rules.put(ObjectGroup.NAME_GROUP, rule);
534                rules.put(ObjectGroup.COLUMN_GROUP, rule);
535                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
536                break;
537            }
538
539            case dbvclickhouse: {
540                IdentifierRules rule = IdentifierRules.forCouchbase();
541                rules.put(ObjectGroup.NAME_GROUP, rule);
542                rules.put(ObjectGroup.COLUMN_GROUP, rule);
543                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
544                break;
545            }
546
547            case dbvcouchbase: {
548                IdentifierRules rule = IdentifierRules.forCouchbase();
549                rules.put(ObjectGroup.NAME_GROUP, rule);
550                rules.put(ObjectGroup.COLUMN_GROUP, rule);
551                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
552                break;
553            }
554
555            case dbvmssql:
556            case dbvazuresql: {
557                IdentifierRules rule = IdentifierRules.forSQLServer(flags.defaultCollation);
558                rules.put(ObjectGroup.NAME_GROUP, rule);
559                rules.put(ObjectGroup.COLUMN_GROUP, rule);
560                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
561                break;
562            }
563
564            case dbvmysql: {
565                // Verified on MySQL 8.4.10 (2026-10-11); indexes and parameters get their own
566                // rules in defaultTypeRulesFor. OceanBase, Doris and StarRocks keep the old cells.
567                rules.put(ObjectGroup.NAME_GROUP,
568                         IdentifierRules.forMySQLNative(flags.mysqlLowerCaseTableNames));
569                rules.put(ObjectGroup.COLUMN_GROUP, IdentifierRules.forMySQLNativeColumn());
570                rules.put(ObjectGroup.ROUTINE_GROUP, IdentifierRules.forMySQLNativeRoutine());
571                break;
572            }
573
574            case dbvoceanbase: {
575                // Phase 1: identifier rules mirror MySQL because the default
576                // EOBTenantMode is MYSQL and the IdentifierProfile is built
577                // before the user has had a chance to call setOBTenantMode.
578                // TODO(oceanbase Phase 3): split this into MYSQL/ORACLE
579                // branches once the EOBTenantMode is queryable from
580                // IdentifierProfileFlags or wherever the profile is built.
581                rules.put(ObjectGroup.NAME_GROUP,
582                         IdentifierRules.forMySQL(flags.mysqlLowerCaseTableNames));
583                rules.put(ObjectGroup.COLUMN_GROUP, IdentifierRules.forMySQLColumn());
584                rules.put(ObjectGroup.ROUTINE_GROUP, IdentifierRules.forMySQLRoutine());
585                break;
586            }
587
588            case dbvdoris: {
589                rules.put(ObjectGroup.NAME_GROUP, IdentifierRules.forDoris());
590                rules.put(ObjectGroup.COLUMN_GROUP, IdentifierRules.forDorisColumn());
591                rules.put(ObjectGroup.ROUTINE_GROUP, IdentifierRules.forMySQLRoutine());
592                break;
593            }
594
595            case dbvstarrocks: {
596                rules.put(ObjectGroup.NAME_GROUP, IdentifierRules.forStarrocks());
597                rules.put(ObjectGroup.COLUMN_GROUP, IdentifierRules.forStarrocksColumn());
598                rules.put(ObjectGroup.ROUTINE_GROUP, IdentifierRules.forMySQLRoutine());
599                break;
600            }
601
602            case dbvbigquery: {
603                rules.put(ObjectGroup.NAME_GROUP, IdentifierRules.forBigQueryTable());
604                rules.put(ObjectGroup.COLUMN_GROUP, IdentifierRules.forBigQueryColumn());
605                rules.put(ObjectGroup.ROUTINE_GROUP, IdentifierRules.forBigQueryTable());
606                break;
607            }
608
609            case dbvdb2:
610            case dbvnetezza:
611            case dbvexasol: {
612                IdentifierRules rule = IdentifierRules.forDB2();
613                rules.put(ObjectGroup.NAME_GROUP, rule);
614                rules.put(ObjectGroup.COLUMN_GROUP, rule);
615                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
616                break;
617            }
618
619            case dbvsnowflake: {
620                IdentifierRules rule = IdentifierRules.forSnowflake(
621                    flags.snowflakeQuotedIdentifiersIgnoreCase);
622                rules.put(ObjectGroup.NAME_GROUP, rule);
623                rules.put(ObjectGroup.COLUMN_GROUP, rule);
624                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
625                break;
626            }
627
628            case dbvhana: {
629                IdentifierRules rule = IdentifierRules.forHANA();
630                rules.put(ObjectGroup.NAME_GROUP, rule);
631                rules.put(ObjectGroup.COLUMN_GROUP, rule);
632                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
633                break;
634            }
635
636            case dbvpresto:
637            case dbvtrino: {
638                IdentifierRules rule = IdentifierRules.forPresto();
639                rules.put(ObjectGroup.NAME_GROUP, rule);
640                rules.put(ObjectGroup.COLUMN_GROUP, rule);
641                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
642                break;
643            }
644
645            case dbvvertica: {
646                IdentifierRules rule = IdentifierRules.forVertica();
647                rules.put(ObjectGroup.NAME_GROUP, rule);
648                rules.put(ObjectGroup.COLUMN_GROUP, rule);
649                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
650                break;
651            }
652
653            case dbvhive:
654            case dbvsparksql:
655            case dbvimpala:
656            case dbvdatabricks: {
657                IdentifierRules rule = IdentifierRules.forHive();
658                rules.put(ObjectGroup.NAME_GROUP, rule);
659                rules.put(ObjectGroup.COLUMN_GROUP, rule);
660                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
661                break;
662            }
663
664            case dbvflink: {
665                IdentifierRules rule = IdentifierRules.forFlink();
666                rules.put(ObjectGroup.NAME_GROUP, rule);
667                rules.put(ObjectGroup.COLUMN_GROUP, rule);
668                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
669                break;
670            }
671
672            case dbvteradata: {
673                IdentifierRules rule = IdentifierRules.forTeradata();
674                rules.put(ObjectGroup.NAME_GROUP, rule);
675                rules.put(ObjectGroup.COLUMN_GROUP, rule);
676                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
677                break;
678            }
679
680            case dbvathena: {
681                IdentifierRules rule = IdentifierRules.forAthena();
682                rules.put(ObjectGroup.NAME_GROUP, rule);
683                rules.put(ObjectGroup.COLUMN_GROUP, rule);
684                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
685                break;
686            }
687
688            default: {
689                // Vendors without a documented rule of their own here (Informix, OpenEdge, SOQL)
690                // keep the PostgreSQL shape they have always had.
691                IdentifierRules rule = IdentifierRules.forPostgreSQL();
692                rules.put(ObjectGroup.NAME_GROUP, rule);
693                rules.put(ObjectGroup.COLUMN_GROUP, rule);
694                rules.put(ObjectGroup.ROUTINE_GROUP, rule);
695                break;
696            }
697        }
698
699        return rules;
700    }
701
702    /**
703     * A vendor's own rule for single object categories that do not follow their group, under the
704     * given flags. Empty for every vendor but MySQL.
705     *
706     * <p>MySQL (verified on 8.4.10): index names do not follow {@code lower_case_table_names} —
707     * they compare like column names (under mode 0, {@code INDEX Idx} then {@code ADD INDEX idx}
708     * fails with ERROR 1061) — and routine parameters compare like routine names
709     * ({@code CREATE PROCEDURE pr(P int, p int)} fails with ERROR 1330). Both categories stay in
710     * NAME_GROUP (moving them would regroup them for every vendor); only their rule differs.
711     *
712     * <p>Precedence: an explicit category override and a caller's group override both win over
713     * these; they win over the group's vendor-seeded rule.
714     */
715    static EnumMap<ESQLDataObjectType, IdentifierRules> defaultTypeRulesFor(EDbVendor vendor,
716                                                                           VendorFlags flags) {
717        EnumMap<ESQLDataObjectType, IdentifierRules> rules = new EnumMap<>(ESQLDataObjectType.class);
718        if (vendor == EDbVendor.dbvmysql) {
719            rules.put(ESQLDataObjectType.dotIndex, IdentifierRules.forMySQLNativeColumn());
720            rules.put(ESQLDataObjectType.dotParameter, IdentifierRules.forMySQLNativeRoutine());
721        }
722        return rules;
723    }
724
725    // ===== Builder inner class =====
726
727    /**
728     * Builder for creating customized IdentifierProfile instances.
729     *
730     * <p>The builder is seeded from vendor defaults. Overrides can be applied
731     * per object group. The built profile is immutable.
732     *
733     * <p>Usage example:
734     * <pre>
735     * IdentifierProfile profile = IdentifierProfile
736     *     .builder(EDbVendor.dbvmssql)
737     *     .withColumnRules(customColumnRules)
738     *     .build();
739     * </pre>
740     */
741    public static final class Builder {
742
743        private EDbVendor vendor;
744        private VendorFlags flags;
745        private EnumMap<ObjectGroup, IdentifierRules> seededRules;
746        private final EnumMap<ObjectGroup, IdentifierRules> overrides;
747        private final EnumMap<ESQLDataObjectType, IdentifierRules> typeOverrides;
748        /** Groups whose SEED came from a profile's group override ({@link IdentifierProfile#toBuilder}). */
749        private final EnumSet<ObjectGroup> seededGroupOverrides = EnumSet.noneOf(ObjectGroup.class);
750
751        Builder(EDbVendor vendor, VendorFlags flags,
752                EnumMap<ObjectGroup, IdentifierRules> seedRules) {
753            this.vendor = vendor;
754            this.flags = flags;
755            this.seededRules = new EnumMap<>(seedRules);
756            this.overrides = new EnumMap<>(ObjectGroup.class);
757            this.typeOverrides = new EnumMap<>(ESQLDataObjectType.class);
758        }
759
760        /**
761         * Override VendorFlags (replaces default-seeded flags).
762         * Re-seeds rules that depend on flags while preserving
763         * explicit overrides set via withRules().
764         *
765         * @param flags new vendor flags
766         * @return this builder
767         */
768        public Builder withFlags(VendorFlags flags) {
769            this.flags = Objects.requireNonNull(flags, "flags");
770            this.seededRules = defaultRulesFor(this.vendor, flags);
771            // The seed is the vendor's again: nothing in it is a caller's override any more.
772            this.seededGroupOverrides.clear();
773            return this;
774        }
775
776        /**
777         * Override the rules for a specific object group.
778         *
779         * @param group the object group to override
780         * @param rules the custom rules
781         * @return this builder
782         */
783        public Builder withRules(ObjectGroup group, IdentifierRules rules) {
784            Objects.requireNonNull(group, "group");
785            Objects.requireNonNull(rules, "rules");
786            overrides.put(group, rules);
787            return this;
788        }
789
790        /**
791         * Convenience: override NAME_GROUP rules.
792         *
793         * @param rules the custom rules for names
794         * @return this builder
795         */
796        public Builder withNameRules(IdentifierRules rules) {
797            return withRules(ObjectGroup.NAME_GROUP, rules);
798        }
799
800        /**
801         * Convenience: override COLUMN_GROUP rules.
802         *
803         * @param rules the custom rules for columns
804         * @return this builder
805         */
806        public Builder withColumnRules(IdentifierRules rules) {
807            return withRules(ObjectGroup.COLUMN_GROUP, rules);
808        }
809
810        /**
811         * Convenience: override ROUTINE_GROUP rules.
812         *
813         * @param rules the custom rules for routines
814         * @return this builder
815         */
816        public Builder withRoutineRules(IdentifierRules rules) {
817            return withRules(ObjectGroup.ROUTINE_GROUP, rules);
818        }
819
820        /**
821         * Override the rules of ONE object category — catalog/project, schema, table, column,
822         * function … — independently of every other category (design §5.1).
823         *
824         * <p>This is the finest granularity and wins over a {@link #withRules(ObjectGroup,
825         * IdentifierRules) group} override and over the vendor default. It is deliberately a new
826         * name rather than an overload of {@code withRules}: {@code withRules(null, rules)} would
827         * become ambiguous, and an object CATEGORY is not an object group.
828         *
829         * <p>The argument is a category, never the name of a particular dataset, table or column —
830         * a profile expresses "how table names compare here", not "this table compares like that".
831         *
832         * <p>Repeating a category keeps the LAST value. To go back to inheriting, call
833         * {@link #withoutObjectRules(ESQLDataObjectType)}; {@code null} rules are rejected rather
834         * than treated as some implicit rule.
835         *
836         * <p>Note what a category override does and does not claim: it is the analysis assumption
837         * the CALLER supplies. It is not read from a live server, and an arbitrary override is not
838         * automatically a vendor's native semantics.
839         *
840         * @param objectType the object category to configure
841         * @param rules the complete rule cell (unquoted fold/compare, quoted fold/compare)
842         * @return this builder
843         */
844        public Builder withObjectRules(ESQLDataObjectType objectType, IdentifierRules rules) {
845            Objects.requireNonNull(objectType, "objectType");
846            Objects.requireNonNull(rules, "rules");
847            typeOverrides.put(objectType, rules);
848            return this;
849        }
850
851        /**
852         * Drop a category override set earlier, so the category inherits its group / vendor rule
853         * again. A category that was never overridden is unaffected.
854         *
855         * @param objectType the object category to stop overriding
856         * @return this builder
857         */
858        public Builder withoutObjectRules(ESQLDataObjectType objectType) {
859            Objects.requireNonNull(objectType, "objectType");
860            typeOverrides.remove(objectType);
861            return this;
862        }
863
864        /**
865         * Drop a group override set earlier, so the group falls back to the vendor-seeded rule.
866         * Category overrides are untouched — they sit above the group level.
867         *
868         * @param group the object group to stop overriding
869         * @return this builder
870         */
871        public Builder withoutRules(ObjectGroup group) {
872            Objects.requireNonNull(group, "group");
873            overrides.remove(group);
874            return this;
875        }
876
877        /**
878         * Build an immutable IdentifierProfile.
879         * Merges seeded rules with overrides (overrides win), then applies per-category overrides.
880         *
881         * @return the built profile
882         */
883        public IdentifierProfile build() {
884            EnumMap<ObjectGroup, IdentifierRules> merged = new EnumMap<>(seededRules);
885            merged.putAll(overrides);
886            EnumSet<ObjectGroup> overridden = EnumSet.copyOf(seededGroupOverrides);
887            overridden.addAll(overrides.keySet());
888            return new IdentifierProfile(vendor, merged, overridden, typeOverrides, flags);
889        }
890    }
891
892    // ===== 查询接口 =====
893
894    /**
895     * 获取指定对象类型的标识符规则
896     *
897     * @param objectType 对象类型
898     * @return 标识符规则
899     */
900    public IdentifierRules getRules(ESQLDataObjectType objectType) {
901        if (objectType == null) {
902            // Historic behavior of mapToGroup(null): an unstated category is a plain name.
903            return rulesByGroup.get(ObjectGroup.NAME_GROUP);
904        }
905        return effectiveRules.get(objectType);
906    }
907
908    /**
909     * The effective rule cell of one object category with {@code SAME_AS_UNQUOTED} resolved —
910     * the <em>rule domain</em> a {@link CanonKey} of that category belongs to.
911     *
912     * <p>Since categories inside one {@link ObjectGroup} may now carry different rules, the
913     * group alone no longer identifies the domain a canonical payload was produced in: with
914     * tables insensitive and schemas sensitive, a table keyed as {@code orders} and a schema
915     * literally named {@code orders} would otherwise produce equal keys. Two categories with the
916     * same effective cell share a domain, which is what keeps a default profile's
917     * table/schema/catalog keys interchangeable exactly as before.
918     *
919     * @param objectType the object category
920     * @return the resolved rule cell; never null
921     */
922    public IdentifierRules ruleDomainOf(ESQLDataObjectType objectType) {
923        return getRules(objectType).resolvedCell();
924    }
925
926    /**
927     * Whether this profile and {@code other} are the SAME effective strategy: same vendor, same
928     * flags, and the same resolved rule for every object category — regardless of how each was
929     * built (vendor default, group override, category override) or of object identity.
930     *
931     * <p>The {@link #getFingerprint() fingerprint} is a hash: it can accelerate this answer but
932     * must never stand in for it, since distinct strategies can collide in 32 bits.
933     *
934     * @param other the profile to compare with; null is never the same strategy
935     * @return true if the two profiles decide every identifier question identically
936     */
937    public boolean hasSameEffectivePolicyAs(IdentifierProfile other) {
938        if (this == other) {
939            return true;
940        }
941        if (other == null) {
942            return false;
943        }
944        return vendor == other.vendor
945                && flags.equals(other.flags)
946                && effectiveRules.equals(other.effectiveRules);
947    }
948
949    /**
950     * Value equality over the complete effective strategy — see
951     * {@link #hasSameEffectivePolicyAs}. Two profiles that decide every identifier question the
952     * same way ARE the same profile, so an equivalent instance passed at a second entry point
953     * does not read as a conflicting configuration.
954     */
955    @Override
956    public boolean equals(Object o) {
957        if (this == o) {
958            return true;
959        }
960        if (!(o instanceof IdentifierProfile)) {
961            return false;
962        }
963        return hasSameEffectivePolicyAs((IdentifierProfile) o);
964    }
965
966    @Override
967    public int hashCode() {
968        return (int) fingerprint;
969    }
970
971    /**
972     * The categories this profile configures explicitly, finest granularity first. Diagnostics
973     * only — the strategy itself is {@link #getRules}.
974     *
975     * @return an unmodifiable view of the per-category overrides
976     */
977    public Map<ESQLDataObjectType, IdentifierRules> getObjectRuleOverrides() {
978        return Collections.unmodifiableMap(rulesByType);
979    }
980
981    /**
982     * 获取 vendor flags
983     *
984     * @return vendor flags
985     */
986    public VendorFlags getFlags() {
987        return flags;
988    }
989
990    /**
991     * 获取数据库厂商
992     *
993     * @return 厂商
994     */
995    public EDbVendor getVendor() {
996        return vendor;
997    }
998
999    /**
1000     * 按对象组获取规则(内部使用)
1001     *
1002     * <p>This is the GROUP-level rule — the value a category inherits when it carries no
1003     * {@link Builder#withObjectRules} override. It is NOT what governs a comparison; only
1004     * {@link #getRules(ESQLDataObjectType)} is. Nothing that decides equality may read this.
1005     *
1006     * @param group 对象组
1007     * @return 标识符规则
1008     */
1009    IdentifierRules getRulesByGroup(ObjectGroup group) {
1010        return rulesByGroup.get(group);
1011    }
1012
1013    /**
1014     * 将对象类型映射到对象组(公开访问器,供 {@link CanonKey} 的规则域标识使用)
1015     *
1016     * @param objectType 对象类型
1017     * @return 对象组
1018     */
1019    public ObjectGroup groupOf(ESQLDataObjectType objectType) {
1020        return mapToGroup(objectType);
1021    }
1022
1023    /**
1024     * 将对象类型映射到对象组
1025     */
1026    private ObjectGroup mapToGroup(ESQLDataObjectType type) {
1027        if (type == null) return ObjectGroup.NAME_GROUP;
1028
1029        // Fail-closed and exhaustive on purpose (stage S0R). A `default: NAME_GROUP` looked
1030        // harmless but meant a NEW object type silently acquired the name rules — and a test
1031        // asserting "the group is non-null" could not tell the difference. Every value is listed;
1032        // a new one fails here until someone assigns it deliberately.
1033        switch (type) {
1034            case dotColumn:
1035                return ObjectGroup.COLUMN_GROUP;
1036
1037            // All four routine-like types share the routine comparison group. Only dotFunction
1038            // used to, which was a defect rather than a namespace statement:
1039            // CatalogModelValidator works around it by comparing every routine as dotFunction,
1040            // and its test proves why — MySQL with lower_case_table_names=0 still treats `Proc`
1041            // and `proc` as one procedure name. The four ESQLDataObjectType values stay distinct
1042            // because quoting can differ per type (DAX), but their CASE rules are the same.
1043            case dotRoutine:
1044            case dotFunction:
1045            case dotProcedure:
1046            case dotOraclePackage:
1047                return ObjectGroup.ROUTINE_GROUP;
1048
1049            case dotUnknown:
1050            case dotCatalog:
1051            case dotSchema:
1052            case dotTable:
1053            case dotTrigger:
1054            case dotSynonyms:
1055            case dotDblink:
1056            case dotDataType:
1057            case dotParameter:
1058            case dotServer:
1059            case dotSequence:
1060            case dotIndex:
1061            case dotConstraint:
1062                return ObjectGroup.NAME_GROUP;
1063
1064            default:
1065                throw new IllegalStateException("No object group for " + type
1066                        + " - add it explicitly to IdentifierProfile.mapToGroup");
1067        }
1068    }
1069
1070    // ===== 指纹计算(用于缓存失效) =====
1071
1072    /**
1073     * 计算配置指纹(用于 TObjectName 缓存失效)
1074     *
1075     * <p>当 vendor 或 flags 变化时,指纹会改变,触发缓存失效
1076     *
1077     * @return 配置指纹(64位哈希值)
1078     */
1079    public long getFingerprint() {
1080        return fingerprint;
1081    }
1082
1083    // ===== toString 方法(用于调试) =====
1084
1085    @Override
1086    public String toString() {
1087        if (rulesByType.isEmpty()) {
1088            return String.format("IdentifierProfile{vendor=%s, flags=%s}", vendor, flags);
1089        }
1090        return String.format("IdentifierProfile{vendor=%s, flags=%s, objectRules=%s}",
1091            vendor, flags, rulesByType);
1092    }
1093}