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 * Atomic result of one Semantic IR build invocation.
010 *
011 * <p>The program, complete diagnostics, and recovery sidecar are published
012 * together so callers cannot accidentally observe a recovered program while
013 * missing the error that made it invalid for strict publication. A fatal
014 * rejection has a null program and at least one ERROR diagnostic. A normal
015 * successful build has a non-null program, no ERROR diagnostics, and empty
016 * recovery metadata.
017 *
018 * <p><b>API status: advanced/preview.</b> Prefer
019 * {@link SqlSemanticAnalyzer} for the supported SQL-text pipeline. Direct
020 * builder callers should use this atomic result instead of pairing a legacy
021 * {@code build} call with thread-local diagnostic draining.
022 */
023public final class SemanticBuildResult {
024
025    private final SemanticProgram recoveredProgram;
026    private final List<Diagnostic> diagnostics;
027    private final RecoveryMetadata recoveryMetadata;
028    private final boolean hasErrors;
029
030    private SemanticBuildResult(SemanticProgram recoveredProgram,
031                                List<Diagnostic> diagnostics,
032                                RecoveryMetadata recoveryMetadata) {
033        this.recoveredProgram = recoveredProgram;
034        List<Diagnostic> copy = new ArrayList<>(
035                Objects.requireNonNull(diagnostics, "diagnostics"));
036        boolean error = false;
037        for (Diagnostic diagnostic : copy) {
038            Objects.requireNonNull(diagnostic,
039                    "diagnostics must not contain null");
040            if (diagnostic.getSeverity() == Severity.ERROR) {
041                error = true;
042            }
043        }
044        this.diagnostics = Collections.unmodifiableList(copy);
045        this.recoveryMetadata = Objects.requireNonNull(
046                recoveryMetadata, "recoveryMetadata");
047        this.hasErrors = error;
048
049        if (recoveredProgram == null && !error) {
050            throw new IllegalArgumentException(
051                    "a result without a program must contain an ERROR diagnostic");
052        }
053        if (recoveredProgram == null && !recoveryMetadata.isEmpty()) {
054            throw new IllegalArgumentException(
055                    "recovery metadata requires a recovered program");
056        }
057        for (RecoveryMetadata.Entry entry : recoveryMetadata.getEntries()) {
058            if (!copy.contains(entry.getRootDiagnostic())) {
059                throw new IllegalArgumentException(
060                        "recovery root diagnostic must be present in diagnostics");
061            }
062        }
063    }
064
065    /** Construct a result with a built program and optional diagnostics. */
066    public static SemanticBuildResult completed(
067            SemanticProgram program, List<Diagnostic> diagnostics) {
068        SemanticBuildResult result = new SemanticBuildResult(
069                Objects.requireNonNull(program, "program"), diagnostics,
070                RecoveryMetadata.empty());
071        if (result.hasErrors()) {
072            throw new IllegalArgumentException(
073                    "a completed result cannot contain ERROR diagnostics; "
074                            + "use recovered with explicit metadata");
075        }
076        return result;
077    }
078
079    /** Construct a fatal result. At least one diagnostic must be an ERROR. */
080    public static SemanticBuildResult rejected(List<Diagnostic> diagnostics) {
081        return new SemanticBuildResult(
082                null, diagnostics, RecoveryMetadata.empty());
083    }
084
085    /**
086     * Construct a result that retains a program plus explicit recovery
087     * associations. The diagnostics must contain every entry's root
088     * diagnostic.
089     */
090    public static SemanticBuildResult recovered(
091            SemanticProgram recoveredProgram, List<Diagnostic> diagnostics,
092            RecoveryMetadata recoveryMetadata) {
093        if (Objects.requireNonNull(recoveryMetadata, "recoveryMetadata").isEmpty()) {
094            throw new IllegalArgumentException(
095                    "a recovered result requires non-empty recovery metadata");
096        }
097        return new SemanticBuildResult(
098                Objects.requireNonNull(recoveredProgram, "recoveredProgram"),
099                diagnostics, recoveryMetadata);
100    }
101
102    /**
103     * Return the built program, including a compatibility recovery program
104     * when diagnostics contain an ERROR; null only for a fatal rejection.
105     */
106    public SemanticProgram getRecoveredProgram() {
107        return recoveredProgram;
108    }
109
110    /** Complete immutable diagnostics for this invocation. */
111    public List<Diagnostic> getDiagnostics() {
112        return diagnostics;
113    }
114
115    /** Non-null recovery association sidecar. */
116    public RecoveryMetadata getRecoveryMetadata() {
117        return recoveryMetadata;
118    }
119
120    public boolean hasErrors() {
121        return hasErrors;
122    }
123
124    /**
125     * True when a program is retained despite an ERROR or when a producer
126     * explicitly recorded recovered subjects.
127     */
128    public boolean hasRecoveredInvalidSemantics() {
129        return recoveredProgram != null
130                && (hasErrors || !recoveryMetadata.isEmpty());
131    }
132}