001package gudusoft.gsqlparser.ir.semantic;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.List;
006import java.util.Objects;
007
008/**
009 * Immutable sidecar that links recovered Semantic IR subjects to the root
010 * diagnostics that caused recovery.
011 *
012 * <p>Normal builds use {@link #empty()}. Recovery-capable validators add one
013 * {@link Entry} per recovered subject instead of weakening the invariants of
014 * ordinary Semantic IR entities. The subject kind and id are stable,
015 * non-localized identifiers owned by the producer (for example
016 * {@code "JOIN"} and {@code "statement:0/join:1"}); consumers must not parse
017 * user-visible diagnostic messages to recover this association.
018 *
019 * <p><b>API status: advanced/preview.</b> This is part of the atomic low-level
020 * builder protocol, not Join Analysis Consumption Profile v1.
021 */
022public final class RecoveryMetadata {
023
024    private static final RecoveryMetadata EMPTY =
025            new RecoveryMetadata(Collections.<Entry>emptyList());
026
027    private final List<Entry> entries;
028
029    private RecoveryMetadata(List<Entry> entries) {
030        List<Entry> copy = new ArrayList<>(
031                Objects.requireNonNull(entries, "entries"));
032        for (Entry entry : copy) {
033            Objects.requireNonNull(entry, "entries must not contain null");
034        }
035        this.entries = Collections.unmodifiableList(copy);
036    }
037
038    /** Return the shared empty metadata value used by normal builds. */
039    public static RecoveryMetadata empty() {
040        return EMPTY;
041    }
042
043    /**
044     * Construct metadata from recovered-subject associations.
045     *
046     * @return {@link #empty()} when {@code entries} is empty
047     */
048    public static RecoveryMetadata of(List<Entry> entries) {
049        Objects.requireNonNull(entries, "entries");
050        return entries.isEmpty() ? EMPTY : new RecoveryMetadata(entries);
051    }
052
053    /** Recovery associations in deterministic producer order. */
054    public List<Entry> getEntries() {
055        return entries;
056    }
057
058    public boolean isEmpty() {
059        return entries.isEmpty();
060    }
061
062    @Override
063    public boolean equals(Object o) {
064        if (this == o) return true;
065        if (!(o instanceof RecoveryMetadata)) return false;
066        RecoveryMetadata that = (RecoveryMetadata) o;
067        return entries.equals(that.entries);
068    }
069
070    @Override
071    public int hashCode() {
072        return entries.hashCode();
073    }
074
075    @Override
076    public String toString() {
077        return "RecoveryMetadata{entries=" + entries + "}";
078    }
079
080    /** One recovered-subject to root-diagnostic association. */
081    public static final class Entry {
082        private final String evidenceId;
083        private final String subjectKind;
084        private final String subjectId;
085        private final Diagnostic rootDiagnostic;
086
087        public Entry(String evidenceId, String subjectKind, String subjectId,
088                     Diagnostic rootDiagnostic) {
089            this.evidenceId = requireNonEmpty(evidenceId, "evidenceId");
090            this.subjectKind = requireNonEmpty(subjectKind, "subjectKind");
091            this.subjectId = requireNonEmpty(subjectId, "subjectId");
092            this.rootDiagnostic = Objects.requireNonNull(
093                    rootDiagnostic, "rootDiagnostic");
094        }
095
096        public String getEvidenceId() {
097            return evidenceId;
098        }
099
100        public String getSubjectKind() {
101            return subjectKind;
102        }
103
104        public String getSubjectId() {
105            return subjectId;
106        }
107
108        public Diagnostic getRootDiagnostic() {
109            return rootDiagnostic;
110        }
111
112        @Override
113        public boolean equals(Object o) {
114            if (this == o) return true;
115            if (!(o instanceof Entry)) return false;
116            Entry entry = (Entry) o;
117            return evidenceId.equals(entry.evidenceId)
118                    && subjectKind.equals(entry.subjectKind)
119                    && subjectId.equals(entry.subjectId)
120                    && rootDiagnostic.equals(entry.rootDiagnostic);
121        }
122
123        @Override
124        public int hashCode() {
125            return Objects.hash(evidenceId, subjectKind, subjectId,
126                    rootDiagnostic);
127        }
128
129        @Override
130        public String toString() {
131            return "Entry{evidenceId='" + evidenceId + '\''
132                    + ", subjectKind='" + subjectKind + '\''
133                    + ", subjectId='" + subjectId + '\''
134                    + ", rootDiagnostic=" + rootDiagnostic + "}";
135        }
136
137        private static String requireNonEmpty(String value, String name) {
138            Objects.requireNonNull(value, name);
139            if (value.isEmpty()) {
140                throw new IllegalArgumentException(name + " must not be empty");
141            }
142            return value;
143        }
144    }
145}