001package gudusoft.gsqlparser.sqlenv; 002 003/** 004 * Maps a component's semantic role to the {@link ESQLDataObjectType} whose comparison and quoting 005 * rules apply (stage S0R of docs/refactor/identifier_identity_display_separation_plan.md). 006 * 007 * <h3>Why role and comparison type are two different things</h3> 008 * {@link IdentifierComponentRole} answers "what IS this component" — a server, a schema, the base 009 * column. {@link ESQLDataObjectType} answers "under which rules is it compared and quoted". They 010 * are not the same question and must not be the same enum: a role set needs SERVER and 011 * STRUCT_FIELD, which no comparison domain of its own exists for, while the comparison type must 012 * stay the thing {@code IdentifierProfile} and {@code IdentifierCodec} are keyed by — the profile 013 * routes it to an {@code ObjectGroup} for case rules, and the codec needs the full type because 014 * quoting differs per type on some vendors (SQL Server quotes a column differently from a table). 015 * 016 * <h3>Two owners, split by layer — not one</h3> 017 * This class owns the roles whose comparison type the ROLE alone decides. The remaining case, 018 * {@link IdentifierComponentRole#OBJECT}, depends on the object's entity kind and is owned by 019 * {@code CatalogIdentifierPolicy.comparisonTypeOfKind} in the catalog layer. The split is 020 * structural, not stylistic: {@code catalog.runtime} already depends on {@code sqlenv}, so holding 021 * the kind mapping here would close a package cycle and make the low-level identifier layer 022 * import a high-level catalog type that calls straight back into it. 023 * 024 * <h3>What this class deliberately does NOT decide</h3> 025 * The legal ORDER of roles. An earlier version carried a pairwise {@code mayFollow} predicate, and 026 * it was the wrong abstraction: a pairwise test cannot express "at most one SERVER", "a column key 027 * must terminate in a base column or a field path", or the kind-dependent nesting that lets a 028 * PACKAGE contain a PROCEDURE but forbids a SEQUENCE containing a column — and it silently merged 029 * five different languages (resolved identity, partial lookup, column path, package member, table 030 * child) into one. Sequence validation belongs to each domain's whole-shape validator, which sees 031 * the complete component vector, the entity kinds and the declared layout. 032 */ 033public final class IdentifierRolePolicy { 034 035 private IdentifierRolePolicy() { 036 } 037 038 /** 039 * The comparison/quoting type for a component whose role determines it. 040 * 041 * <p>{@link IdentifierComponentRole#OBJECT} is deliberately REFUSED: the comparison type of a 042 * named catalog object depends on its entity kind, which is a catalog concept this package 043 * must not depend on. A catalog caller asks 044 * {@code CatalogIdentifierPolicy.comparisonTypeOfKind} for those. Keeping the two apart is 045 * what stops the low-level identifier layer from importing the high-level catalog layer. 046 * 047 * @throws IllegalArgumentException for a null role or for {@code OBJECT} 048 */ 049 public static ESQLDataObjectType comparisonTypeOf(IdentifierComponentRole role) { 050 if (role == null) { 051 throw new IllegalArgumentException("component role is null"); 052 } 053 switch (role) { 054 case SERVER: 055 return ESQLDataObjectType.dotServer; 056 case CATALOG: 057 return ESQLDataObjectType.dotCatalog; 058 case SCHEMA: 059 return ESQLDataObjectType.dotSchema; 060 case BASE_COLUMN: 061 // A struct field that IS an identifier compares under the same rules as a column: 062 // it is an attribute name in the same namespace family, and no dialect gives 063 // composite attributes their own case rules. 064 case STRUCT_FIELD: 065 return ESQLDataObjectType.dotColumn; 066 case DATABASE_LINK: 067 return ESQLDataObjectType.dotDblink; 068 case OBJECT: 069 throw new IllegalArgumentException( 070 "an OBJECT component's comparison type depends on its CatalogObjectKind; " 071 + "ask CatalogIdentifierPolicy.comparisonTypeOfKind instead"); 072 default: 073 throw new IllegalStateException("unmapped component role: " + role); 074 } 075 } 076 077 /** Whether this role's comparison type is decided by the role alone. */ 078 public static boolean isRoleDecided(IdentifierComponentRole role) { 079 return role != null && role != IdentifierComponentRole.OBJECT; 080 } 081}