001package gudusoft.gsqlparser.catalog.diagnostic;
002
003import gudusoft.gsqlparser.EErrorType;
004import gudusoft.gsqlparser.TGSqlParser;
005import gudusoft.gsqlparser.TSyntaxError;
006import gudusoft.gsqlparser.catalog.runtime.CatalogQualifiedName;
007
008import java.util.List;
009
010/**
011 * Sink that receives {@link CatalogDiagnostic}s emitted by readers, providers, validators,
012 * and the runtime.
013 *
014 * <p>Plan §7.3. {@link #collecting(List)} is the default sink for tests and embedding;
015 * {@link #toParser(TGSqlParser)} is the parser-side opt-in surfacing path landed in P1E
016 * (T1E.1) — WARN/ERROR diagnostics become {@code EErrorType.spwarningdbobject} entries
017 * on {@link TGSqlParser#getSyntaxErrors()}.</p>
018 */
019public interface CatalogDiagnosticSink {
020
021    void accept(CatalogDiagnostic diag);
022
023    /**
024     * Sink that appends every accepted diagnostic into the supplied list.
025     * The list is not cleared and is not made thread-safe — callers control synchronization.
026     */
027    static CatalogDiagnosticSink collecting(final List<CatalogDiagnostic> sink) {
028        if (sink == null) {
029            throw new IllegalArgumentException("CatalogDiagnosticSink.collecting: sink list is required");
030        }
031        return new CatalogDiagnosticSink() {
032            @Override
033            public void accept(CatalogDiagnostic diag) {
034                if (diag == null) {
035                    throw new IllegalArgumentException("CatalogDiagnosticSink: diag may not be null");
036                }
037                sink.add(diag);
038            }
039        };
040    }
041
042    /**
043     * Discards every diagnostic. Useful when a caller wants strict-mode behavior to drive
044     * exceptions and does not care to collect non-fatal warnings.
045     */
046    static CatalogDiagnosticSink discarding() {
047        return new CatalogDiagnosticSink() {
048            @Override
049            public void accept(CatalogDiagnostic diag) {
050                // no-op
051            }
052        };
053    }
054
055    /**
056     * Surface WARN and ERROR diagnostics as {@link EErrorType#spwarningdbobject}
057     * entries on {@link TGSqlParser#getSyntaxErrors()}. INFO diagnostics are dropped:
058     * the parser-side warning channel is for actionable items, and the design plan §15
059     * pairs the {@code spwarningdbobject} category with WARN/ERROR specifically.
060     *
061     * <p><b>Opt-in.</b> Default-mode parsing (no sink attached anywhere) keeps
062     * {@code getSyntaxErrors()} byte-identical to today — this method only takes effect
063     * when the caller wires the returned sink into
064     * {@link gudusoft.gsqlparser.catalog.input.CatalogLoadOptions.Builder#diagnosticSink}.</p>
065     *
066     * <p>The supplied parser's {@code syntaxErrors} list is mutated in place. Each WARN
067     * gets a {@link TSyntaxError} whose {@code tokentext} carries the qualified-name's
068     * last segment (or {@code "<catalog>"} when the diagnostic has no name attached) and
069     * whose {@code hint} is {@code "<code>: <message>"}. {@code lineNo}/{@code columnNo}
070     * default to 0 (catalog diagnostics do not yet carry source positions; that is a P3
071     * follow-up); {@code errorno} is 0 (the {@code spwarningdbobject} type is the
072     * discriminator); {@code posInList} is {@code -1}; {@code sqlStatement} is null.</p>
073     *
074     * <p><b>Parse-return-code interaction.</b> {@link TGSqlParser#parse()} returns
075     * {@code getErrorCount()}, which is the size of {@code syntaxErrors}. With this sink
076     * attached, a WARN catalog diagnostic emitted during {@code parse()} contributes to
077     * that count even when the SQL itself is syntactically valid — that is the explicit
078     * contract of opting into surfacing. Callers who need a 0-return for
079     * syntactically-valid SQL should not attach this sink, or should iterate
080     * {@code getSyntaxErrors()} and filter on {@link EErrorType#spwarningdbobject}.</p>
081     *
082     * <p>A null parser argument is rejected — this sink is meaningless without a parser
083     * to write into.</p>
084     */
085    static CatalogDiagnosticSink toParser(final TGSqlParser parser) {
086        if (parser == null) {
087            throw new IllegalArgumentException("CatalogDiagnosticSink.toParser: parser is required");
088        }
089        return new CatalogDiagnosticSink() {
090            @Override
091            public void accept(CatalogDiagnostic diag) {
092                if (diag == null) {
093                    throw new IllegalArgumentException("CatalogDiagnosticSink: diag may not be null");
094                }
095                CatalogDiagnosticSeverity sev = diag.severity();
096                if (sev != CatalogDiagnosticSeverity.WARN
097                    && sev != CatalogDiagnosticSeverity.ERROR) {
098                    return;
099                }
100                String tokenText = "<catalog>";
101                CatalogQualifiedName qname = diag.name().orElse(null);
102                if (qname != null) {
103                    List<String> segs = qname.normalized();
104                    if (segs != null && !segs.isEmpty()) {
105                        String last = segs.get(segs.size() - 1);
106                        if (last != null && !last.isEmpty()) {
107                            tokenText = last;
108                        }
109                    }
110                }
111                String hint = diag.code().name() + ": " + diag.message();
112                TSyntaxError err = new TSyntaxError(
113                    tokenText,
114                    /* lineNo */ 0,
115                    /* columnNo */ 0,
116                    hint,
117                    EErrorType.spwarningdbobject,
118                    /* errorno */ 0,
119                    /* sqlStatement */ null,
120                    /* posInList */ -1);
121                parser.getSyntaxErrors().add(err);
122            }
123        };
124    }
125}