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}