001package gudusoft.gsqlparser.sqlenv; 002 003import java.util.Objects; 004 005/** 006 * 标识符规则四元组(per vendor, per object group) 007 * 008 * <p>定义数据库厂商的标识符大小写规则,区分 quoted 和 unquoted 标识符的处理方式。 009 * 010 * <p>设计来源:dbobject_search.md 资深设计师方案 011 * 012 * <p>使用示例: 013 * <pre> 014 * // Oracle: unquoted 折叠为大写且不敏感,quoted 保留原样且敏感 015 * IdentifierRules oracleRules = IdentifierRules.forOracle(); 016 * 017 * // Couchbase: 全部大小写敏感 018 * IdentifierRules couchbaseRules = IdentifierRules.forCouchbase(); 019 * </pre> 020 * 021 * @since 3.1.0.9 022 */ 023public final class IdentifierRules { 024 025 // ===== 四元组定义 ===== 026 027 /** 028 * Unquoted 标识符的大小写折叠规则 029 */ 030 public final CaseFold unquotedFold; 031 032 /** 033 * Unquoted 标识符的大小写比较规则 034 */ 035 public final CaseCompare unquotedCompare; 036 037 /** 038 * Quoted 标识符的大小写折叠规则(通常为 NONE,保留原样) 039 */ 040 public final CaseFold quotedFold; 041 042 /** 043 * Quoted 标识符的大小写比较规则 044 */ 045 public final CaseCompare quotedCompare; 046 047 /** 048 * How a branch of this cell that compares {@link CaseCompare#INSENSITIVE INSENSITIVE} but does 049 * not fold ({@link CaseFold#NONE}) decides equality: two names are equal when their images 050 * under this mapping are equal. {@link CaseCollapse#SIMPLE_FOLD} (Unicode simple case folding) 051 * for every cell built with the four-argument constructor. A branch that folds is compared by 052 * its folded text and never consults this. 053 * 054 * <p>Kept apart from the fold on purpose: the fold is the name a database STORES (what 055 * {@link IdentifierService#normalize} returns), the collapse is only how it COMPARES. MySQL 056 * keeps a column name as written but compares it by its simple lowercase mapping, so its 057 * column cell is {@code NONE} with collapse {@link CaseCollapse#LOWER_SIMPLE}. 058 */ 059 public final CaseCollapse collapse; 060 061 /** 062 * The longest name the database keeps, in UTF-8 bytes; a longer name is cut to the longest 063 * prefix of whole characters that fits, for storing and looking up alike, quoted or not. 064 * {@code 0} means no truncation. PostgreSQL: 63 ({@code NAMEDATALEN - 1}). 065 */ 066 public final int maxUtf8Bytes; 067 068 // ===== 枚举定义 ===== 069 070 /** 071 * 大小写折叠规则(Case Folding) 072 * 073 * <p>决定如何规范化标识符的大小写 074 */ 075 public enum CaseFold { 076 /** 077 * 不转换(保留原样) 078 * <p>例如:Couchbase unquoted, 所有 quoted identifiers 079 */ 080 NONE, 081 082 /** 083 * 转大写 084 * <p>例如:Oracle unquoted, DB2 unquoted, Snowflake unquoted 085 */ 086 UPPER, 087 088 /** 089 * 转小写 090 * <p>例如:PostgreSQL unquoted, Redshift unquoted, Greenplum unquoted 091 */ 092 LOWER, 093 094 /** 095 * Simple uppercase mapping, one code point to one ({@code ß} is kept): each code point 096 * becomes its UnicodeData Simple_Uppercase_Mapping. SQL-LR {@code UPPER_SIMPLE}. 097 * <p>例如:Oracle unquoted ({@code straße} is stored as {@code STRAßE}, not 098 * {@code STRASSE}; verified on Oracle 23.26). 099 * @see gudusoft.gsqlparser.sqlenv.unicode.GspCaseData#toUpperSimple 100 */ 101 UPPER_SIMPLE, 102 103 /** 104 * Simple lowercase mapping, one code point to one, no Final_Sigma context 105 * ({@code İ} becomes {@code i}). SQL-LR {@code LOWER_SIMPLE}. 106 * <p>例如:MySQL table names under {@code lower_case_table_names=1} 107 * (verified on MySQL 8.4). 108 * @see gudusoft.gsqlparser.sqlenv.unicode.GspCaseData#toLowerSimple 109 */ 110 LOWER_SIMPLE, 111 112 /** 113 * Only {@code A}..{@code Z} are lowered; every other character is kept. SQL-LR 114 * {@code LOWER_ASCII}. 115 * <p>例如:PostgreSQL unquoted in a UTF8 database ({@code ÄBC} is stored as 116 * {@code Äbc}; verified on PostgreSQL 16). 117 * @see gudusoft.gsqlparser.sqlenv.unicode.GspCaseData#toLowerAscii 118 */ 119 LOWER_ASCII 120 } 121 122 /** 123 * The mapping under which an {@link CaseCompare#INSENSITIVE INSENSITIVE}, non-folding branch 124 * compares: two names are one name when their images are equal. See {@link #collapse}. 125 */ 126 public enum CaseCollapse { 127 /** 128 * Unicode simple case folding (CaseFolding.txt statuses C and S). The default, and the 129 * only behaviour before 2026-10: BigQuery columns, Hive / Teradata / DuckDB quoted names, 130 * every custom cell built with the four-argument constructor. 131 */ 132 SIMPLE_FOLD, 133 134 /** 135 * Simple lowercase mapping ({@link CaseFold#LOWER_SIMPLE}). MySQL table names under 136 * {@code lower_case_table_names} 1 and 2, column and index names: {@code ſ} and {@code s}, 137 * {@code ς} and {@code σ} are DIFFERENT columns, {@code İ} and {@code i} the same table. 138 */ 139 LOWER_SIMPLE, 140 141 /** 142 * Simple uppercase mapping ({@link CaseFold#UPPER_SIMPLE}). MySQL routine and parameter 143 * names: {@code ı} and {@code i} are the same function. 144 */ 145 UPPER_SIMPLE 146 } 147 148 /** 149 * 大小写比较规则(Case Comparison) 150 * 151 * <p>决定如何比较两个标识符是否相等 152 */ 153 public enum CaseCompare { 154 /** 155 * 大小写敏感 156 * <p>例如:Couchbase 所有标识符, 所有 quoted identifiers 157 */ 158 SENSITIVE, 159 160 /** 161 * 大小写不敏感 162 * <p>例如:Oracle unquoted, MySQL unquoted 163 */ 164 INSENSITIVE, 165 166 /** 167 * 基于 collation(运行时决定) 168 * <p>例如:SQL Server, Azure SQL 169 * <p>需要使用 {@link java.text.Collator} 进行比较 170 */ 171 COLLATION_BASED, 172 173 /** 174 * 与 unquoted 规则一致 175 * <p>例如:Presto quoted identifiers, Vertica quoted identifiers 176 * <p>在解析时需要回退到 unquotedCompare 177 */ 178 SAME_AS_UNQUOTED 179 } 180 181 // ===== 构造函数 ===== 182 183 /** 184 * 构造标识符规则 185 * 186 * @param unquotedFold unquoted 标识符的折叠规则 187 * @param unquotedCompare unquoted 标识符的比较规则 188 * @param quotedFold quoted 标识符的折叠规则 189 * @param quotedCompare quoted 标识符的比较规则 190 */ 191 public IdentifierRules(CaseFold unquotedFold, CaseCompare unquotedCompare, 192 CaseFold quotedFold, CaseCompare quotedCompare) { 193 this(unquotedFold, unquotedCompare, quotedFold, quotedCompare, CaseCollapse.SIMPLE_FOLD, 0); 194 } 195 196 /** 197 * 构造标识符规则(完整参数) 198 * 199 * @param unquotedFold unquoted 标识符的折叠规则 200 * @param unquotedCompare unquoted 标识符的比较规则 201 * @param quotedFold quoted 标识符的折叠规则 202 * @param quotedCompare quoted 标识符的比较规则 203 * @param collapse how an INSENSITIVE branch that does not fold compares ({@link #collapse}) 204 * @param maxUtf8Bytes the length in UTF-8 bytes a name is cut to, {@code 0} for none 205 * ({@link #maxUtf8Bytes}) 206 */ 207 public IdentifierRules(CaseFold unquotedFold, CaseCompare unquotedCompare, 208 CaseFold quotedFold, CaseCompare quotedCompare, 209 CaseCollapse collapse, int maxUtf8Bytes) { 210 this.unquotedFold = Objects.requireNonNull(unquotedFold, "unquotedFold"); 211 this.unquotedCompare = Objects.requireNonNull(unquotedCompare, "unquotedCompare"); 212 this.quotedFold = Objects.requireNonNull(quotedFold, "quotedFold"); 213 this.quotedCompare = Objects.requireNonNull(quotedCompare, "quotedCompare"); 214 this.collapse = Objects.requireNonNull(collapse, "collapse"); 215 if (maxUtf8Bytes < 0) { 216 throw new IllegalArgumentException("maxUtf8Bytes must be 0 (no limit) or positive: " 217 + maxUtf8Bytes); 218 } 219 this.maxUtf8Bytes = maxUtf8Bytes; 220 } 221 222 // ===== 缓存的单例实例 ===== 223 224 private static final IdentifierRules ORACLE = new IdentifierRules( 225 CaseFold.UPPER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 226 227 private static final IdentifierRules POSTGRESQL = new IdentifierRules( 228 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 229 230 private static final IdentifierRules COUCHBASE = new IdentifierRules( 231 CaseFold.NONE, CaseCompare.SENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 232 233 private static final IdentifierRules PRESTO = new IdentifierRules( 234 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.SAME_AS_UNQUOTED); 235 236 private static final IdentifierRules REDSHIFT = new IdentifierRules( 237 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.LOWER, CaseCompare.INSENSITIVE); 238 239 private static final IdentifierRules HIVE = new IdentifierRules( 240 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 241 242 private static final IdentifierRules TERADATA = new IdentifierRules( 243 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 244 245 private static final IdentifierRules FLINK = new IdentifierRules( 246 CaseFold.NONE, CaseCompare.SENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 247 248 private static final IdentifierRules DUCKDB = new IdentifierRules( 249 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 250 251 private static final IdentifierRules SQLITE = new IdentifierRules( 252 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 253 254 private static final IdentifierRules SYBASE = new IdentifierRules( 255 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 256 257 private static final IdentifierRules SQL_ANYWHERE = new IdentifierRules( 258 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 259 260 private static final IdentifierRules ACCESS = new IdentifierRules( 261 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 262 263 private static final IdentifierRules DAX = new IdentifierRules( 264 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 265 266 private static final IdentifierRules MDX = new IdentifierRules( 267 CaseFold.LOWER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 268 269 private static final IdentifierRules POWER_QUERY = new IdentifierRules( 270 CaseFold.NONE, CaseCompare.SENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 271 272 private static final IdentifierRules ANSI = new IdentifierRules( 273 CaseFold.UPPER, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 274 275 private static final IdentifierRules BIGQUERY_TABLE = new IdentifierRules( 276 CaseFold.NONE, CaseCompare.SENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 277 278 private static final IdentifierRules BIGQUERY_COLUMN = new IdentifierRules( 279 CaseFold.NONE, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 280 281 private static final IdentifierRules MYSQL_COLUMN = new IdentifierRules( 282 CaseFold.NONE, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE); 283 284 // ----- The verified engines (2026-10-11). Each cell below was checked against a real server; 285 // the forks that used to share a cell with these vendors keep the old one (forOracle(), 286 // forPostgreSQL(), forMySQL(int), forMySQLColumn(), forMySQLRoutine()) until they are verified 287 // on their own engines. 288 289 private static final IdentifierRules ORACLE_NATIVE = new IdentifierRules( 290 CaseFold.UPPER_SIMPLE, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 291 292 /** PostgreSQL's NAMEDATALEN - 1 with the default build (NAMEDATALEN = 64). */ 293 public static final int POSTGRESQL_MAX_IDENTIFIER_BYTES = 63; 294 295 private static final IdentifierRules POSTGRESQL_NATIVE = new IdentifierRules( 296 CaseFold.LOWER_ASCII, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE, 297 CaseCollapse.SIMPLE_FOLD, POSTGRESQL_MAX_IDENTIFIER_BYTES); 298 299 private static final IdentifierRules MYSQL_NATIVE_LCTN0 = new IdentifierRules( 300 CaseFold.NONE, CaseCompare.SENSITIVE, CaseFold.NONE, CaseCompare.SENSITIVE); 301 302 private static final IdentifierRules MYSQL_NATIVE_LCTN1 = new IdentifierRules( 303 CaseFold.LOWER_SIMPLE, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE, 304 CaseCollapse.LOWER_SIMPLE, 0); 305 306 private static final IdentifierRules MYSQL_NATIVE_LCTN2 = new IdentifierRules( 307 CaseFold.NONE, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE, 308 CaseCollapse.LOWER_SIMPLE, 0); 309 310 private static final IdentifierRules MYSQL_NATIVE_COLUMN = new IdentifierRules( 311 CaseFold.NONE, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE, 312 CaseCollapse.LOWER_SIMPLE, 0); 313 314 private static final IdentifierRules MYSQL_NATIVE_ROUTINE = new IdentifierRules( 315 CaseFold.NONE, CaseCompare.INSENSITIVE, CaseFold.NONE, CaseCompare.INSENSITIVE, 316 CaseCollapse.UPPER_SIMPLE, 0); 317 318 // ===== 预设规则工厂方法 ===== 319 320 /** 321 * Oracle 标识符规则 322 * 323 * <p><strong>实际数据库行为(Oracle 12c+):</strong> 324 * <ul> 325 * <li>Unquoted: 折叠为大写,比较不敏感 (CREATE TABLE foo → stored as FOO, foo=FOO=Foo) 326 * <li>Quoted: 保留原样,比较敏感 (CREATE TABLE "foo" → stored as foo, "foo"!="FOO") 327 * </ul> 328 * 329 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 330 * <ul> 331 * <li>columnCollationCaseSensitive = {@code true} → ❌ <strong>INCOMPATIBLE</strong> (legacy preserved case, new folds to UPPER) 332 * <li>functionCollationCaseSensitive = {@code true} → ❌ <strong>INCOMPATIBLE</strong> (legacy preserved case, new folds to UPPER) 333 * <li>tableCollationCaseSensitive = {@code true} → ❌ <strong>INCOMPATIBLE</strong> (legacy preserved case, new folds to UPPER) 334 * <li>catalogCollationCaseSensitive = {@code false} → ✅ <strong>COMPATIBLE</strong> (both fold to UPPER) 335 * </ul> 336 * 337 * <p><strong>测试用例影响:</strong> 338 * <ul> 339 * <li>✅ 新规则正确:Oracle 确实将 unquoted identifiers 折叠为大写</li> 340 * <li>⚠️ 如果旧测试期望保留原始大小写 (如 "myTable" 保持为 "myTable"),则测试会失败</li> 341 * <li>⚠️ 应更新测试期望为大写 (如 "myTable" → "MYTABLE")</li> 342 * </ul> 343 * 344 * <p><strong>IdentifierRules 配置:</strong> 345 * <ul> 346 * <li>Unquoted: 折叠为大写 ({@link CaseFold#UPPER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 347 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 348 * </ul> 349 * 350 * <p><strong>Since 2026-10-11 this is the shape the Oracle-like FORKS share</strong> (DB2, 351 * Netezza, Exasol, HANA, Snowflake, Dameng, Firebird), none of which has been verified on its 352 * own engine. {@link gudusoft.gsqlparser.EDbVendor#dbvoracle} itself uses 353 * {@link #forOracleNative()}: Oracle upper-cases an unquoted name with the SIMPLE mapping, so 354 * {@code straße} is {@code STRAßE}, which this cell's full mapping turns into {@code STRASSE}. 355 */ 356 public static IdentifierRules forOracle() { 357 return ORACLE; 358 } 359 360 /** 361 * Oracle 标识符规则,as verified on Oracle 23.26 (AL32UTF8), used by 362 * {@link gudusoft.gsqlparser.EDbVendor#dbvoracle}. 363 * 364 * <ul> 365 * <li>Unquoted: the simple uppercase mapping ({@link CaseFold#UPPER_SIMPLE}), case-insensitive. 366 * {@code CREATE TABLE straße} is listed in {@code user_tables} as {@code STRAßE} (6 367 * characters); {@code FROM "STRAßE"} finds it, {@code FROM STRASSE} raises ORA-00942. 368 * <li>Quoted: kept as written ({@link CaseFold#NONE}), case-sensitive. 369 * </ul> 370 * 371 * <p>SQL-LR {@code oracle/rules-1}, cell {@code U_UPPER_SIMPLE_CI__Q_NONE_CS}. 372 */ 373 public static IdentifierRules forOracleNative() { 374 return ORACLE_NATIVE; 375 } 376 377 /** 378 * Dameng (达梦) 标识符规则(与 Oracle 相同) 379 * 380 * <p>Dameng follows Oracle identifier conventions: 381 * <ul> 382 * <li>Unquoted: 折叠为大写, 比较不敏感 383 * <li>Quoted: 保留原样, 比较敏感 384 * </ul> 385 */ 386 public static IdentifierRules forDameng() { 387 return forOracle(); 388 } 389 390 /** 391 * PostgreSQL / Redshift / Greenplum 标识符规则 392 * 393 * <p><strong>实际数据库行为(PostgreSQL 12+):</strong> 394 * <ul> 395 * <li>Unquoted: 折叠为小写,比较不敏感 (CREATE TABLE MyTable → stored as mytable, MyTable=mytable=MYTABLE) 396 * <li>Quoted: 保留原样,比较敏感 (CREATE TABLE "MyTable" → stored as MyTable, "MyTable"!="mytable") 397 * </ul> 398 * 399 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 400 * <ul> 401 * <li>columnCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new folds to LOWER) 402 * <li>functionCollationCaseSensitive = {@code true} → ❌ <strong>INCOMPATIBLE</strong> (legacy preserved case, new folds to LOWER) 403 * <li>tableCollationCaseSensitive = {@code true} → ❌ <strong>INCOMPATIBLE</strong> (legacy preserved case, new folds to LOWER) 404 * <li>catalogCollationCaseSensitive = {@code true} → ❌ <strong>INCOMPATIBLE</strong> (legacy preserved case, new folds to LOWER) 405 * </ul> 406 * 407 * <p><strong>测试用例影响:</strong> 408 * <ul> 409 * <li>✅ 新规则正确:PostgreSQL 确实将 unquoted identifiers 折叠为小写</li> 410 * <li>⚠️ 如果旧测试期望大写 (如 "MyTable" → "MYTABLE"),则测试会失败</li> 411 * <li>⚠️ 应更新测试期望为小写 (如 "MyTable" → "mytable")</li> 412 * <li>⚠️ 如果旧测试期望保留原始大小写,也会失败</li> 413 * </ul> 414 * 415 * <p><strong>IdentifierRules 配置:</strong> 416 * <ul> 417 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 418 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 419 * </ul> 420 * 421 * <p><strong>Since 2026-10-11 this is the shape the PostgreSQL-like FORKS share</strong> 422 * (Greenplum, GaussDB, EDB, and the fallback of Informix, OpenEdge and SOQL), none verified on 423 * its own engine. {@link gudusoft.gsqlparser.EDbVendor#dbvpostgresql} itself uses 424 * {@link #forPostgreSQLNative()}. 425 */ 426 public static IdentifierRules forPostgreSQL() { 427 return POSTGRESQL; 428 } 429 430 /** 431 * PostgreSQL 标识符规则,as verified on PostgreSQL 16.15 with a UTF8 database, used by 432 * {@link gudusoft.gsqlparser.EDbVendor#dbvpostgresql}. 433 * 434 * <ul> 435 * <li>Unquoted: only {@code A}..{@code Z} are lowered ({@link CaseFold#LOWER_ASCII}) — 436 * {@code downcase_identifier} lowers nothing else in a multibyte encoding. After 437 * {@code CREATE TABLE "Äbc"}, {@code FROM ÄBC} finds it and {@code FROM "äbc"} does not. 438 * <li>Quoted: kept as written, case-sensitive. 439 * <li>Both: cut to {@value #POSTGRESQL_MAX_IDENTIFIER_BYTES} UTF-8 bytes on a whole character 440 * ({@code truncate_identifier} / {@code pg_mbcliplen}), so two names that share their first 441 * 63 bytes are one name: {@code CREATE TABLE <63 a>x} is found by {@code FROM <63 a>y}. 442 * </ul> 443 * 444 * <p>SQL-LR {@code postgresql/rules-1}, cell {@code U_LOWER_ASCII_CI__Q_NONE_CS} with 445 * {@code truncation = UTF8_BYTES 63}. 446 */ 447 public static IdentifierRules forPostgreSQLNative() { 448 return POSTGRESQL_NATIVE; 449 } 450 451 /** 452 * Amazon Redshift 标识符规则(默认配置,{@code enable_case_sensitive_identifier = false}) 453 * 454 * <p><strong>实际数据库行为(AWS 文档 "Names and identifiers"):</strong> 455 * standard 与 delimited identifier 都不区分大小写,并折叠为小写存储; 456 * 只有打开 {@code enable_case_sensitive_identifier} 时,delimited identifier 才保留大小写。 457 * 因此 {@code "Orders"}、{@code "ORDERS"} 与 {@code orders} 是同一个对象。 458 * 459 * <p><strong>IdentifierRules 配置:</strong> 460 * <ul> 461 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 462 * <li>Quoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 463 * </ul> 464 */ 465 public static IdentifierRules forRedshift() { 466 return REDSHIFT; 467 } 468 469 /** 470 * Couchbase N1QL 标识符规则 471 * 472 * <p><strong>实际数据库行为(Couchbase N1QL):</strong> 473 * <ul> 474 * <li>Unquoted: 保留原样,比较敏感 475 * <li>Quoted: 保留原样,比较敏感 476 * </ul> 477 * 478 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 479 * <ul> 480 * <li>tableCollationCaseSensitive = {@code false} → ❌ <strong>INCOMPATIBLE</strong> (legacy folded to UPPER, new preserves case) 481 * </ul> 482 * 483 * <p><strong>IdentifierRules 配置:</strong> 484 * <ul> 485 * <li>Unquoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 486 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 487 * </ul> 488 */ 489 public static IdentifierRules forCouchbase() { 490 return COUCHBASE; 491 } 492 493 /** 494 * SQL Server / Azure SQL 标识符规则 495 * 496 * <p><strong>实际数据库行为(SQL Server 2019+):</strong> 497 * <ul> 498 * <li>Unquoted: 保留原样,比较由 collation 决定 (CREATE TABLE MyTable → stored as MyTable) 499 * <li>Quoted: 保留原样,比较由 collation 决定 (CREATE TABLE [MyTable] → stored as MyTable) 500 * <li>默认 collation (SQL_Latin1_General_CP1_CI_AS): 大小写不敏感 (MyTable=mytable=MYTABLE) 501 * <li>CS collation (SQL_Latin1_General_CP1_CS_AS): 大小写敏感 (MyTable!=mytable) 502 * </ul> 503 * 504 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 505 * <ul> 506 * <li>columnCollationCaseSensitive = {@code false} → ❌ <strong>INCOMPATIBLE</strong> (legacy folded to UPPER, new preserves case) 507 * <li>functionCollationCaseSensitive = {@code false} → ❌ <strong>INCOMPATIBLE</strong> (legacy folded to UPPER, new preserves case) 508 * <li>tableCollationCaseSensitive = {@code false} → ❌ <strong>INCOMPATIBLE</strong> (legacy folded to UPPER, new preserves case) 509 * <li>catalogCollationCaseSensitive = {@code false} → ❌ <strong>INCOMPATIBLE</strong> (legacy folded to UPPER, new preserves case) 510 * </ul> 511 * 512 * <p><strong>测试用例影响:</strong> 513 * <ul> 514 * <li>✅ 新规则正确:SQL Server 保留标识符原始大小写,使用 collation 进行比较</li> 515 * <li>⚠️ 如果旧测试期望 "MyTable" → "MYTABLE",则测试会失败</li> 516 * <li>⚠️ 应更新测试期望为保留原样 (如 "MyTable" 保持为 "MyTable")</li> 517 * <li>⚠️ 这是导致 dataflow 测试 processId 变化的根本原因!</li> 518 * <li>📝 参考: investigation_findings_2025_10_20.md</li> 519 * </ul> 520 * 521 * <p><strong>IdentifierRules 配置:</strong> 522 * <ul> 523 * <li>Unquoted: 不折叠 ({@link CaseFold#NONE}), 基于 collation 比较 ({@link CaseCompare#COLLATION_BASED}) 524 * <li>Quoted: 不折叠 ({@link CaseFold#NONE}), 基于 collation 比较 ({@link CaseCompare#COLLATION_BASED}) 525 * <li>需要配合 {@link CollatorProvider} 使用,默认使用 SQL_Latin1_General_CP1_CI_AS (大小写不敏感) 526 * </ul> 527 * 528 * <p><strong>注意:</strong>SQL Server 的大小写行为完全由 collation 决定,无法简单折叠。 529 */ 530 public static IdentifierRules forSQLServer() { 531 return forSQLServer(null); 532 } 533 534 /** 535 * SQL Server / Azure SQL 标识符规则(根据 collation 决定大小写比较) 536 * 537 * <p>SQL Server 的大小写行为完全由 collation 决定: 538 * <ul> 539 * <li>CI collation (如 SQL_Latin1_General_CP1_CI_AS): 大小写不敏感 540 * <li>CS collation (如 SQL_Latin1_General_CP1_CS_AS): 大小写敏感 541 * </ul> 542 * 543 * <p>Unquoted 和 quoted 标识符都保留原始大小写(CaseFold.NONE), 544 * 比较方式由 collation 名称中的 _CI_ 或 _CS_ 决定。 545 * 546 * <p>A case-sensitive collation is exact ({@link CaseCompare#SENSITIVE}), except 547 * {@code Latin1_General_CS_AI}, whose accent equivalence was measured on the engine 548 * ({@link CollatorProvider#hasMeasuredAccentFolding}): it compares through the collator 549 * ({@link CaseCompare#COLLATION_BASED}), which folds accents by the measured table and then 550 * compares exactly (verified on SQL Server 2022: under {@code Latin1_General_CS_AI} the table 551 * {@code café} is found by {@code cafe}, not by {@code Cafe}). Every other collation, a 552 * case-insensitive one or {@code _BIN}/{@code _BIN2}, compares through the collator. A 553 * {@code _CS_AI} collation that was not measured stays exact, as before. 554 * 555 * @param collation SQL Server collation 名称(如 "SQL_Latin1_General_CP1_CS_AS"), 556 * 传 null 或空字符串使用默认(case-insensitive) 557 */ 558 public static IdentifierRules forSQLServer(String collation) { 559 CaseCompare compare = isCaseSensitiveCollation(collation) 560 ? CaseCompare.SENSITIVE 561 : CaseCompare.COLLATION_BASED; 562 return new IdentifierRules( 563 CaseFold.NONE, // unquoted 不折叠(保留原样) 564 compare, // 基于 collation 决定比较方式 565 CaseFold.NONE, // quoted 不折叠(保留原样) 566 compare // quoted 与 unquoted 一致 567 ); 568 } 569 570 /** 571 * 检查 SQL Server collation 名称是否为大小写敏感。 572 * 573 * <p>SQL Server collation 命名规则:名称中包含 "_CS_" 表示 case-sensitive, 574 * "_CI_" 表示 case-insensitive。 575 * 576 * @param collation collation 名称 577 * @return true 如果 collation 是 case-sensitive 578 */ 579 private static boolean isCaseSensitiveCollation(String collation) { 580 if (collation == null || collation.isEmpty()) { 581 return false; // 默认 case-insensitive 582 } 583 // SQL Server collation naming: _CS_ = case-sensitive, _CI_ = case-insensitive 584 String upper = collation.toUpperCase(java.util.Locale.ROOT); 585 boolean caseSensitive = upper.contains("_CS_") || upper.endsWith("_CS"); 586 // A case-sensitive but ACCENT-INSENSITIVE collation (_CS_AI) is not exact: on SQL Server 587 // 2022 with Latin1_General_CS_AI, `FROM cafe` finds the table café and `CREATE TABLE cafe` 588 // then fails as a duplicate. Exact comparison cannot say that, so the collator decides 589 // (CollatorProvider: the measured accent table, then exact). Only for the collation whose 590 // accents were measured; any other _CS_AI collation stays exact, as it was before. 591 return caseSensitive && !CollatorProvider.hasMeasuredAccentFolding(collation); 592 } 593 594 /** 595 * MySQL 标识符规则(table/schema names) 596 * 597 * <p><strong>实际数据库行为(MySQL 8.0+):</strong> 598 * <p>根据 {@code lower_case_table_names} 系统变量决定: 599 * <ul> 600 * <li>0 (Unix/Linux): 大小写敏感,保留原样 (CREATE TABLE MyTable → stored as MyTable, MyTable!=mytable) 601 * <li>1 (Windows): 存储为小写,比较不敏感 (CREATE TABLE MyTable → stored as mytable, MyTable=mytable=MYTABLE) 602 * <li>2 (macOS): 存储保留原样,比较不敏感 (CREATE TABLE MyTable → stored as MyTable, MyTable=mytable=MYTABLE) 603 * </ul> 604 * 605 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 606 * <ul> 607 * <li>columnCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new doesn't fold or folds to LOWER) 608 * <li>functionCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new doesn't fold) 609 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new behavior depends on lower_case_table_names) 610 * <li>catalogCollationCaseSensitive = {@code true} → ⚠️ <strong>PARTIAL</strong> (legacy preserved case, new is insensitive) 611 * </ul> 612 * 613 * <p><strong>测试用例影响:</strong> 614 * <ul> 615 * <li>✅ 新规则正确:MySQL 的行为确实依赖 lower_case_table_names 设置</li> 616 * <li>⚠️ 如果旧测试期望 "MyTable" → "MYTABLE",则在模式 1/2 下测试会失败</li> 617 * <li>⚠️ 模式 0: 应期望保留原样 (如 "MyTable" 保持为 "MyTable"),区分大小写</li> 618 * <li>⚠️ 模式 1: 应期望小写 (如 "MyTable" → "mytable"),不区分大小写</li> 619 * <li>⚠️ 模式 2: 应期望保留原样 (如 "MyTable" 保持为 "MyTable"),不区分大小写</li> 620 * </ul> 621 * 622 * <p><strong>IdentifierRules 配置:</strong> 623 * <ul> 624 * <li>模式 0: 不折叠 ({@link CaseFold#NONE}), 敏感 ({@link CaseCompare#SENSITIVE}) 625 * <li>模式 1: 折叠为小写 ({@link CaseFold#LOWER}), 不敏感 ({@link CaseCompare#INSENSITIVE}) 626 * <li>模式 2: 不折叠 ({@link CaseFold#NONE}), 不敏感 ({@link CaseCompare#INSENSITIVE}) 627 * <li>Quoted: 保留原样,但也不敏感 (MySQL 特殊行为) 628 * </ul> 629 * 630 * <p><strong>Since 2026-10-11 this is the shape the MySQL-like FORKS share</strong> (OceanBase, 631 * Doris, StarRocks), none verified on its own engine. 632 * {@link gudusoft.gsqlparser.EDbVendor#dbvmysql} itself uses {@link #forMySQLNative(int)}. 633 * 634 * @param lowerCaseTableNames {@code lower_case_table_names} 值(0, 1, 2) 635 */ 636 public static IdentifierRules forMySQL(int lowerCaseTableNames) { 637 if (lowerCaseTableNames == 0) { 638 // Unix/Linux: 大小写敏感 639 return new IdentifierRules( 640 CaseFold.NONE, 641 CaseCompare.SENSITIVE, 642 CaseFold.NONE, 643 CaseCompare.SENSITIVE 644 ); 645 } else { 646 // Windows/macOS: 比较不敏感 647 CaseFold fold = (lowerCaseTableNames == 1) ? CaseFold.LOWER : CaseFold.NONE; 648 return new IdentifierRules( 649 fold, // 根据设置决定是否折叠 650 CaseCompare.INSENSITIVE, // 比较不敏感 651 CaseFold.NONE, // quoted 保留原样 652 CaseCompare.INSENSITIVE // quoted 也不敏感(MySQL 特性) 653 ); 654 } 655 } 656 657 /** 658 * MySQL table / schema / table-alias names, as verified on MySQL 8.4.10, used by 659 * {@link gudusoft.gsqlparser.EDbVendor#dbvmysql}. 660 * 661 * <ul> 662 * <li>{@code lower_case_table_names = 0}: stored as given, case-sensitive (unchanged). 663 * <li>{@code 1}: every name, quoted or not, is lowered with MySQL's one-to-one per-character 664 * mapping ({@link CaseFold#LOWER_SIMPLE}); quoting does not create another name. 665 * {@code CREATE TABLE İ} is stored as {@code i}, and {@code FROM İ}, {@code FROM `İ`} and 666 * {@code FROM i} all find it. The quoted branch keeps its spelling as the stored form 667 * GSP reports but compares by the same mapping ({@link CaseCollapse#LOWER_SIMPLE}). 668 * <li>{@code 2}: stored as given, compared by the same lowercase mapping. 669 * </ul> 670 * 671 * <p>SQL-LR {@code mysql/rules-1}: cells {@code U_LOWER_SIMPLE_CI__Q_LOWER_SIMPLE_CI} and, 672 * for {@code lowerCaseTableNames = 0}, {@code U_NONE_CS__Q_NONE_CS}. 673 * 674 * @param lowerCaseTableNames {@code lower_case_table_names} 值(0, 1, 2) 675 */ 676 public static IdentifierRules forMySQLNative(int lowerCaseTableNames) { 677 if (lowerCaseTableNames == 0) { 678 return MYSQL_NATIVE_LCTN0; 679 } 680 return lowerCaseTableNames == 1 ? MYSQL_NATIVE_LCTN1 : MYSQL_NATIVE_LCTN2; 681 } 682 683 /** 684 * MySQL column, column-alias and index names, as verified on MySQL 8.4.10: kept as written and 685 * compared by the simple lowercase mapping ({@link CaseCollapse#LOWER_SIMPLE}), whatever 686 * {@code lower_case_table_names} is. {@code CREATE TABLE c1(`ſ` int, s int)} and 687 * {@code c2(`ς` int, `σ` int)} succeed: those are distinct columns, which Unicode case folding 688 * would merge. Index names follow the same rule ({@code INDEX Idx} then {@code ADD INDEX idx} 689 * fails with ERROR 1061 even under {@code lower_case_table_names = 0}). 690 */ 691 public static IdentifierRules forMySQLNativeColumn() { 692 return MYSQL_NATIVE_COLUMN; 693 } 694 695 /** 696 * MySQL function, procedure and routine-parameter names, as verified on MySQL 8.4.10: kept as 697 * written; two names are the same when their simple UPPERCASE forms are equal 698 * ({@link CaseCollapse#UPPER_SIMPLE}), whatever {@code lower_case_table_names} is. 699 * {@code CREATE FUNCTION `ı`()} is called by {@code SELECT i()}, and a later 700 * {@code CREATE FUNCTION i()} fails as already existing; {@code CREATE PROCEDURE pr(P int, p int)} 701 * fails with ERROR 1330 under {@code lower_case_table_names = 0}. 702 */ 703 public static IdentifierRules forMySQLNativeRoutine() { 704 return MYSQL_NATIVE_ROUTINE; 705 } 706 707 /** 708 * MySQL 列名规则(始终大小写不敏感) 709 * 710 * <p><strong>实际数据库行为(MySQL 8.0+):</strong> 711 * <ul> 712 * <li>列名始终大小写不敏感,不受 lower_case_table_names 影响 713 * <li>存储时保留原样,比较时不敏感 (SELECT MyColumn → stored as MyColumn, MyColumn=mycolumn) 714 * </ul> 715 * 716 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 717 * <ul> 718 * <li>columnCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new preserves case but is INSENSITIVE) 719 * </ul> 720 * 721 * <p>Since 2026-10-11 the shape of the forks only; MySQL itself uses 722 * {@link #forMySQLNativeColumn()}. 723 */ 724 public static IdentifierRules forMySQLColumn() { 725 return MYSQL_COLUMN; 726 } 727 728 /** 729 * MySQL 函数名规则(始终大小写不敏感) 730 * 731 * <p><strong>实际数据库行为(MySQL 8.0+):</strong> 732 * <ul> 733 * <li>函数名/存储过程名始终大小写不敏感 734 * <li>存储时保留原样,比较时不敏感 (与列名相同) 735 * </ul> 736 * 737 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 738 * <ul> 739 * <li>functionCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new preserves case but is INSENSITIVE) 740 * </ul> 741 * 742 * <p>Since 2026-10-11 the shape of the forks only; MySQL itself uses 743 * {@link #forMySQLNativeRoutine()}. 744 */ 745 public static IdentifierRules forMySQLRoutine() { 746 return forMySQLColumn(); 747 } 748 749 /** 750 * BigQuery 表名规则(大小写敏感) 751 * 752 * <p><strong>实际数据库行为(BigQuery Standard SQL):</strong> 753 * <ul> 754 * <li>Unquoted: 保留原样,比较敏感 (CREATE TABLE MyTable → stored as MyTable, MyTable!=mytable) 755 * <li>Quoted: 保留原样,比较敏感 (CREATE TABLE `MyTable` → stored as MyTable, MyTable!=mytable) 756 * <li>表名/dataset名/project名都是大小写敏感的</li> 757 * </ul> 758 * 759 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 760 * <ul> 761 * <li>tableCollationCaseSensitive = {@code false} → ❌ <strong>INCOMPATIBLE</strong> (legacy folded to UPPER, new preserves case and is SENSITIVE) 762 * <li>catalogCollationCaseSensitive = {@code false} → ❌ <strong>INCOMPATIBLE</strong> (legacy folded to UPPER, new preserves case and is SENSITIVE) 763 * </ul> 764 * 765 * <p><strong>测试用例影响:</strong> 766 * <ul> 767 * <li>✅ 新规则正确:BigQuery 表名确实是大小写敏感的</li> 768 * <li>⚠️ 如果旧测试期望 "MyTable" → "MYTABLE",则测试会失败</li> 769 * <li>⚠️ 应更新测试期望为保留原样 (如 "MyTable" 保持为 "MyTable")</li> 770 * <li>⚠️ 旧代码可能错误地匹配了不同大小写的表名,新代码会正确拒绝</li> 771 * </ul> 772 */ 773 public static IdentifierRules forBigQueryTable() { 774 return BIGQUERY_TABLE; 775 } 776 777 /** 778 * BigQuery 列名规则(大小写不敏感) 779 * 780 * <p><strong>实际数据库行为(BigQuery Standard SQL):</strong> 781 * <ul> 782 * <li>Unquoted: 保留原样,比较不敏感 (SELECT MyColumn → stored as MyColumn, MyColumn=mycolumn=MYCOLUMN) 783 * <li>Quoted: 保留原样,比较不敏感 (SELECT `MyColumn` → MyColumn=mycolumn) 784 * <li>列名是大小写不敏感的(与表名不同)</li> 785 * </ul> 786 * 787 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 788 * <ul> 789 * <li>columnCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new preserves case but is INSENSITIVE) 790 * </ul> 791 * 792 * <p><strong>测试用例影响:</strong> 793 * <ul> 794 * <li>✅ 新规则正确:BigQuery 列名确实是大小写不敏感的</li> 795 * <li>⚠️ 如果旧测试期望 "MyColumn" → "MYCOLUMN",则测试会失败</li> 796 * <li>⚠️ 应更新测试期望为保留原样 (如 "MyColumn" 保持为 "MyColumn")</li> 797 * <li>✅ 但比较时应忽略大小写 (MyColumn = mycolumn = MYCOLUMN)</li> 798 * </ul> 799 */ 800 public static IdentifierRules forBigQueryColumn() { 801 return BIGQUERY_COLUMN; 802 } 803 804 /** 805 * DB2 / Netezza / Exasol 标识符规则(与 Oracle 相同) 806 * 807 * <p><strong>实际数据库行为(DB2 11+):</strong> 808 * <ul> 809 * <li>Unquoted: 折叠为大写,比较不敏感 (与 Oracle 相同) 810 * <li>Quoted: 保留原样,比较敏感 811 * </ul> 812 * 813 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 814 * <ul> 815 * <li>DB2 tableCollationCaseSensitive = {@code true} → ❌ <strong>INCOMPATIBLE</strong> (legacy preserved case, new folds to UPPER) 816 * <li>DB2 catalogCollationCaseSensitive = {@code true} → ❌ <strong>INCOMPATIBLE</strong> (legacy preserved case, new folds to UPPER) 817 * <li>参见 {@link #forOracle()} 的详细说明 818 * </ul> 819 */ 820 public static IdentifierRules forDB2() { 821 return forOracle(); 822 } 823 824 /** 825 * Snowflake 标识符规则(与 Oracle 相同) 826 * 827 * <p><strong>实际数据库行为(Snowflake):</strong> 828 * <ul> 829 * <li>Unquoted: 折叠为大写,比较不敏感 (与 Oracle 相同) 830 * <li>Quoted: 保留原样,比较敏感 831 * </ul> 832 * 833 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 834 * <ul> 835 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (both fold to UPPER, COMPATIBLE) 836 * <li>columnCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (both fold to UPPER, COMPATIBLE) 837 * <li>参见 {@link #forOracle()} 的详细说明 838 * </ul> 839 */ 840 public static IdentifierRules forSnowflake() { 841 return forSnowflake(false); 842 } 843 844 /** 845 * Snowflake 标识符规则(根据 QUOTED_IDENTIFIERS_IGNORE_CASE 决定) 846 * 847 * <p>Snowflake 默认行为: 848 * <ul> 849 * <li>Unquoted: 折叠为大写,比较不敏感(与 Oracle 相同) 850 * <li>Quoted: 保留原样,比较敏感 851 * </ul> 852 * 853 * <p>当 QUOTED_IDENTIFIERS_IGNORE_CASE = TRUE 时: 854 * <ul> 855 * <li>Quoted 标识符也折叠为大写,比较不敏感 856 * </ul> 857 * 858 * @param quotedIdentifiersIgnoreCase Snowflake QUOTED_IDENTIFIERS_IGNORE_CASE 参数 859 */ 860 public static IdentifierRules forSnowflake(boolean quotedIdentifiersIgnoreCase) { 861 if (quotedIdentifiersIgnoreCase) { 862 // Quoted identifiers also fold to upper and are case-insensitive 863 return new IdentifierRules( 864 CaseFold.UPPER, 865 CaseCompare.INSENSITIVE, 866 CaseFold.UPPER, // quoted 也折叠为大写 867 CaseCompare.INSENSITIVE // quoted 也不敏感 868 ); 869 } 870 return forOracle(); // 默认:quoted 保留原样且敏感 871 } 872 873 /** 874 * SAP HANA 标识符规则(与 Oracle 相同) 875 * 876 * <p><strong>实际数据库行为(SAP HANA):</strong> 877 * <ul> 878 * <li>Unquoted: 折叠为大写,比较不敏感 (与 Oracle 相同) 879 * <li>Quoted: 保留原样,比较敏感 880 * </ul> 881 * 882 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 883 * <ul> 884 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (both fold to UPPER, COMPATIBLE) 885 * <li>参见 {@link #forOracle()} 的详细说明 886 * </ul> 887 */ 888 public static IdentifierRules forHANA() { 889 return forOracle(); 890 } 891 892 /** 893 * Presto / Trino 标识符规则 894 * 895 * <p><strong>实际数据库行为(Presto/Trino):</strong> 896 * <ul> 897 * <li>Unquoted: 折叠为小写,比较不敏感 (CREATE TABLE MyTable → stored as mytable) 898 * <li>Quoted: 保留原样,但与 unquoted 规则一致(比较时仍不敏感) 899 * </ul> 900 * 901 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 902 * <ul> 903 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new folds to LOWER) 904 * <li>columnCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new folds to LOWER) 905 * </ul> 906 * 907 * <p><strong>测试用例影响:</strong> 908 * <ul> 909 * <li>✅ 新规则正确:Presto/Trino 折叠为小写,quoted 标识符与 unquoted 规则一致</li> 910 * <li>⚠️ 如果旧测试期望 "MyTable" → "MYTABLE",则测试会失败</li> 911 * <li>⚠️ 应更新测试期望为小写 (如 "MyTable" → "mytable")</li> 912 * </ul> 913 * 914 * <p><strong>IdentifierRules 配置:</strong> 915 * <ul> 916 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 不敏感 ({@link CaseCompare#INSENSITIVE}) 917 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 与 unquoted 一致 ({@link CaseCompare#SAME_AS_UNQUOTED}) 918 * </ul> 919 */ 920 public static IdentifierRules forPresto() { 921 return PRESTO; 922 } 923 924 /** 925 * Vertica 标识符规则(与 Presto 相同) 926 * 927 * <p><strong>实际数据库行为(Vertica):</strong> 928 * <ul> 929 * <li>Unquoted: 折叠为小写,比较不敏感 (与 Presto 相同) 930 * <li>Quoted: 保留原样,但与 unquoted 规则一致 931 * </ul> 932 * 933 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 934 * <ul> 935 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new folds to LOWER) 936 * <li>参见 {@link #forPresto()} 的详细说明 937 * </ul> 938 */ 939 public static IdentifierRules forVertica() { 940 return forPresto(); 941 } 942 943 /** 944 * Hive / SparkSQL / Impala / Databricks 标识符规则 945 * 946 * <p><strong>实际数据库行为:</strong>反引号只允许名字里出现特殊字符和保留字,不改变大小写规则, 947 * 所以 {@code `Orders`}、{@code `ORDERS`} 与 {@code orders} 是同一个对象: 948 * <ul> 949 * <li>Hive (LanguageManual DDL): "Table names and column names are case insensitive" 950 * <li>Spark SQL (Identifiers): "Both regular identifiers and delimited identifiers are case-insensitive." 951 * <li>Impala (Overview of Impala Identifiers): t1 and T1 "always refer to the same table, 952 * regardless of quote characters"; names are folded to lowercase 953 * <li>Databricks (Identifiers): "Identifiers are case-insensitive when referenced." 954 * </ul> 955 * 956 * <p><strong>IdentifierRules 配置:</strong> 957 * <ul> 958 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 959 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 960 * </ul> 961 * Quoted 不折叠:Spark / Databricks 的列名保留大小写(元数据里的 {@code `Amount`} 显示为 962 * {@code Amount}),只是比较时不区分;折叠会把元数据的写法改成小写。 963 * 964 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 965 * <ul> 966 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new folds to LOWER) 967 * </ul> 968 */ 969 public static IdentifierRules forHive() { 970 return HIVE; 971 } 972 973 /** 974 * Teradata 标识符规则 975 * 976 * <p><strong>实际数据库行为:</strong>对象名不区分大小写,加不加双引号都一样;双引号只允许名字里 977 * 出现特殊字符和保留字。所以 {@code "ORDERS"}、{@code "Orders"} 与 {@code orders} 是同一个对象: 978 * <ul> 979 * <li>SAS/ACCESS Teradata naming conventions: "A name, even when enclosed in double quotation 980 * marks, is not case sensitive. For example, CUSTOMER is the same as customer." 981 * <li>Exasol Teradata migration guide: object names in Teradata are case insensitive 982 * </ul> 983 * 这对元数据尤其要紧:sqlflow-exporter 导出的 Teradata 名字全部带双引号 984 * ({@code '"' || TRIM(TABLENAME) || '"'}),SQL 里不带引号的引用必须能找到它们。 985 * 986 * <p><strong>IdentifierRules 配置:</strong> 987 * <ul> 988 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 989 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 990 * </ul> 991 * Quoted 不折叠:Teradata 按创建时的大小写存名字(元数据里的 {@code "ORDERS"} 显示为 992 * {@code ORDERS}),只是比较时不区分。 993 * 994 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 995 * <ul> 996 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new folds to LOWER) 997 * </ul> 998 */ 999 public static IdentifierRules forTeradata() { 1000 return TERADATA; 1001 } 1002 1003 /** 1004 * Athena 标识符规则(与 Presto 相同) 1005 * 1006 * <p><strong>实际数据库行为(AWS Athena):</strong> 1007 * <ul> 1008 * <li>Unquoted: 折叠为小写,比较不敏感 (与 Presto 相同,基于 Trino/Presto) 1009 * <li>Quoted: 保留原样,但与 unquoted 规则一致 1010 * </ul> 1011 * 1012 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 1013 * <ul> 1014 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new folds to LOWER) 1015 * <li>参见 {@link #forPresto()} 的详细说明 1016 * </ul> 1017 */ 1018 public static IdentifierRules forAthena() { 1019 return forPresto(); 1020 } 1021 1022 /** 1023 * GaussDB 标识符规则(与 PostgreSQL 相同) 1024 * 1025 * <p><strong>实际数据库行为(华为 GaussDB):</strong> 1026 * <ul> 1027 * <li>Unquoted: 折叠为小写,比较不敏感 (与 PostgreSQL 相同,基于 PostgreSQL) 1028 * <li>Quoted: 保留原样,比较敏感 1029 * </ul> 1030 * 1031 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 1032 * <ul> 1033 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> (legacy folded to UPPER, new folds to LOWER) 1034 * <li>参见 {@link #forPostgreSQL()} 的详细说明 1035 * </ul> 1036 */ 1037 public static IdentifierRules forGaussDB() { 1038 return forPostgreSQL(); 1039 } 1040 1041 /** 1042 * Databricks 标识符规则(与 Hive 相同) 1043 * 1044 * <p><strong>实际数据库行为(Databricks "Identifiers"):</strong> 1045 * "Identifiers are case-insensitive when referenced.",反引号标识符也一样, 1046 * 按 UTF8_LCASE collation 比较。 1047 * 1048 * <p><strong>IdentifierRules 配置:</strong>见 {@link #forHive()} 1049 * (unquoted 与 quoted 都折叠为小写,比较不敏感)。 1050 */ 1051 public static IdentifierRules forDatabricks() { 1052 return forHive(); 1053 } 1054 1055 /** 1056 * Apache Flink SQL 标识符规则 1057 * 1058 * <p><strong>实际数据库行为(Flink SQL Queries Overview):</strong>"Flink SQL uses a lexical 1059 * policy for identifier (table, attribute, function names) similar to Java: The case of 1060 * identifiers is preserved whether or not they are quoted. After which, identifiers are 1061 * matched case-sensitively." 反引号只允许名字里出现非字母数字字符,不改变大小写规则。 1062 * 所以 {@code `userId`} 与 {@code userId} 是同一个名字,而 {@code userId} 与 {@code userid} 1063 * 是两个名字。 1064 * 1065 * <p><strong>IdentifierRules 配置:</strong> 1066 * <ul> 1067 * <li>Unquoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 1068 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 1069 * </ul> 1070 * 1071 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 1072 * <ul> 1073 * <li>tableCollationCaseSensitive = {@code false} → ❌ <strong>INCOMPATIBLE</strong> (legacy compared case-insensitively) 1074 * </ul> 1075 */ 1076 public static IdentifierRules forFlink() { 1077 return FLINK; 1078 } 1079 1080 /** 1081 * Apache Doris 标识符规则(与 MySQL 相同) 1082 * 1083 * <p><strong>行为说明:</strong> 1084 * <ul> 1085 * <li>Doris 是 MySQL 兼容的 OLAP 数据库 1086 * <li>标识符处理遵循 MySQL 的规则 1087 * <li>表名/数据库名大小写敏感性取决于操作系统(类似 MySQL lower_case_table_names) 1088 * <li>列名始终大小写不敏感 1089 * </ul> 1090 * 1091 * <p><strong>与 Legacy TSQLEnv 兼容性:</strong> 1092 * <ul> 1093 * <li>tableCollationCaseSensitive = {@code false} → ⚠️ <strong>PARTIAL</strong> 1094 * <li>catalogCollationCaseSensitive = {@code true} → ✅ <strong>COMPATIBLE</strong> 1095 * </ul> 1096 * 1097 * @param lowerCaseTableNames {@code lower_case_table_names} 值(0, 1, 2),默认使用 1 1098 */ 1099 public static IdentifierRules forDoris(int lowerCaseTableNames) { 1100 return forMySQL(lowerCaseTableNames); 1101 } 1102 1103 /** 1104 * Apache Doris 标识符规则(默认配置,相当于 lower_case_table_names=1) 1105 * 1106 * <p><strong>行为说明:</strong> 1107 * <ul> 1108 * <li>Doris 是 MySQL 兼容的 OLAP 数据库 1109 * <li>默认情况下,表名折叠为小写,比较不敏感 1110 * </ul> 1111 */ 1112 public static IdentifierRules forDoris() { 1113 return forMySQL(1); // Default to lowercase folding, case-insensitive 1114 } 1115 1116 /** 1117 * Apache Doris 列名规则(与 MySQL 列名相同,始终大小写不敏感) 1118 */ 1119 public static IdentifierRules forDorisColumn() { 1120 return forMySQLColumn(); 1121 } 1122 1123 /** 1124 * StarRocks 标识符规则(带参数,相当于 MySQL 的 lower_case_table_names) 1125 * 1126 * <p><strong>行为说明:</strong> 1127 * <ul> 1128 * <li>StarRocks 是 Doris 的分支,MySQL 兼容的 OLAP 数据库 1129 * <li>与 Doris 和 MySQL 使用相同的标识符规则 1130 * </ul> 1131 * 1132 * @param lowerCaseTableNames {@code lower_case_table_names} 值(0, 1, 2),默认使用 1 1133 */ 1134 public static IdentifierRules forStarrocks(int lowerCaseTableNames) { 1135 return forMySQL(lowerCaseTableNames); 1136 } 1137 1138 /** 1139 * StarRocks 标识符规则(默认配置,相当于 lower_case_table_names=1) 1140 * 1141 * <p><strong>行为说明:</strong> 1142 * <ul> 1143 * <li>StarRocks 是 Doris 的分支,MySQL 兼容的 OLAP 数据库 1144 * <li>默认情况下,表名折叠为小写,比较不敏感 1145 * </ul> 1146 */ 1147 public static IdentifierRules forStarrocks() { 1148 return forMySQL(1); // Default to lowercase folding, case-insensitive 1149 } 1150 1151 /** 1152 * StarRocks 列名规则(与 MySQL 列名相同,始终大小写不敏感) 1153 */ 1154 public static IdentifierRules forStarrocksColumn() { 1155 return forMySQLColumn(); 1156 } 1157 1158 /** 1159 * SQLite 标识符规则 1160 * 1161 * <p><strong>实际数据库行为(SQLite 3.45.1 实测):</strong>名字不区分大小写,加不加引号都一样, 1162 * 并保留定义时的大小写。{@code CREATE TABLE "Orders" ("Amount" INT)} 之后, 1163 * {@code SELECT amount FROM orders}、{@code SELECT "amount" FROM "orders"} 都能查到, 1164 * 再建 {@code "orders"} 会报 {@code table "orders" already exists}。 1165 * 1166 * <p><strong>IdentifierRules 配置:</strong> 1167 * <ul> 1168 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1169 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1170 * </ul> 1171 */ 1172 public static IdentifierRules forSqlite() { 1173 return SQLITE; 1174 } 1175 1176 /** 1177 * Sybase ASE 标识符规则 1178 * 1179 * <p><strong>实际数据库行为(SAP IQ "Case-sensitivity of Identifiers" 对 ASE 的说明):</strong> 1180 * "In Adaptive Server Enterprise, the case-sensitivity of identifiers follows the 1181 * case-sensitivity of the data",即由服务器安装时的 sort order 决定;双引号只允许名字里出现 1182 * 特殊字符和保留字,不改变大小写规则。GSP 按大小写不敏感的服务器建模(见 1183 * {@code db_collation.md} §2.12,与旧 TSQLEnv 一致),所以带不带引号都不区分大小写。 1184 * binary sort order(区分大小写)的服务器请用 1185 * {@link IdentifierProfile#builder(gudusoft.gsqlparser.EDbVendor)} 配置 NONE/SENSITIVE。 1186 * 1187 * <p><strong>IdentifierRules 配置:</strong> 1188 * <ul> 1189 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1190 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1191 * </ul> 1192 */ 1193 public static IdentifierRules forSybase() { 1194 return SYBASE; 1195 } 1196 1197 /** 1198 * SAP IQ / SQL Anywhere 标识符规则 1199 * 1200 * <p><strong>实际数据库行为:</strong>SAP IQ "Case-sensitivity of Identifiers":"SAP Sybase IQ does not 1201 * support case-sensitive identifiers.";SQL Anywhere "Case Sensitivity":SQL Anywhere does not 1202 * support case sensitive identifiers。加不加引号都不区分大小写,并保留定义时的大小写。 1203 * 1204 * <p><strong>IdentifierRules 配置:</strong> 1205 * <ul> 1206 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1207 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1208 * </ul> 1209 */ 1210 public static IdentifierRules forSqlAnywhere() { 1211 return SQL_ANYWHERE; 1212 } 1213 1214 /** 1215 * Microsoft Access 标识符规则 1216 * 1217 * <p><strong>实际数据库行为:</strong>Access 的表名、字段名不区分大小写;方括号只用来引用含特殊字符 1218 * 或保留字的名字,不改变大小写规则,并保留定义时的大小写。 1219 * 1220 * <p><strong>IdentifierRules 配置:</strong> 1221 * <ul> 1222 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1223 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1224 * </ul> 1225 */ 1226 public static IdentifierRules forAccess() { 1227 return ACCESS; 1228 } 1229 1230 /** 1231 * DAX 标识符规则 1232 * 1233 * <p><strong>实际行为(Microsoft Learn "DAX syntax"):</strong>"All object names are 1234 * case-insensitive; for example, the names SALES and Sales would represent the same table." 1235 * 单引号表名 {@code 'Sales'}、方括号列名 {@code [Amount]} 也一样。 1236 * 1237 * <p><strong>IdentifierRules 配置:</strong> 1238 * <ul> 1239 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1240 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1241 * </ul> 1242 */ 1243 public static IdentifierRules forDax() { 1244 return DAX; 1245 } 1246 1247 /** 1248 * MDX 标识符规则 1249 * 1250 * <p><strong>实际行为:</strong>Analysis Services 默认不区分大小写(SQLBI "Letter case-sensitivity in 1251 * DAX, Power BI and Analysis Services":Power BI and Analysis Services are case-insensitive by 1252 * default),方括号界定的名字 {@code [Measures].[Sales]} 与 {@code [measures].[sales]} 相同。 1253 * 1254 * <p><strong>IdentifierRules 配置:</strong> 1255 * <ul> 1256 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1257 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1258 * </ul> 1259 */ 1260 public static IdentifierRules forMdx() { 1261 return MDX; 1262 } 1263 1264 /** 1265 * Power Query M 标识符规则 1266 * 1267 * <p><strong>实际行为(Microsoft Learn "Power Query M formula language"):</strong>M 区分大小写, 1268 * 变量名、函数名都是("Because the Power Query M formula language is case sensitive, comparisons 1269 * between items that are identical but have different cases results in identifying the items 1270 * as being different")。{@code #"Name"} 形式只允许名字里出现空格等字符,不改变大小写规则。 1271 * 1272 * <p><strong>IdentifierRules 配置:</strong> 1273 * <ul> 1274 * <li>Unquoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 1275 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 1276 * </ul> 1277 */ 1278 public static IdentifierRules forPowerQuery() { 1279 return POWER_QUERY; 1280 } 1281 1282 /** 1283 * DuckDB 标识符规则 1284 * 1285 * <p><strong>实际数据库行为(DuckDB "Keywords and Identifiers"):</strong>"Identifiers in DuckDB are 1286 * always case-insensitive, similarly to PostgreSQL. However, unlike PostgreSQL (and some other 1287 * major SQL implementations), DuckDB also treats quoted identifiers as case-insensitive." 1288 * 同时保留定义时的大小写。 1289 * 1290 * <p><strong>IdentifierRules 配置:</strong> 1291 * <ul> 1292 * <li>Unquoted: 折叠为小写 ({@link CaseFold#LOWER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1293 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1294 * </ul> 1295 */ 1296 public static IdentifierRules forDuckDB() { 1297 return DUCKDB; 1298 } 1299 1300 /** 1301 * Firebird 标识符规则(与 Oracle 相同) 1302 * 1303 * <p><strong>实际数据库行为(Firebird Language Reference, "Identifiers"):</strong>不带引号的名字 1304 * 按大写存储,带引号的名字按原样存储、区分大小写。所以 {@code "FULLNAME"} 与 {@code FULLNAME}、 1305 * {@code fullname}、{@code FullName} 是同一个名字,而 {@code "FullName"} 与 {@code FullName} 不是。 1306 * 1307 * <p><strong>IdentifierRules 配置:</strong> 1308 * <ul> 1309 * <li>Unquoted: 折叠为大写 ({@link CaseFold#UPPER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1310 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 1311 * </ul> 1312 */ 1313 public static IdentifierRules forFirebird() { 1314 return forOracle(); 1315 } 1316 1317 /** 1318 * ANSI / ISO SQL 标识符规则(SQL-92 起的标准,与 Oracle 相同) 1319 * 1320 * <p><strong>标准行为:</strong>regular identifier 不区分大小写,按大写处理(折叠为大写); 1321 * delimited identifier 保留原样、区分大小写。所以 {@code "ORDERS"} 与 {@code orders} 是同一个名字, 1322 * 而 {@code "orders"} 与 {@code orders} 不是。 1323 * 1324 * <p><strong>IdentifierRules 配置:</strong> 1325 * <ul> 1326 * <li>Unquoted: 折叠为大写 ({@link CaseFold#UPPER}), 比较不敏感 ({@link CaseCompare#INSENSITIVE}) 1327 * <li>Quoted: 保留原样 ({@link CaseFold#NONE}), 比较敏感 ({@link CaseCompare#SENSITIVE}) 1328 * </ul> 1329 */ 1330 public static IdentifierRules forAnsi() { 1331 return ANSI; 1332 } 1333 1334 /** 1335 * 通用规则({@code dbvgeneric}):没有具体方言时按 SQL 标准,见 {@link #forAnsi()} 1336 */ 1337 public static IdentifierRules forGeneric() { 1338 return forAnsi(); 1339 } 1340 1341 // ===== Value semantics ===== 1342 1343 /** 1344 * This cell with {@link CaseCompare#SAME_AS_UNQUOTED} resolved — the form that identifies a 1345 * <em>rule domain</em>, so two cells that behave identically are not treated as two domains 1346 * merely because one spells its quoted compare as "same as unquoted". 1347 * 1348 * @return {@code this} when nothing needs resolving, else the resolved equivalent 1349 */ 1350 public IdentifierRules resolvedCell() { 1351 if (quotedCompare != CaseCompare.SAME_AS_UNQUOTED 1352 || unquotedCompare == CaseCompare.SAME_AS_UNQUOTED) { 1353 // Nothing to resolve, or a degenerate cell whose unquoted branch is itself the 1354 // indirection: IdentifierService.effectiveCompare leaves that as written, so leave it. 1355 return this; 1356 } 1357 IdentifierRules cached = resolved; 1358 if (cached == null) { 1359 cached = new IdentifierRules(unquotedFold, unquotedCompare, quotedFold, unquotedCompare, 1360 collapse, maxUtf8Bytes); 1361 resolved = cached; 1362 } 1363 return cached; 1364 } 1365 1366 private transient volatile IdentifierRules resolved; 1367 1368 @Override 1369 public boolean equals(Object o) { 1370 if (this == o) return true; 1371 if (o == null || getClass() != o.getClass()) return false; 1372 IdentifierRules that = (IdentifierRules) o; 1373 return unquotedFold == that.unquotedFold 1374 && unquotedCompare == that.unquotedCompare 1375 && quotedFold == that.quotedFold 1376 && quotedCompare == that.quotedCompare 1377 && collapse == that.collapse 1378 && maxUtf8Bytes == that.maxUtf8Bytes; 1379 } 1380 1381 @Override 1382 public int hashCode() { 1383 int h = Objects.hash(unquotedFold, unquotedCompare, quotedFold, quotedCompare); 1384 if (!hasDefaultExtensions()) { 1385 h = 31 * h + Objects.hash(collapse, maxUtf8Bytes); 1386 } 1387 return h; 1388 } 1389 1390 /** True when the cell uses neither a non-default {@link #collapse} nor truncation. */ 1391 boolean hasDefaultExtensions() { 1392 return collapse == CaseCollapse.SIMPLE_FOLD && maxUtf8Bytes == 0; 1393 } 1394 1395 // ===== toString 方法(用于调试) ===== 1396 1397 @Override 1398 public String toString() { 1399 if (hasDefaultExtensions()) { 1400 return String.format("IdentifierRules{unquoted=%s/%s, quoted=%s/%s}", 1401 unquotedFold, unquotedCompare, quotedFold, quotedCompare); 1402 } 1403 return String.format("IdentifierRules{unquoted=%s/%s, quoted=%s/%s, collapse=%s, maxUtf8Bytes=%d}", 1404 unquotedFold, unquotedCompare, quotedFold, quotedCompare, collapse, maxUtf8Bytes); 1405 } 1406}