public class IdentifierService extends Object
提供所有标识符规范化和比较的统一入口。
为什么需要它(面向入门者):
不同数据库对名称的大小写和引号行为差别很大(是否区分大小写、是否折叠为大/小写、是否受 collation 影响)。
如果在业务代码里到处手写 toLowerCase/equalsIgnoreCase,很容易出错且难以维护。
本服务将“如何折叠与如何比较”的规则集中在一个地方,确保全局一致、可扩展、易测试。
与其它类的关系:
IdentifierRules:一张“规则卡片”(策略),描述未引号/带引号的折叠(fold)与比较(compare)。IdentifierProfile:一个“厂商档案”,为某个数据库厂商打包不同对象组(表/列/函数)的规则,并携带厂商开关(如 MySQL lower_case_table_names、SQL Server collation)。normalize/areEqual/keyForMap 接口(门面)。设计理念与收益:
keyForMap(java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) 先做规范化,Map 查找 O(1);SQL Server 经 CollatorProvider 做 collation 比较。使用了哪些设计模式:
IdentifierRules 即为可替换的规则策略。IdentifierRules.forOracle()/forPostgreSQL()、IdentifierProfile.forVendor(...) 产出预设策略组合。IdentifierProfile 与可选 CollatorProvider,便于替换与测试。CollatorProvider 解耦 SQL Server 的 collation 依赖。关键约束(必须遵守):
keyForMap(java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)areEqual(java.lang.String, java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)String.toUpperCase/toLowerCase/equalsIgnoreCase使用示例:
IdentifierService service = new IdentifierService(profile, collatorProvider);
// 规范化标识符(用于索引键)
String key = service.normalize("MyTable", ESQLDataObjectType.dotTable);
// Oracle: "MYTABLE", PostgreSQL: "mytable", ClickHouse: "MyTable"
// 比较两个标识符
boolean eq = service.areEqual("MyTable", "MYTABLE", ESQLDataObjectType.dotTable);
// Oracle: true, ClickHouse: false
// 构造索引键
String mapKey = service.keyForMap("MyTable", ESQLDataObjectType.dotTable);
| Modifier and Type | Class and Description |
|---|---|
static class |
IdentifierService.MixedKindProfileException
Typed rejection for mixed-kind custom profiles (one branch
collation-based, one fold-based — the cell shape
areEqual(java.lang.String, java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)'s
mixed-payload-kind guard never key-matches). |
| Constructor and Description |
|---|
IdentifierService(IdentifierProfile profile,
CollatorProvider collatorProvider)
构造标识符服务
|
| Modifier and Type | Method and Description |
|---|---|
ApproximateIdentifierKey |
approximateKeyV1(ESQLDataObjectType objectType,
String name,
IdentifierInputForm form)
Mint the best-effort key that serves EVERY rule cell, including the
collation-based and mixed-kind cells
persistentKeyV1(gudusoft.gsqlparser.sqlenv.ESQLDataObjectType, java.lang.String, gudusoft.gsqlparser.sqlenv.IdentifierInputForm) rejects:
quote-strip (lenient — a malformed quoted spelling keeps its identity
as written, mirroring areEqual(java.lang.String, java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)'s totality) plus pinned simple
case-fold collapse. |
boolean |
areEqual(String ident1,
String ident2,
ESQLDataObjectType objectType)
比较两个标识符是否相等(P0d.1:canonical-key equality)。
|
static boolean |
areEqualStatic(EDbVendor dbVendor,
ESQLDataObjectType objectType,
String ident1,
String ident2)
High-performance static identifier comparison method with caching
|
void |
assertSingleSegmentOrThrow(String identifier,
ESQLDataObjectType objectType)
断言标识符为单段,否则抛出异常
|
String |
buildCompositeKey(List<String> segments,
ESQLDataObjectType objectType)
构造复合键(使用长度前缀编码避免冲突)
|
String |
buildCompositeKey(String qualifiedName,
ESQLDataObjectType objectType)
构造复合键(从完整限定名)
|
CanonKey |
canonKey(String identifier,
ESQLDataObjectType objectType)
Build the canonical key of a single identifier segment (unification plan stage P0c).
|
static CanonKey |
canonKeyStatic(EDbVendor dbVendor,
ESQLDataObjectType objectType,
String identifier)
Static
canonKey(java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) accessor over the cached default-flags service per vendor
(same cache areEqualStatic(gudusoft.gsqlparser.EDbVendor, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType, java.lang.String, java.lang.String) uses, so key equality mirrors that relation). |
boolean |
canUseCompositeKey(String qualifiedName)
判断是否可以使用复合键快速路径
|
String |
describePolicyV1(ESQLDataObjectType objectType)
Human-readable rendering of the V1 policies for one object type — a
diagnostic, explicitly NOT the identity (
policyId is the hash
of the binary descriptor), so this formatting can improve without
rotating any stored key. |
List<String> |
expandVendorSpecific(List<String> segments,
EDbVendor vendor)
厂商级预处理(展开特殊语法)
|
List<String> |
expandVendorSpecific(List<String> segments,
EDbVendor vendor,
String defaultSchema)
厂商级预处理(展开特殊语法)
|
CollatorProvider |
getCollatorProvider()
获取 Collator 提供者
|
IdentifierProfile |
getProfile()
获取标识符配置档案
|
String |
keyForMap(String identifier,
ESQLDataObjectType objectType)
为 Map 索引构造键(单个标识符段)
|
List<String> |
keysForHierarchy(String qualifiedName,
List<ESQLDataObjectType> partTypes)
生成层级索引所需的段级键列表
|
String |
normalize(String identifier,
ESQLDataObjectType objectType)
规范化标识符(去引号 + 大小写折叠)
|
String |
normalizeQualifiedName(String qualifiedName,
ESQLDataObjectType objectType)
规范化完整限定名(等价于 SQLUtil.getIdentifierNormalName)
|
String |
normalizeSegment(String segment,
ESQLDataObjectType objectType)
规范化单个段(去引号 + 大小写折叠)
|
static String |
normalizeStatic(EDbVendor dbVendor,
ESQLDataObjectType objectType,
String identifier)
High-performance static normalize method with caching
|
List<String> |
parseQualifiedName(String qualifiedName)
解析完整限定名为段列表
|
PersistentIdentifierKey |
persistentKeyV1(ESQLDataObjectType objectType,
String name,
IdentifierInputForm form)
Mint the persistable canonical key of one identifier segment.
|
public IdentifierService(IdentifierProfile profile, CollatorProvider collatorProvider)
profile - 厂商标识符配置档案collatorProvider - Collator 提供者(SQL Server 专用,可为 null)public static String normalizeStatic(EDbVendor dbVendor, ESQLDataObjectType objectType, String identifier)
This method provides a convenient static interface for identifier normalization while leveraging a pre-populated cache of IdentifierService instances.
Performance characteristics:
Usage example:
String normalized = IdentifierService.normalizeStatic(
EDbVendor.dbvoracle,
ESQLDataObjectType.dotTable,
"MyTable"
);
// Result: "MYTABLE"
dbVendor - database vendorobjectType - object type (table, column, schema, etc.)identifier - identifier to normalize (may be quoted)public static boolean areEqualStatic(EDbVendor dbVendor, ESQLDataObjectType objectType, String ident1, String ident2)
This method provides a convenient static interface for identifier comparison while leveraging a pre-populated cache of IdentifierService instances.
Performance characteristics:
Usage example:
boolean equal = IdentifierService.areEqualStatic(
EDbVendor.dbvoracle,
ESQLDataObjectType.dotTable,
"MyTable",
"MYTABLE"
);
// Result: true (Oracle is case-insensitive for unquoted identifiers)
dbVendor - database vendorobjectType - object type (table, column, schema, etc.)ident1 - first identifier to compare (may be quoted)ident2 - second identifier to compare (may be quoted)public static CanonKey canonKeyStatic(EDbVendor dbVendor, ESQLDataObjectType objectType, String identifier)
canonKey(java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) accessor over the cached default-flags service per vendor
(same cache areEqualStatic(gudusoft.gsqlparser.EDbVendor, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType, java.lang.String, java.lang.String) uses, so key equality mirrors that relation).public String normalize(String identifier, ESQLDataObjectType objectType)
用于构造索引键,确保相同语义的标识符生成相同的键。
identifier - 原始标识符(可能带引号)objectType - 对象类型public boolean areEqual(String ident1, String ident2, ESQLDataObjectType objectType)
Defined as canonKey(ident1).equals(canonKey(ident2)), which makes this an
equivalence relation by construction. The previous rule selection ("quoted compare if
EITHER operand is quoted" over quote-stripped-but-unfolded text) was an OR-relation and
non-transitive on quoted-case-preserving dialects: Oracle foo ~ "foo" and
foo ~ "FOO" but "foo" ≁ "FOO". Under the canonical relation each operand
folds by its own quote state (unquoted foo → FOO matches quoted
"FOO", not quoted "foo"), matching how the names are actually stored.
ident1 - 标识符 1ident2 - 标识符 2objectType - 对象类型public String keyForMap(String identifier, ESQLDataObjectType objectType)
注意:此方法仅用于分层索引的单段键,不用于复合键。
对于 COLLATION_BASED(SQL Server),不做 fold,返回原始标识符 (后续通过桶+Collator 比较)。
identifier - 标识符objectType - 对象类型public CanonKey canonKey(String identifier, ESQLDataObjectType objectType)
Fold-based rules use the keyForMap(java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) text as the payload; collation-based
rules (SQL Server / Azure) use CollationKey bytes from the SAME
collator areEqual(java.lang.String, java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) compares with, so key equality mirrors that collator. When
a collation-based profile has no CollatorProvider, areEqual falls back
to comparing caseCollapse(java.lang.String) payloads (pinned Unicode simple case folding since
U5); the key mirrors that exactly by carrying the collapsed decoded text.
NOTE: since P0d.1, areEqual(java.lang.String, java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) IS canonical-key equality. The legacy
TSQLEnv.compareIdentifier façade migrates to this relation in P0d.2; until
then the P0c equivalence proof measures that remaining engine's distance from it.
identifier - single identifier segment (may be quoted); null returns nullobjectType - kind of database object the name refers topublic PersistentIdentifierKey persistentKeyV1(ESQLDataObjectType objectType, String name, IdentifierInputForm form)
Contract (no carve-out). For two
IdentifierInputForm.SQL_LEXICAL inputs a, b
under the same service and object type:
persistentKeyV1(type, a, SQL_LEXICAL).equals(persistentKeyV1(type, b, SQL_LEXICAL))
⟺ areEqual(a, b, type).
IdentifierInputForm.CATALOG_STORED inputs join the same
equivalence domain: a stored name keys equal to the lexical spelling
that produced it (Oracle stored MixedCase ≡ lexical
"MixedCase"; stored FOO ≡ lexical foo; MySQL
mode-1 stored straße ≡ bare lexical straße). In cells
whose unquoted branch folds while the quoted branch compares
insensitively without folding (MySQL table names, Presto family), the
unquoted fold and the quoted collapse are DIFFERENT maps wherever full
folding and simple folding disagree (final sigma: ΟΣ lowers to
ος while the collapse maps ς to σ) — that
asymmetry is areEqual(java.lang.String, java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)'s own U5-certified branch semantics, and
stored names side with the fold-image (the form the catalog actually
holds), not with the quoted collapse. Keys are stable across JVMs and
JDK versions — folding is pinned (GspCaseData), never JVM case
tables — and the policyId
rotates exactly when the resolved equivalence behavior changes.
Payload semantics. SQL_LEXICAL: strict codec decode for
recognized quoted spellings (IdentifierCodec.decodeLexical(gudusoft.gsqlparser.sqlenv.IdentifierProfile, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType, java.lang.String) —
malformed input throws IdentifierCodec.MalformedIdentifierException;
this API never guesses identity), then the quote-state branch's fold,
then collapse when the branch compares INSENSITIVE without folding.
CATALOG_STORED: no recognition, no decode. SENSITIVE stored-name
domains keep the stored text; INSENSITIVE domains take the cell's fold
when any branch folds (idempotent on real catalog data — stored names
ARE fold-images) and the case-collapse only in never-folding cells.
(Refines the plan's "INSENSITIVE cells collapse / SENSITIVE cells keep
the stored text": in folding cells the collapse is the wrong
canonicalizer for stored names — see persistentPayloadV1 — and for
folding quoted branches — Snowflake
QUOTED_IDENTIFIERS_IGNORE_CASE — the fold keeps cross-form
equality intact.)
Rejections — before descriptor construction AND before any
decode/payload work. Cells with both branches resolving to
COLLATION_BASED throw UnsupportedOperationException naming the
collation (collator weights are not portably reproducible); mixed-kind
custom profiles (one branch collation-based, one fold-based — the
areEqual(java.lang.String, java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) mixed-payload-kind guard case) throw the dedicated
subtype IdentifierService.MixedKindProfileException — the biconditional's scope
is fold/fold cells. approximateKeyV1(gudusoft.gsqlparser.sqlenv.ESQLDataObjectType, java.lang.String, gudusoft.gsqlparser.sqlenv.IdentifierInputForm) serves both.
Null name returns null. The empty string returns a defined
key (empty payload, real policyId): degenerate but stable.
objectType - kind of database object the name refers toname - single identifier segmentform - how the string reached the caller — lexical SQL text or a
catalog-stored name; the forms are not interchangeablepublic ApproximateIdentifierKey approximateKeyV1(ESQLDataObjectType objectType, String name, IdentifierInputForm form)
persistentKeyV1(gudusoft.gsqlparser.sqlenv.ESQLDataObjectType, java.lang.String, gudusoft.gsqlparser.sqlenv.IdentifierInputForm) rejects:
quote-strip (lenient — a malformed quoted spelling keeps its identity
as written, mirroring areEqual(java.lang.String, java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType)'s totality) plus pinned simple
case-fold collapse. CATALOG_STORED input is collapsed as-is.
Claims NO areEqual biconditional — see
ApproximateIdentifierKey. The distinct type and the distinct
ak1- policy namespace guarantee these keys can never be stored
or matched where exact keys are expected.
objectType - kind of database object the name refers toname - single identifier segmentform - lexical SQL text or catalog-stored namepublic String describePolicyV1(ESQLDataObjectType objectType)
policyId is the hash
of the binary descriptor), so this formatting can improve without
rotating any stored key.objectType - kind of database objectpublic boolean canUseCompositeKey(String qualifiedName)
条件(必须全部满足):
qualifiedName - 完整限定名(如 "db.schema.table")public String buildCompositeKey(List<String> segments, ESQLDataObjectType objectType)
格式: len1#segment1|len2#segment2|len3#segment3|objectType
例如: "3#db1|6#schema|5#table|dotTable"
优势:避免分隔符冲突(标识符可能包含 '.' 或 '|')
segments - 标识符段列表objectType - 对象类型public String buildCompositeKey(String qualifiedName, ESQLDataObjectType objectType)
qualifiedName - 完整限定名(如 "db.schema.table")objectType - 对象类型public List<String> parseQualifiedName(String qualifiedName)
包装 SQLUtil#parseNames(String, EDbVendor) 以支持厂商特定解析:
qualifiedName - 完整限定名(如 "db.schema.table" 或 "db..table")public List<String> expandVendorSpecific(List<String> segments, EDbVendor vendor)
处理厂商特定语法:
segments - 原始段列表vendor - 数据库厂商public List<String> expandVendorSpecific(List<String> segments, EDbVendor vendor, String defaultSchema)
处理厂商特定语法:
segments - 原始段列表vendor - 数据库厂商defaultSchema - 默认 schema,如果为 null 则使用 "dbo"public String normalizeSegment(String segment, ESQLDataObjectType objectType)
与 normalize(String, ESQLDataObjectType) 类似,但语义明确为"单段"处理。
segment - 单个段标识符objectType - 对象类型public String normalizeQualifiedName(String qualifiedName, ESQLDataObjectType objectType)
处理流程:
parseQualifiedName(String)
expandVendorSpecific(List, EDbVendor)
normalizeSegment(String, ESQLDataObjectType)
qualifiedName - 完整限定名objectType - 最终对象类型(dotTable/dotColumn/dotSchema 等)public List<String> keysForHierarchy(String qualifiedName, List<ESQLDataObjectType> partTypes)
用于分层索引(hierarchical index),确保各层键由 keyForMap(java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) 统一生成。
注意:调用前应先调用 parseQualifiedName(String) 和
expandVendorSpecific(List, EDbVendor) 完成解析与展开。
qualifiedName - 完整限定名partTypes - 各段对应的对象类型列表(长度应与段数一致)IllegalArgumentException - 如果段数与类型数不匹配public void assertSingleSegmentOrThrow(String identifier, ESQLDataObjectType objectType)
用于在 keyForMap(java.lang.String, gudusoft.gsqlparser.sqlenv.ESQLDataObjectType) 入口处确保输入为单段标识符(不包含 '.' 分隔符)。
如果 TBaseType.ALLOW_MULTI_SEGMENT_IN_KEY 为 true(兼容模式),
则仅记录警告日志而不抛异常。
identifier - 待检查的标识符IllegalArgumentException - 如果标识符包含多段且未启用兼容模式public IdentifierProfile getProfile()
public CollatorProvider getCollatorProvider()