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}