001package gudusoft.gsqlparser.dlineage.dataflow.model;
002
003/**
004 * Structural identity of one lineage graph node — a single column slot inside one
005 * table or result set (stage S0 of
006 * docs/refactor/identifier_identity_display_separation_plan.md).
007 *
008 * <h3>Why a name cannot do this job</h3>
009 * The lineage walker currently keys nodes by {@code (parent display name, column display
010 * name)}. Duplicate output names are legal SQL: {@code SELECT a AS [x], b AS x} on SQL
011 * Server has two projection slots, two distinct source dependencies — and one published
012 * name. Keying by name therefore conflates them, which is why
013 * {@link ResultColumn} today lowercases its alias and appends a {@code (1)} suffix to break
014 * the tie. That workaround mutates a published name to carry identity.
015 *
016 * <p>Note what is <em>not</em> the fix: making the name key case-sensitive. The two aliases
017 * {@code [x]} and {@code x} are genuinely the same identifier under SQL Server's rules, so
018 * a correct case-sensitive key still merges them. The slots are distinct for a structural
019 * reason — they are different positions in the projection — so identity must be structural.
020 *
021 * <h3>What the ids are</h3>
022 * Every analysis-model node ({@link Table}, {@link ResultSet}, {@link TableColumn},
023 * {@link ResultColumn}) takes its {@code id} from one shared counter,
024 * {@code ModelBindingManager.get().TABLE_COLUMN_ID}, and none of them exposes a setter, so
025 * within one analysis an id is unique across kinds and immutable. A column id alone is
026 * therefore already unique; the parent id is carried as well so an index can bucket by
027 * container.
028 *
029 * <p><strong>No value is treated as "unassigned".</strong> The counter starts from
030 * {@code Option.getStartId()}, which callers may set to zero or a negative number — with
031 * {@code startId = -100} the published ids really are negative — so there is no sentinel this
032 * class could reject. That is also why the factories take model objects wherever possible:
033 * deriving both ids from one object is what prevents a mismatched pair, since no validation
034 * can.
035 *
036 * <p><strong>Do not build this from the serialized model.</strong> The XML/JSON DTOs
037 * ({@code dataflow.model.xml.table} and friends) carry {@code String} ids that are
038 * <em>renumbered</em> at output time (see {@code DataFlowAnalyzer}'s table-merge pass), so
039 * they are a presentation concern and are not stable identity.
040 *
041 * <p>There are therefore TWO identity domains and this type covers only the first:
042 *
043 * <ul>
044 *   <li><strong>internal analysis model</strong> — immutable {@code long} ids from one shared
045 *       counter. This class.</li>
046 *   <li><strong>post-serialization graph</strong> — XML {@code String} {@code parent_id} and
047 *       column {@code id}. Walkers that run on the serialized model, such as
048 *       {@code RoutineSummaryExtractor}, live there and cannot use this type. They need
049 *       {@link SerializedNodeKey}, whose whole point is that it keys on the column ID and
050 *       never on the column NAME.</li>
051 * </ul>
052 */
053public final class LineageNodeKey {
054
055    private final long parentId;
056    private final long columnId;
057
058    private LineageNodeKey(long parentId, long columnId) {
059        this.parentId = parentId;
060        this.columnId = columnId;
061    }
062
063    /**
064     * Key for a result-set projection slot. The container is read from the column itself, so
065     * the pair cannot be mismatched.
066     *
067     * @throws IllegalArgumentException if the column is null or has no result set
068     */
069    public static LineageNodeKey of(ResultColumn column) {
070        requireNonNull(column, "column");
071        ResultSet parent = column.getResultSet();
072        requireNonNull(parent, "result set of column " + column.getId());
073        return new LineageNodeKey(parent.getId(), column.getId());
074    }
075
076    /**
077     * Key for a column of a table endpoint. The container is read from the column itself.
078     *
079     * @throws IllegalArgumentException if the column is null or has no table
080     */
081    public static LineageNodeKey of(TableColumn column) {
082        requireNonNull(column, "column");
083        Table parent = column.getTable();
084        requireNonNull(parent, "table of column " + column.getId());
085        return new LineageNodeKey(parent.getId(), column.getId());
086    }
087
088    /**
089     * Key for a {@link RelationRows} pseudo-node — whole-row flow through a result set. It is a
090     * real graph node with its own id from the same counter, so it needs a node key; the text
091     * label {@code "RelationRows"} is a different thing and belongs to {@link ModelLabelKey}.
092     */
093    public static LineageNodeKey of(ResultSetRelationRows rows) {
094        requireNonNull(rows, "relation rows");
095        requireNonNull(rows.getHolder(), "result set of relation rows " + rows.getId());
096        return new LineageNodeKey(rows.getHolder().getId(), rows.getId());
097    }
098
099    /** Key for a table's {@link RelationRows} pseudo-node. */
100    public static LineageNodeKey of(TableRelationRows rows) {
101        requireNonNull(rows, "relation rows");
102        requireNonNull(rows.getHolder(), "table of relation rows " + rows.getId());
103        return new LineageNodeKey(rows.getHolder().getId(), rows.getId());
104    }
105
106    /**
107     * Key from raw ids. <strong>Deliberately not public.</strong> It cannot check that the
108     * column belongs to the parent, so a stale parent id paired with a live column id would
109     * compile and produce a key for a node that does not exist — which defeats the whole point
110     * of deriving the container from the column. A caller that holds a parent and a column
111     * separately should call the model overload and ASSERT its own parent matches, rather than
112     * passing both in.
113     */
114    static LineageNodeKey ofUncheckedIds(long parentId, long columnId) {
115        return new LineageNodeKey(parentId, columnId);
116    }
117
118    public long getParentId() {
119        return parentId;
120    }
121
122    public long getColumnId() {
123        return columnId;
124    }
125
126    @Override
127    public boolean equals(Object o) {
128        if (this == o) {
129            return true;
130        }
131        if (o == null || getClass() != o.getClass()) {
132            return false;
133        }
134        LineageNodeKey that = (LineageNodeKey) o;
135        return parentId == that.parentId && columnId == that.columnId;
136    }
137
138    @Override
139    public int hashCode() {
140        int result = (int) (parentId ^ (parentId >>> 32));
141        return 31 * result + (int) (columnId ^ (columnId >>> 32));
142    }
143
144    @Override
145    public String toString() {
146        return "LineageNodeKey{" + parentId + "#" + columnId + "}";
147    }
148
149    private static void requireNonNull(Object value, String what) {
150        if (value == null) {
151            throw new IllegalArgumentException(what + " is null");
152        }
153    }
154}