001package gudusoft.gsqlparser.catalog.runtime; 002 003import gudusoft.gsqlparser.sqlenv.IdentifierComponentRole; 004 005import java.util.ArrayList; 006import java.util.EnumSet; 007import java.util.List; 008import java.util.Set; 009 010/** 011 * Whole-shape validation of a catalog name (stage S0R of 012 * docs/refactor/identifier_identity_display_separation_plan.md). 013 * 014 * <h3>Why whole-shape, and why two entry points</h3> 015 * A pairwise "may this role follow that one" predicate cannot express the rules that matter — at 016 * most one of each container, the terminal agreeing with the declared kind, kind-dependent nesting, 017 * and above all whether the name is COMPLETE. Completeness is not a property of adjacent pairs; it 018 * is a property of the component list measured against the topology the namespace declares. 019 * 020 * <p>A resolved IDENTITY and a LOOKUP are different languages. A lookup may omit levels — {@code t}, 021 * {@code s.t} and {@code db..t} are all normal. An identity may not: every level its topology 022 * declares must be named, because an unresolved level inside a key silently binds to whatever the 023 * session default happened to be, and then collides with the same name in another schema. 024 */ 025public final class CatalogNameGrammar { 026 027 /** Kinds that own columns. */ 028 private static final Set<CatalogObjectKind> RELATIONS = EnumSet.of( 029 CatalogObjectKind.TABLE, CatalogObjectKind.VIEW, CatalogObjectKind.MATERIALIZED_VIEW); 030 031 /** Kinds that own routines. */ 032 private static final Set<CatalogObjectKind> ROUTINE_CONTAINERS = EnumSet.of( 033 CatalogObjectKind.PACKAGE); 034 035 /** Kinds a routine container may own. */ 036 private static final Set<CatalogObjectKind> ROUTINE_MEMBERS = EnumSet.of( 037 CatalogObjectKind.ROUTINE, CatalogObjectKind.FUNCTION, CatalogObjectKind.PROCEDURE); 038 039 /** Kinds a relation may own as a child object. */ 040 private static final Set<CatalogObjectKind> RELATION_CHILDREN = EnumSet.of( 041 CatalogObjectKind.INDEX, CatalogObjectKind.CONSTRAINT); 042 043 /** 044 * Kinds an {@link IdentifierComponentRole#OBJECT} component may carry. CATALOG, SCHEMA and 045 * COLUMN are excluded: those have their own roles, and allowing them as OBJECT kinds would give 046 * one entity two spellings of its identity — {@code OBJECT(COLUMN)} also slips past the rule 047 * that a bare column is not a complete identity. 048 */ 049 private static final Set<CatalogObjectKind> OBJECT_KINDS; 050 051 static { 052 Set<CatalogObjectKind> kinds = EnumSet.allOf(CatalogObjectKind.class); 053 kinds.remove(CatalogObjectKind.CATALOG); 054 kinds.remove(CatalogObjectKind.SCHEMA); 055 kinds.remove(CatalogObjectKind.COLUMN); 056 OBJECT_KINDS = java.util.Collections.unmodifiableSet(kinds); 057 } 058 059 private CatalogNameGrammar() { 060 } 061 062 /** Whether this kind owns columns. */ 063 public static boolean isRelation(CatalogObjectKind kind) { 064 return kind != null && RELATIONS.contains(kind); 065 } 066 067 /** Whether a kind may be carried by an OBJECT component. */ 068 public static boolean isObjectKind(CatalogObjectKind kind) { 069 return kind != null && OBJECT_KINDS.contains(kind); 070 } 071 072 /** 073 * Whether {@code container} may directly contain {@code child} as an object — the ONE transition 074 * table, shared by resolved identities and lookups so the two cannot disagree about what nests. 075 */ 076 public static boolean mayContainObject(CatalogObjectKind container, CatalogObjectKind child) { 077 if (container == null || child == null) { 078 return false; 079 } 080 return (ROUTINE_CONTAINERS.contains(container) && ROUTINE_MEMBERS.contains(child)) 081 || (RELATIONS.contains(container) && RELATION_CHILDREN.contains(child)); 082 } 083 084 /** 085 * The family a NESTED object of this kind must have as its parent. 086 * 087 * <p><strong>This does not decide whether the object IS nested.</strong> That is a property of 088 * the name's shape, not of the kind: a FUNCTION may sit directly in a schema or inside a package, 089 * and an INDEX may be named at schema level or under its relation. An earlier version used this 090 * method to decide existence, which rejected every schema-level function and — because a TABLE 091 * is not a "child kind" — accepted {@code PACKAGE.TABLE} without ever examining the extra 092 * component. 093 * 094 * @return the required family, or null when no nesting of this kind is modelled 095 */ 096 public static CatalogParentConstraint nestedParentFamilyFor(CatalogObjectKind childKind) { 097 if (childKind == CatalogObjectKind.COLUMN || RELATION_CHILDREN.contains(childKind)) { 098 return CatalogParentConstraint.RELATION; 099 } 100 if (ROUTINE_MEMBERS.contains(childKind)) { 101 return CatalogParentConstraint.ROUTINE_CONTAINER; 102 } 103 return null; 104 } 105 106 /** @deprecated renamed to {@link #nestedParentFamilyFor}, which says what it actually answers. */ 107 @Deprecated 108 public static CatalogParentConstraint parentConstraintFor(CatalogObjectKind childKind) { 109 return nestedParentFamilyFor(childKind); 110 } 111 112 // ------------------------------------------------------------------ resolved identity 113 114 /** 115 * Validates a RESOLVED identity: legal roles, legal nesting, and COMPLETE against the declared 116 * topology. 117 * 118 * @param roles one role per component, outermost first 119 * @param kinds the entity kind at each position; null except at OBJECT positions 120 * @param terminalKind what the name denotes as a whole 121 * @param hierarchy which container levels the namespace HAS — declared by the caller, because 122 * the component list cannot distinguish "this dialect has no schema" from 123 * "the schema was not resolved" 124 * @throws IllegalArgumentException naming the first rule broken 125 */ 126 public static void validateResolvedIdentity(List<IdentifierComponentRole> roles, 127 List<CatalogObjectKind> kinds, 128 CatalogObjectKind terminalKind, 129 CatalogHierarchy hierarchy) { 130 if (roles == null || roles.isEmpty()) { 131 throw new IllegalArgumentException("an identity needs at least one component"); 132 } 133 if (kinds == null || kinds.size() != roles.size()) { 134 throw new IllegalArgumentException("every component needs a kind slot (null for " 135 + "non-OBJECT roles): " + roles.size() + " roles, " 136 + (kinds == null ? "no" : String.valueOf(kinds.size())) + " kinds"); 137 } 138 if (terminalKind == null) { 139 throw new IllegalArgumentException("an identity must declare what it denotes"); 140 } 141 if (hierarchy == null) { 142 throw new IllegalArgumentException("an identity must declare its namespace topology: " 143 + "whether db.t is complete depends on whether the namespace has a schema level, " 144 + "and the component list cannot say"); 145 } 146 147 Set<IdentifierComponentRole> containersSeen = EnumSet.noneOf(IdentifierComponentRole.class); 148 for (int i = 0; i < roles.size(); i++) { 149 IdentifierComponentRole role = roles.get(i); 150 CatalogObjectKind kind = kinds.get(i); 151 if (role == null) { 152 throw new IllegalArgumentException("component " + i + " has no role"); 153 } 154 if (role == IdentifierComponentRole.DATABASE_LINK) { 155 throw new IllegalArgumentException("a database link is a postfix qualifier, not an " 156 + "ordered component; pass it separately"); 157 } 158 if (role == IdentifierComponentRole.STRUCT_FIELD) { 159 throw new IllegalArgumentException("a struct field belongs to a column path, not to " 160 + "a catalog identity"); 161 } 162 if ((role == IdentifierComponentRole.OBJECT) != (kind != null)) { 163 throw new IllegalArgumentException("component " + i + " is " + role 164 + " but " + (kind == null ? "has no kind" : "carries kind " + kind) 165 + "; exactly the OBJECT components carry a kind"); 166 } 167 if (role == IdentifierComponentRole.OBJECT && !isObjectKind(kind)) { 168 throw new IllegalArgumentException("component " + i + ": " + kind + " is not an " 169 + "object kind — a catalog, a schema and a column each have their own role, " 170 + "so carrying one as an OBJECT kind would give it a second identity"); 171 } 172 if (role == IdentifierComponentRole.SERVER || role == IdentifierComponentRole.CATALOG 173 || role == IdentifierComponentRole.SCHEMA) { 174 if (!containersSeen.add(role)) { 175 throw new IllegalArgumentException("two " + role + " components"); 176 } 177 // SERVER is an endpoint qualifier, not a topology level, so it is never contradicted. 178 if (role != IdentifierComponentRole.SERVER && !hierarchy.has(role)) { 179 throw new IllegalArgumentException("the declared topology " + hierarchy 180 + " has no " + role + " level, so naming one is a contradiction"); 181 } 182 } 183 if (i > 0) { 184 requireLegalStep(roles.get(i - 1), kinds.get(i - 1), role, kind, i); 185 } else if (role == IdentifierComponentRole.BASE_COLUMN) { 186 throw new IllegalArgumentException("a bare column is not a complete catalog " 187 + "identity: a column is scoped by its relation. Key it inside that " 188 + "relation, or build a lookup instead"); 189 } 190 } 191 192 requireTerminalAgrees(roles, kinds, terminalKind); 193 requireComplete(roles, containersSeen, terminalKind, hierarchy); 194 } 195 196 /** 197 * Every container level the topology declares, and that sits ABOVE the terminal's own level, 198 * must be named. This is the completeness rule: it is what makes {@code db.t} a complete MySQL 199 * table identity and an INCOMPLETE Oracle one. 200 * 201 * <p>SERVER is exempt: its absence means the local server, not a missing level. 202 */ 203 private static void requireComplete(List<IdentifierComponentRole> roles, 204 Set<IdentifierComponentRole> containersSeen, 205 CatalogObjectKind terminalKind, 206 CatalogHierarchy hierarchy) { 207 IdentifierComponentRole outermostNeeded = outermostRequiredContainer(roles); 208 for (IdentifierComponentRole container : hierarchy.containers()) { 209 if (!isAbove(container, outermostNeeded)) { 210 continue; 211 } 212 if (!containersSeen.contains(container)) { 213 throw new IllegalArgumentException("the declared topology " + hierarchy 214 + " has a " + container + " level and this " + terminalKind 215 + " identity does not name it. An unnamed level is not resolved, and an " 216 + "identity containing an unresolved level silently binds to whatever the " 217 + "session default happened to be — build a CatalogLookupName instead"); 218 } 219 } 220 } 221 222 /** 223 * The level the name's own content starts at: a catalog identity needs nothing above it, a 224 * schema identity needs the catalog, an object or column identity needs both. 225 */ 226 private static IdentifierComponentRole outermostRequiredContainer(List<IdentifierComponentRole> roles) { 227 IdentifierComponentRole first = roles.get(0); 228 for (IdentifierComponentRole role : roles) { 229 if (role == IdentifierComponentRole.OBJECT 230 || role == IdentifierComponentRole.BASE_COLUMN) { 231 return IdentifierComponentRole.SCHEMA; 232 } 233 if (role == IdentifierComponentRole.SCHEMA) { 234 return IdentifierComponentRole.CATALOG; 235 } 236 } 237 return first == IdentifierComponentRole.CATALOG || first == IdentifierComponentRole.SERVER 238 ? null : IdentifierComponentRole.CATALOG; 239 } 240 241 /** Whether {@code container} is at or above {@code level}; a null level requires nothing. */ 242 private static boolean isAbove(IdentifierComponentRole container, IdentifierComponentRole level) { 243 if (level == null) { 244 return false; 245 } 246 if (container == IdentifierComponentRole.CATALOG) { 247 return true; 248 } 249 return container == IdentifierComponentRole.SCHEMA 250 && level == IdentifierComponentRole.SCHEMA; 251 } 252 253 private static void requireTerminalAgrees(List<IdentifierComponentRole> roles, 254 List<CatalogObjectKind> kinds, 255 CatalogObjectKind terminalKind) { 256 IdentifierComponentRole lastRole = roles.get(roles.size() - 1); 257 CatalogObjectKind lastKind = kinds.get(kinds.size() - 1); 258 if (lastRole == IdentifierComponentRole.BASE_COLUMN) { 259 if (terminalKind != CatalogObjectKind.COLUMN) { 260 throw new IllegalArgumentException("the name ends in a column but the key declares " 261 + terminalKind); 262 } 263 return; 264 } 265 if (lastRole == IdentifierComponentRole.OBJECT) { 266 if (lastKind != terminalKind) { 267 throw new IllegalArgumentException("the terminal component is " + lastKind 268 + " but the key declares " + terminalKind); 269 } 270 return; 271 } 272 CatalogObjectKind expected = lastRole == IdentifierComponentRole.CATALOG 273 ? CatalogObjectKind.CATALOG 274 : lastRole == IdentifierComponentRole.SCHEMA ? CatalogObjectKind.SCHEMA : null; 275 if (expected == null) { 276 throw new IllegalArgumentException("a name cannot end with " + lastRole); 277 } 278 if (terminalKind != expected) { 279 throw new IllegalArgumentException("the name ends in a " + lastRole 280 + " but the key declares " + terminalKind); 281 } 282 } 283 284 private static void requireLegalStep(IdentifierComponentRole previous, CatalogObjectKind previousKind, 285 IdentifierComponentRole role, CatalogObjectKind kind, int at) { 286 switch (previous) { 287 case SERVER: 288 if (role != IdentifierComponentRole.CATALOG) { 289 throw new IllegalArgumentException("component " + at + ": only a catalog may " 290 + "follow a server in a resolved identity, not " + role 291 + " (a skipped level is an omission, and omissions belong to a lookup)"); 292 } 293 return; 294 case CATALOG: 295 if (role != IdentifierComponentRole.SCHEMA && role != IdentifierComponentRole.OBJECT) { 296 throw new IllegalArgumentException("component " + at + ": a catalog contains a " 297 + "schema, or an object on schemaless dialects, not " + role); 298 } 299 return; 300 case SCHEMA: 301 if (role != IdentifierComponentRole.OBJECT) { 302 throw new IllegalArgumentException("component " + at + ": a schema contains " 303 + "objects, not " + role); 304 } 305 return; 306 case OBJECT: 307 if (role == IdentifierComponentRole.BASE_COLUMN) { 308 if (!isRelation(previousKind)) { 309 throw new IllegalArgumentException("component " + at + ": a " + previousKind 310 + " has no columns; only a table, view or materialized view does"); 311 } 312 return; 313 } 314 if (role == IdentifierComponentRole.OBJECT) { 315 boolean routineMember = ROUTINE_CONTAINERS.contains(previousKind) 316 && ROUTINE_MEMBERS.contains(kind); 317 boolean relationChild = isRelation(previousKind) 318 && RELATION_CHILDREN.contains(kind); 319 if (!routineMember && !relationChild) { 320 throw new IllegalArgumentException("component " + at + ": a " + previousKind 321 + " does not contain a " + kind 322 + "; only PACKAGE->ROUTINE/FUNCTION/PROCEDURE and " 323 + "relation->INDEX/CONSTRAINT nest"); 324 } 325 return; 326 } 327 throw new IllegalArgumentException("component " + at + ": an object cannot contain " 328 + role); 329 case BASE_COLUMN: 330 throw new IllegalArgumentException("component " + at + ": a column contains nothing " 331 + "in a catalog identity (a struct path belongs to a column-path key)"); 332 default: 333 throw new IllegalArgumentException("component " + at + ": " + previous 334 + " cannot contain anything"); 335 } 336 } 337 338 // ------------------------------------------------------------------------ lookup 339 340 /** 341 * Validates a LOOKUP. Unlike the identity form it accepts omissions, but it still has to be a 342 * name that could denote something: the slots must be in hierarchy order, each role may appear 343 * once, every applicable level must have an explicit state, a PRESENT slot must actually carry 344 * text and provenance, and the terminal must be PRESENT and agree with what is being sought. 345 * 346 * <p>The previous version of this method received only a list of slot STATES, so it could check 347 * nothing but "is something present" — it admitted a schema before a catalog, a duplicate 348 * catalog, {@code OBJECT(TABLE)} under terminal {@code PROCEDURE}, and an omitted terminal. 349 * 350 * @param slots the slots, outermost first 351 * @param terminalKind what is being looked for 352 * @param hierarchy the declared topology 353 * @param databaseLink the link text, or null 354 * @param linkForm the link's provenance; required when the link is present 355 */ 356 public static void validateLookup(List<CatalogLookupName.Slot> slots, 357 CatalogObjectKind terminalKind, 358 CatalogHierarchy hierarchy, 359 String databaseLink, 360 gudusoft.gsqlparser.sqlenv.IdentifierInputForm linkForm, 361 CatalogParentConstraint parentConstraint) { 362 if (slots == null || slots.isEmpty()) { 363 throw new IllegalArgumentException("a lookup needs at least one slot"); 364 } 365 if (terminalKind == null) { 366 throw new IllegalArgumentException("a lookup must say what kind it is looking for"); 367 } 368 if (hierarchy == null) { 369 throw new IllegalArgumentException("a lookup must declare its namespace topology"); 370 } 371 372 Set<IdentifierComponentRole> seen = EnumSet.noneOf(IdentifierComponentRole.class); 373 int lastRank = -1; 374 int objectSlots = 0; 375 for (int i = 0; i < slots.size(); i++) { 376 CatalogLookupName.Slot slot = slots.get(i); 377 if (slot == null) { 378 throw new IllegalArgumentException("slot " + i + " is null; use absent(role)"); 379 } 380 IdentifierComponentRole role = slot.getRole(); 381 if (role == IdentifierComponentRole.DATABASE_LINK 382 || role == IdentifierComponentRole.STRUCT_FIELD) { 383 throw new IllegalArgumentException("slot " + i + ": " + role + " is never an " 384 + "ordered catalog slot"); 385 } 386 // OBJECT is the one role that may repeat, and only once: a relation plus its index or 387 // constraint, or a package plus its routine. Every other role names a single level. 388 if (role == IdentifierComponentRole.OBJECT) { 389 if (++objectSlots > 2) { 390 throw new IllegalArgumentException("more than two object slots: a catalog name " 391 + "nests at most a container object and one child"); 392 } 393 } else if (!seen.add(role)) { 394 throw new IllegalArgumentException("two " + role + " slots"); 395 } 396 int rank = rankOf(role); 397 boolean repeatedObject = role == IdentifierComponentRole.OBJECT && objectSlots == 2; 398 if (rank < lastRank || (rank == lastRank && !repeatedObject)) { 399 throw new IllegalArgumentException("slot " + i + ": " + role + " is out of " 400 + "hierarchy order"); 401 } 402 lastRank = rank; 403 404 boolean present = slot.getState() == CatalogLookupName.SlotState.PRESENT; 405 boolean hasText = slot.getText() != null && !slot.getText().isEmpty(); 406 if (present != hasText) { 407 throw new IllegalArgumentException("slot " + i + " (" + role + ") is " 408 + slot.getState() + " but " + (hasText ? "carries text" : "has no text")); 409 } 410 if (present && slot.getForm() == null) { 411 throw new IllegalArgumentException("slot " + i + " (" + role + ") is PRESENT " 412 + "without provenance"); 413 } 414 if (!present && slot.getForm() != null) { 415 throw new IllegalArgumentException("slot " + i + " (" + role + ") is " 416 + slot.getState() + " but carries provenance"); 417 } 418 if (!present && slot.getKind() != null) { 419 throw new IllegalArgumentException("slot " + i + " (" + role + ") is " 420 + slot.getState() + " but carries a kind"); 421 } 422 boolean terminalSlot = i == slots.size() - 1; 423 if (role == IdentifierComponentRole.OBJECT && present) { 424 if (slot.getKind() == null) { 425 // A non-terminal object whose exact kind resolution has not established yet. 426 // The request must then carry the FAMILY it is constrained to, because 427 // "some object" with no constraint at all names nothing in particular. 428 if (terminalSlot) { 429 throw new IllegalArgumentException("slot " + i + ": the terminal object " 430 + "must state its kind — that is what the lookup is seeking"); 431 } 432 if (parentConstraint == null) { 433 throw new IllegalArgumentException("slot " + i + ": a container object with " 434 + "no exact kind needs a parent constraint stating its family; a " 435 + "flat name cannot say whether a column's parent is a table, a " 436 + "view or a materialized view, and guessing is the defect"); 437 } 438 } else if (!isObjectKind(slot.getKind())) { 439 throw new IllegalArgumentException("slot " + i + ": " + slot.getKind() 440 + " is not an object kind"); 441 } 442 } 443 boolean topologyLevel = role == IdentifierComponentRole.CATALOG 444 || role == IdentifierComponentRole.SCHEMA; 445 if (slot.getState() == CatalogLookupName.SlotState.ABSENT && topologyLevel 446 && hierarchy.has(role)) { 447 throw new IllegalArgumentException("slot " + i + ": " + role + " is marked ABSENT " 448 + "but the declared topology " + hierarchy + " has that level; an existing " 449 + "level that was not named is OMITTED, not ABSENT"); 450 } 451 if (slot.getState() != CatalogLookupName.SlotState.ABSENT && topologyLevel 452 && !hierarchy.has(role)) { 453 throw new IllegalArgumentException("slot " + i + ": the declared topology " 454 + hierarchy + " has no " + role + " level, so it can only be ABSENT"); 455 } 456 } 457 458 CatalogLookupName.Slot terminal = slots.get(slots.size() - 1); 459 if (terminal.getState() != CatalogLookupName.SlotState.PRESENT) { 460 throw new IllegalArgumentException("the terminal slot (" + terminal.getRole() 461 + ") is " + terminal.getState() + "; a lookup with no named leaf names nothing"); 462 } 463 // The terminal question comes FIRST: "your name ends in a catalog but you say you seek a 464 // schema" is a more fundamental contradiction than any missing level, and reporting the 465 // missing level instead would send the reader after the wrong problem. 466 requireTerminalSlotAgrees(terminal, terminalKind); 467 468 // Every applicable container level AT OR ABOVE the terminal must have an explicit state, 469 // scoped to match what the builder emits: a catalog lookup has no schema level to state, and 470 // requiring one made the validator contradict the builder. 471 for (IdentifierComponentRole container : hierarchy.containers()) { 472 if (levelAppliesTo(container, terminalKind) && !seen.contains(container)) { 473 throw new IllegalArgumentException("the declared topology " + hierarchy + " has a " 474 + container + " level with no slot; state it as PRESENT or OMITTED rather " 475 + "than leaving it unrepresented"); 476 } 477 } 478 479 requireChildStructure(slots, terminalKind, parentConstraint); 480 481 if (databaseLink != null) { 482 if (databaseLink.isEmpty()) { 483 throw new IllegalArgumentException("a database link with no text is not a link"); 484 } 485 if (linkForm == null) { 486 throw new IllegalArgumentException("a database link needs its provenance"); 487 } 488 requireLinkableLookupTerminal(slots, terminalKind, parentConstraint); 489 } 490 } 491 492 /** 493 * Which LOOKUP shapes may carry a database link. 494 * 495 * <p>A remote relation, synonym or routine obviously may. A COLUMN may too — {@code TObjectName} 496 * documents {@code schema.object.part@dblink} as a real AST shape, and {@code SELECT c FROM 497 * t@remote} makes the column reachable — but only when the request actually names a relation 498 * parent for it. Resolution then attaches the link to the relation identity and composes the 499 * column onto it, which is how the resolved side already works. COLUMN is NOT declared linkable 500 * in general: a column with no relation parent has nothing for the link to qualify. 501 */ 502 private static void requireLinkableLookupTerminal(List<CatalogLookupName.Slot> slots, 503 CatalogObjectKind terminalKind, 504 CatalogParentConstraint parentConstraint) { 505 if (terminalKind != CatalogObjectKind.COLUMN) { 506 requireLinkableTerminal(terminalKind); 507 return; 508 } 509 CatalogLookupName.Slot relation = null; 510 for (CatalogLookupName.Slot slot : slots) { 511 if (slot.getRole() == IdentifierComponentRole.OBJECT) { 512 relation = slot; 513 } 514 } 515 // The relation must be NAMED, not merely modelled: an omitted relation means the link has 516 // nothing concrete to attach to yet, and a resolver must supply the relation first. 517 if (relation == null || relation.getState() != CatalogLookupName.SlotState.PRESENT 518 || parentConstraint == null 519 || !CatalogParentConstraint.RELATION.allowedKinds() 520 .containsAll(parentConstraint.allowedKinds())) { 521 throw new IllegalArgumentException("a database link on a column qualifies the column's " 522 + "RELATION, so the request must name that relation; a column with no relation " 523 + "parent has nothing for the link to qualify"); 524 } 525 } 526 527 /** 528 * The container roles in scope for a name seeking this kind, outermost first — the SINGLE source 529 * of that rule (stage S0R). 530 * 531 * <p>Topology answers which levels EXIST; this answers which are in scope for a particular 532 * terminal, which is a grammar question. The two lived in two copies for one round and disagreed 533 * once, caught only by a test: the builder stopped the vector at the terminal while the validator 534 * still demanded every level. Both now consume this. 535 * 536 * <p>{@link IdentifierComponentRole#SERVER} is not here: it qualifies the endpoint rather than 537 * occupying a level, and is handled separately. 538 */ 539 static List<IdentifierComponentRole> scopedContainerRoles(CatalogObjectKind terminalKind) { 540 if (terminalKind == CatalogObjectKind.CATALOG) { 541 return java.util.Collections.singletonList(IdentifierComponentRole.CATALOG); 542 } 543 return java.util.Collections.unmodifiableList(java.util.Arrays.asList( 544 IdentifierComponentRole.CATALOG, IdentifierComponentRole.SCHEMA)); 545 } 546 547 /** Whether a container level is in scope for a name seeking this kind. */ 548 static boolean levelAppliesTo(IdentifierComponentRole container, CatalogObjectKind terminalKind) { 549 return container == IdentifierComponentRole.SERVER 550 || scopedContainerRoles(terminalKind).contains(container); 551 } 552 553 /** The terminal slot's role and kind must agree with what the lookup says it seeks. */ 554 private static void requireTerminalSlotAgrees(CatalogLookupName.Slot terminal, 555 CatalogObjectKind terminalKind) { 556 switch (terminal.getRole()) { 557 case BASE_COLUMN: 558 if (terminalKind != CatalogObjectKind.COLUMN) { 559 throw new IllegalArgumentException("the lookup ends in a column but seeks " 560 + terminalKind); 561 } 562 return; 563 case OBJECT: 564 if (terminal.getKind() != terminalKind) { 565 throw new IllegalArgumentException("the terminal slot is " + terminal.getKind() 566 + " but the lookup seeks " + terminalKind); 567 } 568 return; 569 case CATALOG: 570 if (terminalKind != CatalogObjectKind.CATALOG) { 571 throw new IllegalArgumentException("the lookup ends in a catalog but seeks " 572 + terminalKind); 573 } 574 return; 575 case SCHEMA: 576 if (terminalKind != CatalogObjectKind.SCHEMA) { 577 throw new IllegalArgumentException("the lookup ends in a schema but seeks " 578 + terminalKind); 579 } 580 return; 581 default: 582 throw new IllegalArgumentException("a lookup cannot end with " + terminal.getRole()); 583 } 584 } 585 586 /** 587 * Validates nesting from the name's SHAPE rather than from its terminal kind. 588 * 589 * <p>The shape says whether the terminal is nested; the kind only says which family the parent 590 * must then belong to. Reading it the other way round produced two opposite errors at once: a 591 * schema-level {@code FUNCTION} was rejected for lacking a package, and {@code PACKAGE.TABLE} was 592 * accepted because a table is not a "child kind" and the surplus component was never examined. 593 */ 594 private static void requireChildStructure(List<CatalogLookupName.Slot> slots, 595 CatalogObjectKind terminalKind, 596 CatalogParentConstraint parentConstraint) { 597 List<CatalogLookupName.Slot> objects = new ArrayList<CatalogLookupName.Slot>(); 598 for (CatalogLookupName.Slot slot : slots) { 599 if (slot.getRole() == IdentifierComponentRole.OBJECT) { 600 objects.add(slot); 601 } 602 } 603 CatalogLookupName.Slot terminal = slots.get(slots.size() - 1); 604 605 if (terminal.getRole() == IdentifierComponentRole.BASE_COLUMN) { 606 // A column is always nested: it needs exactly one relation-parent object. 607 if (objects.size() != 1) { 608 throw new IllegalArgumentException("a column needs exactly one object slot for its " 609 + "relation, even an OMITTED one; found " + objects.size()); 610 } 611 requireFamily(terminalKind, parentConstraint, CatalogParentConstraint.RELATION); 612 requireParentAdmitted(objects.get(0), parentConstraint); 613 return; 614 } 615 616 if (terminal.getRole() != IdentifierComponentRole.OBJECT) { 617 // A container terminal has no object nesting to check. 618 if (parentConstraint != null) { 619 throw new IllegalArgumentException("a " + terminal.getRole() + " terminal is not a " 620 + "nested object, so a parent constraint (" + parentConstraint 621 + ") is meaningless here"); 622 } 623 return; 624 } 625 626 if (objects.size() == 1) { 627 // A top-level object: legal for ANY object kind, including a function or an index named 628 // directly in its schema. 629 if (parentConstraint != null) { 630 throw new IllegalArgumentException("this name has one object component, so " + terminalKind 631 + " is named at container level and has no object parent; the constraint " 632 + parentConstraint + " describes a nesting that is not in the name"); 633 } 634 return; 635 } 636 637 // Two object components: the terminal IS nested, so the nesting must be one the model knows. 638 CatalogParentConstraint required = nestedParentFamilyFor(terminalKind); 639 if (required == null) { 640 throw new IllegalArgumentException("a " + terminalKind + " is never contained in another " 641 + "object, so a name with two object components cannot denote one"); 642 } 643 requireFamily(terminalKind, parentConstraint, required); 644 CatalogLookupName.Slot parent = objects.get(objects.size() - 2); 645 requireParentAdmitted(parent, parentConstraint); 646 if (parent.getKind() != null && !mayContainObject(parent.getKind(), terminalKind)) { 647 throw new IllegalArgumentException("a " + parent.getKind() + " does not contain a " 648 + terminalKind); 649 } 650 } 651 652 private static void requireFamily(CatalogObjectKind terminalKind, 653 CatalogParentConstraint supplied, 654 CatalogParentConstraint required) { 655 if (supplied == null) { 656 throw new IllegalArgumentException("a nested " + terminalKind + " needs a " + required 657 + " parent constraint: without it the request does not say what it is a child OF"); 658 } 659 if (!required.allowedKinds().containsAll(supplied.allowedKinds())) { 660 throw new IllegalArgumentException("a nested " + terminalKind + " requires a " + required 661 + " parent; " + supplied + " admits kinds it cannot legally have"); 662 } 663 } 664 665 private static void requireParentAdmitted(CatalogLookupName.Slot parent, 666 CatalogParentConstraint constraint) { 667 if (parent.getKind() != null && constraint != null && !constraint.admits(parent.getKind())) { 668 throw new IllegalArgumentException("the parent slot is a " + parent.getKind() 669 + ", which " + constraint + " does not admit"); 670 } 671 } 672 673 /** 674 * Only shapes a remote reference is actually known to take may carry a DIRECTLY declared database 675 * link. Allowing it on a catalog, schema, index or constraint would be generalizing past the 676 * evidence. An INHERITED link is not re-judged: a column of a remote table is reachable as 677 * {@code t@remote.c}, so rejecting it would mean a remote relation could have no remote columns. 678 */ 679 static void requireLinkableTerminal(CatalogObjectKind terminalKind) { 680 boolean linkable = RELATIONS.contains(terminalKind) 681 || terminalKind == CatalogObjectKind.SYNONYM 682 || ROUTINE_MEMBERS.contains(terminalKind); 683 if (!linkable) { 684 throw new IllegalArgumentException("a database link qualifies a relation, a synonym or " 685 + "a routine; " + terminalKind + " is not a shape a remote reference is known " 686 + "to take, and generalizing past the evidence is how a wrong identity is built"); 687 } 688 } 689 690 private static int rankOf(IdentifierComponentRole role) { 691 switch (role) { 692 case SERVER: 693 return 0; 694 case CATALOG: 695 return 1; 696 case SCHEMA: 697 return 2; 698 case OBJECT: 699 return 3; 700 case BASE_COLUMN: 701 return 4; 702 default: 703 throw new IllegalArgumentException(role + " has no position in a catalog name"); 704 } 705 } 706}