001package gudusoft.gsqlparser.dlineage.dataflow.model; 002 003/** 004 * Structural identity of a node in the <strong>serialized</strong> lineage graph — the XML/JSON 005 * model, where ids are {@code String} (stage S0 of 006 * docs/refactor/identifier_identity_display_separation_plan.md). 007 * 008 * <h3>Why this is a separate type from {@link LineageNodeKey}</h3> 009 * There are two identity domains and they are not interchangeable. The analysis model uses 010 * immutable {@code long} ids from one shared counter; the serialized DTOs carry {@code String} 011 * ids that are RENUMBERED at output time by the table-merge pass. A walker that runs after 012 * serialization — {@code RoutineSummaryExtractor} is the live example — can only see the string 013 * ids, so it cannot use {@code LineageNodeKey}, and a single type covering both would invite 014 * keying one domain with the other's ids. 015 * 016 * <h3>The point of the type: id, never name</h3> 017 * Serialized walkers today key nodes by {@code (parent_id, column NAME)}. That is the same defect 018 * as keying by {@code (parent name, column name)} in the analysis model, just one field less 019 * wrong: two projection slots of one parent that publish the same name still collapse. This type 020 * keys on the column's own {@code id}, so duplicate published names stay distinct. 021 * 022 * <p>Renumbering is harmless here as long as both halves of a comparison come from the SAME 023 * serialized document — which is the only claim this key makes. Do NOT use it to compare two 024 * documents, two analyzer runs, or a baseline jar against a patched one; for that, compare 025 * endpoints and topology, not ids. 026 */ 027public final class SerializedNodeKey { 028 029 private final String parentId; 030 private final String columnId; 031 032 private SerializedNodeKey(String parentId, String columnId) { 033 this.parentId = parentId; 034 this.columnId = columnId; 035 } 036 037 /** 038 * @param parentId the containing object's serialized id, e.g. {@code targetColumn.getParent_id()} 039 * @param columnId the column's own serialized id, e.g. {@code targetColumn.getId()} 040 * @throws IllegalArgumentException if either id is null or empty. Unlike the analysis model, 041 * where any {@code long} is a legal id, an absent string id means the serializer never 042 * assigned one — so there is nothing to identify, and accepting it would merge every 043 * such node. 044 */ 045 public static SerializedNodeKey of(String parentId, String columnId) { 046 if (parentId == null || parentId.isEmpty()) { 047 throw new IllegalArgumentException("serialized parent id is absent"); 048 } 049 if (columnId == null || columnId.isEmpty()) { 050 throw new IllegalArgumentException("serialized column id is absent; " 051 + "do NOT fall back to the column name — that is the defect this type removes"); 052 } 053 return new SerializedNodeKey(parentId, columnId); 054 } 055 056 public String getParentId() { 057 return parentId; 058 } 059 060 public String getColumnId() { 061 return columnId; 062 } 063 064 @Override 065 public boolean equals(Object o) { 066 if (this == o) { 067 return true; 068 } 069 if (o == null || getClass() != o.getClass()) { 070 return false; 071 } 072 SerializedNodeKey that = (SerializedNodeKey) o; 073 return parentId.equals(that.parentId) && columnId.equals(that.columnId); 074 } 075 076 @Override 077 public int hashCode() { 078 return 31 * parentId.hashCode() + columnId.hashCode(); 079 } 080 081 @Override 082 public String toString() { 083 return "SerializedNodeKey{" + parentId + "#" + columnId + "}"; 084 } 085}