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}