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}