001package gudusoft.gsqlparser.catalog.runtime; 002 003import gudusoft.gsqlparser.sqlenv.IdentifierComponentRole; 004import gudusoft.gsqlparser.sqlenv.IdentifierInputForm; 005 006import java.util.ArrayList; 007import java.util.Collections; 008import java.util.List; 009 010/** 011 * A <strong>request</strong> to find a catalog object — explicitly NOT an identity (stage S0R of 012 * docs/refactor/identifier_identity_display_separation_plan.md). 013 * 014 * <h3>Why a lookup is a different type from a key</h3> 015 * {@code t}, {@code s.t} and {@code db..t} are all normal things to write, and none of them is a 016 * complete identity: the first two leave levels unstated, and the third states that a level was 017 * written but left empty. Only after defaults and the search path supply the missing context does a 018 * {@link CatalogObjectKey} exist. Collapsing the two ideas into one type is what let {@code db..t} 019 * be keyed as {@code db.dbo.t}, which silently binds the wrong object whenever the session default 020 * schema is not {@code dbo}. 021 * 022 * <h3>Three states, never two</h3> 023 * <ul> 024 * <li>{@link SlotState#ABSENT} — the dialect or declared layout genuinely has no such level. 025 * MySQL has no schema level; there is nothing missing.</li> 026 * <li>{@link SlotState#OMITTED} — the level EXISTS and the caller did not name it. {@code db..t} 027 * omits its schema; a bare {@code t} omits both. A resolver must supply it before an identity 028 * can be built, and {@link CatalogObjectKey} refuses to contain one.</li> 029 * <li>{@link SlotState#PRESENT} — named, with its text and the provenance of that text.</li> 030 * </ul> 031 * 032 * Conflating ABSENT with OMITTED loses the only signal that tells "this dialect has no schemas" 033 * apart from "the user did not say which schema", and an empty string conflates both with a level 034 * named by the empty name. 035 */ 036public final class CatalogLookupName { 037 038 /** Whether a hierarchy level is inapplicable, unstated, or named. */ 039 public enum SlotState { 040 ABSENT, 041 OMITTED, 042 PRESENT 043 } 044 045 /** One level of the request. */ 046 public static final class Slot { 047 private final IdentifierComponentRole role; 048 private final SlotState state; 049 private final String text; 050 private final IdentifierInputForm form; 051 private final CatalogObjectKind kind; 052 053 Slot(IdentifierComponentRole role, SlotState state, String text, IdentifierInputForm form, 054 CatalogObjectKind kind) { 055 this.role = role; 056 this.state = state; 057 this.text = text; 058 this.form = form; 059 this.kind = kind; 060 } 061 062 public IdentifierComponentRole getRole() { 063 return role; 064 } 065 066 public SlotState getState() { 067 return state; 068 } 069 070 /** The text, only when {@link #getState()} is PRESENT. */ 071 public String getText() { 072 return text; 073 } 074 075 /** Where the text came from, only when PRESENT. */ 076 public IdentifierInputForm getForm() { 077 return form; 078 } 079 080 /** The entity kind for an OBJECT slot. */ 081 public CatalogObjectKind getKind() { 082 return kind; 083 } 084 085 @Override 086 public String toString() { 087 if (state != SlotState.PRESENT) { 088 return role + "=" + state; 089 } 090 return role + (kind == null ? "" : "(" + kind + ")") + "=" + text + "/" + form; 091 } 092 } 093 094 private final List<Slot> slots; 095 private final CatalogObjectKind terminalKind; 096 private final CatalogHierarchy hierarchy; 097 private final CatalogParentConstraint parentConstraint; 098 private final String databaseLink; 099 private final IdentifierInputForm databaseLinkForm; 100 101 private CatalogLookupName(List<Slot> slots, CatalogObjectKind terminalKind, 102 CatalogHierarchy hierarchy, CatalogParentConstraint parentConstraint, 103 String databaseLink, IdentifierInputForm databaseLinkForm) { 104 this.slots = Collections.unmodifiableList(new ArrayList<Slot>(slots)); 105 this.terminalKind = terminalKind; 106 this.hierarchy = hierarchy; 107 this.parentConstraint = parentConstraint; 108 this.databaseLink = databaseLink; 109 this.databaseLinkForm = databaseLinkForm; 110 } 111 112 /** 113 * A request against a declared namespace topology. The topology is required: whether a missing 114 * schema is an OMISSION or simply inapplicable is what it answers, and no component list can. 115 */ 116 public static Builder builder(CatalogHierarchy hierarchy) { 117 return new Builder(hierarchy); 118 } 119 120 /** 121 * Parses a FLAT name under a layout the caller DECLARES, against a topology the caller DECLARES. 122 * The parser verifies the component count against the layout and refuses otherwise; it never 123 * chooses the layout, which is the whole point — choosing is the defect. 124 * 125 * <p>Where the layout has a container OBJECT whose exact kind a flat name cannot carry — a 126 * column's parent may be a table, a view or a materialized view — the FAMILY is recorded 127 * instead, derived from the layout itself. A lookup legitimately knows only the family; only a 128 * resolved {@link CatalogObjectKey} carries the exact kind. 129 * 130 * <p>Prefer {@link #builder(CatalogHierarchy)} with the parts passed separately. This exists for 131 * callers whose input genuinely is one string. 132 * 133 * @throws IllegalArgumentException if the component count does not match the declared layout 134 */ 135 public static CatalogLookupName ofDeclaredLayout(List<String> components, 136 CatalogNameLayout layout, 137 IdentifierInputForm form, 138 CatalogObjectKind terminalKind, 139 CatalogHierarchy hierarchy) { 140 if (components == null || layout == null) { 141 throw new IllegalArgumentException("components and layout are both required"); 142 } 143 if (components.size() != layout.size()) { 144 throw new IllegalArgumentException("declared layout " + layout + " has " + layout.size() 145 + " components but the name has " + components.size() 146 + "; the caller must declare the layout its name actually has, and the parser " 147 + "will not pick one"); 148 } 149 150 if (!layout.permitsTerminalKind(terminalKind)) { 151 throw new IllegalArgumentException("layout " + layout + " does not describe a " 152 + terminalKind + "; a layout states a SHAPE, and using a child layout to seek a " 153 + "container object would describe that object as a child of another"); 154 } 155 Builder builder = builder(hierarchy); 156 // A level the topology HAS but the declared layout does not write is OMITTED. That is not a 157 // guess: the layout states what was written and the topology states what exists, so the 158 // difference between them is exactly "exists but unnamed". 159 for (IdentifierComponentRole container : hierarchy.containers()) { 160 if (!layout.roles().contains(container)) { 161 builder.omitted(container); 162 } 163 } 164 CatalogParentConstraint containerFamily = containerFamilyOf(layout, terminalKind); 165 if (containerFamily != null) { 166 builder.parentConstraint(containerFamily); 167 } 168 for (int i = 0; i < components.size(); i++) { 169 IdentifierComponentRole role = layout.roleAt(i); 170 String text = components.get(i); 171 if (text == null || text.isEmpty()) { 172 // An empty component in a declared layout is an OMISSION, not a name: `db..t`. 173 builder.omitted(role); 174 continue; 175 } 176 if (role == IdentifierComponentRole.OBJECT) { 177 boolean terminal = i == components.size() - 1; 178 if (terminal) { 179 builder.object(terminalKind, text, form); 180 } else { 181 // The container's exact kind is not expressible here; the family above is. 182 builder.containerObject(text, form); 183 } 184 } else if (role == IdentifierComponentRole.BASE_COLUMN) { 185 builder.baseColumn(text, form); 186 } else { 187 builder.present(role, text, form); 188 } 189 } 190 return builder.build(terminalKind); 191 } 192 193 /** 194 * The family a layout's container object belongs to, or null when the layout has no container 195 * object. Derived from the LAYOUT, which is declared — not from the component count. 196 */ 197 private static CatalogParentConstraint containerFamilyOf(CatalogNameLayout layout, 198 CatalogObjectKind terminalKind) { 199 int objects = 0; 200 boolean terminalIsColumn = false; 201 for (int i = 0; i < layout.size(); i++) { 202 if (layout.roleAt(i) == IdentifierComponentRole.OBJECT) { 203 objects++; 204 } 205 if (i == layout.size() - 1 206 && layout.roleAt(i) == IdentifierComponentRole.BASE_COLUMN) { 207 terminalIsColumn = true; 208 } 209 } 210 if (terminalIsColumn && objects >= 1) { 211 return CatalogParentConstraint.RELATION; 212 } 213 if (objects >= 2) { 214 return layout == CatalogNameLayout.CATALOG_SCHEMA_PACKAGE_MEMBER 215 ? CatalogParentConstraint.ROUTINE_CONTAINER 216 : CatalogParentConstraint.RELATION; 217 } 218 return null; 219 } 220 221 /** @see #builder() */ 222 public static final class Builder { 223 private final List<Slot> slots = new ArrayList<Slot>(); 224 private final CatalogHierarchy hierarchy; 225 private CatalogParentConstraint parentConstraint; 226 private boolean parentConstraintSupplied; 227 private String databaseLink; 228 private IdentifierInputForm databaseLinkForm; 229 230 private Builder(CatalogHierarchy hierarchy) { 231 if (hierarchy == null) { 232 throw new IllegalArgumentException("a lookup must declare its namespace topology: " 233 + "that is what distinguishes an inapplicable level from an unnamed one"); 234 } 235 this.hierarchy = hierarchy; 236 } 237 238 /** 239 * What is known about the parent of the object being sought, when it is a child. A LOOKUP 240 * carries a FAMILY, not an exact kind: parsing {@code schema.relation.column} establishes 241 * that the parent is a relation, not whether it will resolve to a table or a view. 242 */ 243 public Builder parentConstraint(CatalogParentConstraint constraint) { 244 if (constraint == null) { 245 throw new IllegalArgumentException("a parent constraint of null says nothing; omit " 246 + "the call instead"); 247 } 248 // Tracked by WHETHER it was supplied, not by its value. Comparing values let a caller 249 // classify twice with the same answer — which is still double classification, and a 250 // null-then-value sequence slipped through entirely. 251 if (parentConstraintSupplied) { 252 throw new IllegalArgumentException("the parent constraint was already supplied as " 253 + this.parentConstraint + "; stating it twice hides which call the caller " 254 + "meant"); 255 } 256 this.parentConstraint = constraint; 257 this.parentConstraintSupplied = true; 258 return this; 259 } 260 261 /** 262 * Refuses a second slot for a role that names a single level. The normalization step below 263 * reorders slots into hierarchy order, and an earlier version took the FIRST slot of each 264 * role — so a caller that supplied two catalogs silently lost one of them before the 265 * uniqueness validator ever saw it. Rejecting at the point of addition is the only place 266 * where both values are still in hand. 267 */ 268 private void refuseDuplicate(IdentifierComponentRole role) { 269 if (role == IdentifierComponentRole.OBJECT) { 270 return; // a relation plus its child, or a package plus its routine 271 } 272 for (Slot slot : slots) { 273 if (slot.role == role) { 274 throw new IllegalArgumentException("a second " + role + " slot: " 275 + "this role names a single level, and keeping one of two values would " 276 + "discard the other silently"); 277 } 278 } 279 } 280 281 /** The level exists and is named. */ 282 public Builder present(IdentifierComponentRole role, String text, IdentifierInputForm form) { 283 if (text == null || text.isEmpty()) { 284 throw new IllegalArgumentException("a PRESENT " + role + " needs text; use " 285 + "omitted(role) when the level exists but was not named, or absent(role) " 286 + "when the dialect has no such level"); 287 } 288 if (form == null) { 289 throw new IllegalArgumentException("a PRESENT slot needs its provenance"); 290 } 291 refuseDuplicate(role); 292 slots.add(new Slot(role, SlotState.PRESENT, text, form, null)); 293 return this; 294 } 295 296 /** A named object level, with its entity kind. */ 297 public Builder object(CatalogObjectKind kind, String text, IdentifierInputForm form) { 298 if (kind == null) { 299 throw new IllegalArgumentException("an object slot needs its kind"); 300 } 301 if (text == null || text.isEmpty()) { 302 throw new IllegalArgumentException("a PRESENT object needs text"); 303 } 304 slots.add(new Slot(IdentifierComponentRole.OBJECT, SlotState.PRESENT, text, form, kind)); 305 return this; 306 } 307 308 /** 309 * A named container object whose exact kind is not yet known — only the family, which the 310 * request carries through {@link #parentConstraint}. This is what a flat 311 * {@code schema.relation.column} can honestly say about its middle component. 312 */ 313 public Builder containerObject(String text, IdentifierInputForm form) { 314 if (text == null || text.isEmpty()) { 315 throw new IllegalArgumentException("a PRESENT object needs text"); 316 } 317 if (form == null) { 318 throw new IllegalArgumentException("a PRESENT slot needs its provenance"); 319 } 320 slots.add(new Slot(IdentifierComponentRole.OBJECT, SlotState.PRESENT, text, form, null)); 321 return this; 322 } 323 324 /** 325 * A named column level. It installs the RELATION parent constraint itself: a column is 326 * always a child of a relation, which is semantic knowledge rather than a guess, so making 327 * every caller restate it would be ceremony that some caller eventually forgets. 328 */ 329 public Builder baseColumn(String text, IdentifierInputForm form) { 330 present(IdentifierComponentRole.BASE_COLUMN, text, form); 331 if (parentConstraint == null) { 332 // Installed by the type itself, so it does NOT count as the caller having 333 // classified: a caller may still state RELATION explicitly once. 334 parentConstraint = CatalogParentConstraint.RELATION; 335 } 336 return this; 337 } 338 339 /** The level exists but the caller did not name it — a resolver must supply it. */ 340 public Builder omitted(IdentifierComponentRole role) { 341 refuseDuplicate(role); 342 slots.add(new Slot(role, SlotState.OMITTED, null, null, null)); 343 return this; 344 } 345 346 /** The dialect or layout has no such level at all. */ 347 public Builder absent(IdentifierComponentRole role) { 348 refuseDuplicate(role); 349 slots.add(new Slot(role, SlotState.ABSENT, null, null, null)); 350 return this; 351 } 352 353 public Builder databaseLink(String text, IdentifierInputForm form) { 354 if (databaseLink != null) { 355 throw new IllegalArgumentException("a name has one database link"); 356 } 357 if (text == null || text.isEmpty()) { 358 throw new IllegalArgumentException("a database link with no text is not a link"); 359 } 360 if (form == null) { 361 throw new IllegalArgumentException("a database link needs its provenance"); 362 } 363 this.databaseLink = text; 364 this.databaseLinkForm = form; 365 return this; 366 } 367 368 public CatalogLookupName build(CatalogObjectKind terminalKind) { 369 if (terminalKind == null) { 370 throw new IllegalArgumentException("a lookup must say what kind it is looking for"); 371 } 372 // Levels the topology does not have are marked ABSENT automatically. That is not a 373 // guess: the topology DECLARED them inapplicable, and leaving them unrepresented is 374 // what made a bare `t` report itself fully qualified. 375 // Only levels at or ABOVE the requested terminal belong in the vector. A CATALOG lookup 376 // that also carried a schema slot was claiming to name a schema while saying it sought a 377 // catalog — two different requests wearing one shape. 378 List<Slot> complete = new ArrayList<Slot>(); 379 for (IdentifierComponentRole container 380 : new IdentifierComponentRole[]{IdentifierComponentRole.SERVER, 381 IdentifierComponentRole.CATALOG, IdentifierComponentRole.SCHEMA}) { 382 if (!CatalogNameGrammar.levelAppliesTo(container, terminalKind)) { 383 Slot stray = find(container); 384 if (stray != null) { 385 throw new IllegalArgumentException("a " + terminalKind + " lookup names a " 386 + container + ", which sits below what it seeks"); 387 } 388 continue; 389 } 390 Slot existing = find(container); 391 if (existing != null) { 392 complete.add(existing); 393 } else if (container != IdentifierComponentRole.SERVER && !hierarchy.has(container)) { 394 complete.add(new Slot(container, SlotState.ABSENT, null, null, null)); 395 } 396 } 397 for (Slot slot : slots) { 398 if (slot.role != IdentifierComponentRole.SERVER 399 && slot.role != IdentifierComponentRole.CATALOG 400 && slot.role != IdentifierComponentRole.SCHEMA) { 401 complete.add(slot); 402 } 403 } 404 CatalogNameGrammar.validateLookup(complete, terminalKind, hierarchy, databaseLink, 405 databaseLinkForm, parentConstraint); 406 return new CatalogLookupName(complete, terminalKind, hierarchy, parentConstraint, 407 databaseLink, databaseLinkForm); 408 } 409 410 private Slot find(IdentifierComponentRole role) { 411 for (Slot slot : slots) { 412 if (slot.role == role) { 413 return slot; 414 } 415 } 416 return null; 417 } 418 } 419 420 /** The slots, outermost first. */ 421 public List<Slot> getSlots() { 422 return slots; 423 } 424 425 public CatalogObjectKind getTerminalKind() { 426 return terminalKind; 427 } 428 429 /** The declared namespace topology this request was built against. */ 430 public CatalogHierarchy getHierarchy() { 431 return hierarchy; 432 } 433 434 /** What is known about the parent's kind, or null when the request names no child. */ 435 public CatalogParentConstraint getParentConstraint() { 436 return parentConstraint; 437 } 438 439 public String getDatabaseLink() { 440 return databaseLink; 441 } 442 443 public IdentifierInputForm getDatabaseLinkForm() { 444 return databaseLinkForm; 445 } 446 447 /** 448 * Whether every level this request needs is named, so an identity could be built without 449 * consulting any default. An omission here is not an error — it is the normal reason a resolver 450 * is involved. 451 */ 452 public boolean isFullyQualified() { 453 for (Slot slot : slots) { 454 if (slot.state == SlotState.OMITTED) { 455 return false; 456 } 457 } 458 return true; 459 } 460 461 /** 462 * The slot for a role, or null when this request has no such level. 463 * 464 * @throws IllegalArgumentException for {@link IdentifierComponentRole#OBJECT}, which may appear 465 * twice — a relation and its index. Returning the first silently would hand a caller 466 * asking for "the object" the container when it wanted the child. Use 467 * {@link #getTerminalSlot()}, {@link #getParentObjectSlot()} or {@link #getObjectSlots()}. 468 */ 469 public Slot slotOf(IdentifierComponentRole role) { 470 if (role == IdentifierComponentRole.OBJECT) { 471 throw new IllegalArgumentException("OBJECT may appear twice, so asking for 'the' object " 472 + "slot is ambiguous; use getTerminalSlot(), getParentObjectSlot() or " 473 + "getObjectSlots()"); 474 } 475 for (Slot slot : slots) { 476 if (slot.role == role) { 477 return slot; 478 } 479 } 480 return null; 481 } 482 483 /** The innermost slot — what this request is actually looking for. */ 484 public Slot getTerminalSlot() { 485 return slots.get(slots.size() - 1); 486 } 487 488 /** The object slot that contains the terminal, or null when the terminal has no object parent. */ 489 public Slot getParentObjectSlot() { 490 Slot parent = null; 491 for (int i = 0; i < slots.size() - 1; i++) { 492 if (slots.get(i).role == IdentifierComponentRole.OBJECT) { 493 parent = slots.get(i); 494 } 495 } 496 return parent; 497 } 498 499 /** Every object slot, outermost first. */ 500 public List<Slot> getObjectSlots() { 501 List<Slot> objects = new ArrayList<Slot>(2); 502 for (Slot slot : slots) { 503 if (slot.role == IdentifierComponentRole.OBJECT) { 504 objects.add(slot); 505 } 506 } 507 return Collections.unmodifiableList(objects); 508 } 509 510 @Override 511 public String toString() { 512 StringBuilder sb = new StringBuilder("CatalogLookupName(").append(terminalKind).append(") "); 513 for (int i = 0; i < slots.size(); i++) { 514 if (i > 0) { 515 sb.append(" / "); 516 } 517 sb.append(slots.get(i)); 518 } 519 if (databaseLink != null) { 520 sb.append(" @").append(databaseLink); 521 } 522 return sb.toString(); 523 } 524}