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}