001package gudusoft.gsqlparser.dlineage.dataflow.model; 002 003/** 004 * Identity of a dlineage model name that is <strong>not</strong> a SQL identifier — an 005 * expression rendering, a literal constant, a pseudo-rows marker, a function display label 006 * (stage S0 of docs/refactor/identifier_identity_display_separation_plan.md). 007 * 008 * <h3>Why these need their own key</h3> 009 * dlineage's name-keyed indexes hold a mixture: some entries are identifiers (a column, a 010 * table) and some are labels the analyzer invented to describe a computation. Running a 011 * label through identifier normalization applies the wrong rules to it: a constant 012 * {@code 'Ab'} is not an Oracle identifier that folds to {@code 'AB'}, and two different 013 * expressions are not the same expression because a vendor happens to be 014 * case-insensitive. Conversely, comparing an identifier with {@code equals} is wrong in the 015 * other direction. 016 * 017 * <p>So labels compare <strong>exactly</strong> — byte-for-byte, no case folding, no 018 * delimiter stripping, no vendor rules — and they carry a {@link Kind} so that a label can 019 * never collide with an identifier key or with a label of another kind in the same index. 020 * The root {@code CLAUDE.md} allows {@code equalsIgnoreCase} for "dlineage display names" 021 * precisely because they are not identifiers; this type is the positive form of that rule: 022 * if a name is a label, key it here, and the question of which vendor folding applies never 023 * arises. 024 * 025 * <p>This type deliberately does <em>not</em> decide whether a given model name is an 026 * identifier or a label — that is a per-call-site judgement made when the index is 027 * migrated in stage S3. It only makes the two representable as different things. 028 */ 029public final class ModelLabelKey { 030 031 /** What kind of non-identifier name this is. */ 032 public enum Kind { 033 /** A rendered expression, e.g. {@code a + b} or {@code CASE WHEN …}. */ 034 EXPRESSION, 035 /** A literal value, e.g. {@code 'Ab'}, {@code 42}. */ 036 CONSTANT, 037 /** A {@link RelationRows} pseudo-column standing for whole-row flow. */ 038 RELATION_ROWS, 039 /** A function's display label, as distinct from its resolvable routine name. */ 040 FUNCTION_LABEL, 041 /** Anything the analyzer synthesised that is none of the above. */ 042 SYNTHETIC 043 } 044 045 private final Kind kind; 046 private final String text; 047 048 private ModelLabelKey(Kind kind, String text) { 049 this.kind = kind; 050 this.text = text; 051 } 052 053 /** 054 * @param kind what sort of label this is; labels of different kinds never compare equal 055 * @param text the label exactly as the model holds it — not normalized, not trimmed 056 * @throws IllegalArgumentException if either argument is null. An empty string IS a 057 * legal label (an empty expression rendering is a real, distinguishable state); 058 * null is not, because a null label in an index is a bug, not a value. 059 */ 060 public static ModelLabelKey of(Kind kind, String text) { 061 if (kind == null) { 062 throw new IllegalArgumentException("label kind is null"); 063 } 064 if (text == null) { 065 throw new IllegalArgumentException("label text is null for kind " + kind); 066 } 067 return new ModelLabelKey(kind, text); 068 } 069 070 public Kind getKind() { 071 return kind; 072 } 073 074 /** The label text, exactly as supplied. */ 075 public String getText() { 076 return text; 077 } 078 079 @Override 080 public boolean equals(Object o) { 081 if (this == o) { 082 return true; 083 } 084 if (o == null || getClass() != o.getClass()) { 085 return false; 086 } 087 ModelLabelKey that = (ModelLabelKey) o; 088 // Exact comparison is the point of this type: see the class comment. 089 return kind == that.kind && text.equals(that.text); 090 } 091 092 @Override 093 public int hashCode() { 094 return 31 * kind.hashCode() + text.hashCode(); 095 } 096 097 @Override 098 public String toString() { 099 return "ModelLabelKey{" + kind + ":" + text + "}"; 100 } 101}