001package gudusoft.gsqlparser.pp2.render; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.TGSqlParser; 005import gudusoft.gsqlparser.pp.logger.PPLogger; 006import gudusoft.gsqlparser.pp.para.GFmtOpt; 007import gudusoft.gsqlparser.pp.stmtformatter.FormatterFactory; 008import gudusoft.gsqlparser.pp2.Pp2FormatOptions; 009import gudusoft.gsqlparser.pp2.RendererId; 010import gudusoft.gsqlparser.pp2.region.RegionParseOutcome; 011import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 012import gudusoft.gsqlparser.pp2.token.TokenEquivalence; 013 014/** 015 * The "fast lane" pp2 renderer: hands an {@code AST_OK} region's parser to 016 * the existing {@code FormatterFactory.pp(parser, opt)} machinery, then 017 * runs a {@link TokenEquivalence} guard on the result to catch any drift 018 * between input and output. 019 * 020 * <h2>Two failure modes</h2> 021 * 022 * <ol> 023 * <li><b>{@code pp()} throws</b> — possible because pp's mediators / 024 * processors traverse arbitrary AST shapes and may break on edge 025 * cases the parser accepts but pp does not yet handle. The throwable 026 * is logged via {@link PPLogger#error(Throwable)} and the delegate 027 * returns {@code null} so the engine falls through to the next 028 * renderer in the chain.</li> 029 * <li><b>{@code pp()} silently drifts</b> — produces output that no 030 * longer round-trips back to the original token sequence (a comment 031 * dropped, a literal rewritten, etc.). The 032 * {@link TokenEquivalence#equalsModuloFormatting} guard from S10 033 * detects this. Failures are logged via {@link PPLogger#info(String)} 034 * and the delegate returns {@code null}.</li> 035 * </ol> 036 * 037 * <p>The guard is plan-mandated insurance against pp drift. Plan §13/R4 038 * names this as the explicit mitigation; the guard converts silent bugs 039 * into a recoverable fall-through plus a log line. 040 * 041 * <h2>Inputs the delegate can rely on</h2> 042 * 043 * <p>The engine only dispatches {@code AST_OK} outcomes to this renderer, 044 * so the delegate assumes: 045 * <ul> 046 * <li>{@link RegionParseOutcome#getStatus()} is 047 * {@link RegionParseOutcome.Status#AST_OK AST_OK} — 048 * defended by an explicit guard returning {@code null} otherwise.</li> 049 * <li>{@link RegionParseOutcome#getParser()} is non-null and holds 050 * exactly one statement (the S12 contract — verified by 051 * {@code ParseRecoveryEngineTest.parseAll_outcomeParserRendersOnlyItsRegion}).</li> 052 * <li>{@link RegionParseOutcome#getParsedSql()} is the source slice the 053 * parser actually saw (with MSSQL/Sybase {@code GO} stripped) — the 054 * guard compares pp's output against this slice, not the raw range 055 * text.</li> 056 * </ul> 057 * 058 * <p>Plan reference: §5.2, §7.3/S13, §7.4/S13, §10.2, §13/R4. 059 */ 060public class GuardedAstDelegate implements RegionRenderer { 061 062 private final EDbVendor vendor; 063 private long guardFailureCount; 064 private long throwableCount; 065 private long successCount; 066 067 /** 068 * Construct a delegate for the supplied vendor. The vendor is used by 069 * the {@link TokenEquivalence} guard to tokenise both the input and the 070 * output for the comparison. 071 * 072 * @throws NullPointerException if {@code vendor} is null 073 */ 074 public GuardedAstDelegate(EDbVendor vendor) { 075 if (vendor == null) throw new NullPointerException("vendor"); 076 this.vendor = vendor; 077 } 078 079 public EDbVendor getVendor() { return vendor; } 080 081 /** Count of regions that passed both the parse and the guard. */ 082 public long getSuccessCount() { return successCount; } 083 084 /** Count of regions where pp() threw. */ 085 public long getThrowableCount() { return throwableCount; } 086 087 /** Count of regions where pp() returned text that failed the guard. */ 088 public long getGuardFailureCount() { return guardFailureCount; } 089 090 @Override 091 public RendererId id() { return RendererId.GUARDED_AST; } 092 093 @Override 094 public String render(RegionParseOutcome outcome, 095 Pp2TokenStream stream, 096 Pp2FormatOptions opts) { 097 if (outcome == null) throw new NullPointerException("outcome"); 098 if (opts == null) throw new NullPointerException("opts"); 099 // The engine only dispatches AST_OK outcomes here, but defend the 100 // contract anyway so a future caller's mistake is loud, not silent. 101 if (outcome.getStatus() != RegionParseOutcome.Status.AST_OK) { 102 return null; 103 } 104 TGSqlParser parser = outcome.getParser(); 105 if (parser == null) { 106 PPLogger.info("GuardedAstDelegate: AST_OK outcome with null parser " 107 + "(should be impossible per S12 contract): " + outcome); 108 return null; 109 } 110 111 return renderWith(parser, outcome.getParsedSql(), opts); 112 } 113 114 /** 115 * Render via the supplied parser. Exposed {@code protected} so tests 116 * (in a different package) can drive guard-failure scenarios via a 117 * subclass with a stand-in {@link #invokePpFormatter}. Not part of the 118 * public API; production callers go through {@link #render}. 119 */ 120 protected String renderWith(TGSqlParser parser, String inputSlice, 121 Pp2FormatOptions opts) { 122 GFmtOpt gfmt = opts.toGFmtOpt(); 123 String astOutput; 124 try { 125 astOutput = invokePpFormatter(parser, gfmt); 126 } catch (Throwable t) { 127 // pp's mediators / processors may break on AST shapes the 128 // parser accepts. Recover by falling through to the next 129 // renderer; log so the failure is visible during development. 130 throwableCount++; 131 PPLogger.error(t); 132 PPLogger.info("GuardedAstDelegate: FormatterFactory.pp threw on a " 133 + "region; falling through to next renderer. input=\"" 134 + previewForLog(inputSlice) + "\""); 135 return null; 136 } 137 138 if (astOutput == null) { 139 // pp() returning null would be unusual but is not a contract 140 // violation. Treat the same as a guard failure (and emit a 141 // fall-through null on our own). 142 guardFailureCount++; 143 PPLogger.info("GuardedAstDelegate: FormatterFactory.pp returned null" 144 + " for input=\"" + previewForLog(inputSlice) + "\""); 145 return null; 146 } 147 // Note: an empty-string output for a non-empty input slice will 148 // naturally fail the TokenEquivalence guard below (input has solid 149 // tokens, output has none), so we do not treat "" specially here — 150 // the guard is authoritative. 151 152 // Token-equivalence guard. The comparison is dialect-aware: same 153 // vendor used to tokenise both sides; case-insensitive matches the 154 // default formatter behaviour. 155 boolean ok; 156 try { 157 ok = TokenEquivalence.equalsModuloFormatting( 158 inputSlice, astOutput, opts, vendor, true); 159 } catch (Throwable t) { 160 // A throw from the guard itself should not crash the engine. 161 // Treat as guard failure. 162 guardFailureCount++; 163 PPLogger.error(t); 164 PPLogger.info("GuardedAstDelegate: TokenEquivalence guard threw; " 165 + "falling through. input=\"" + previewForLog(inputSlice) + "\""); 166 return null; 167 } 168 if (!ok) { 169 guardFailureCount++; 170 PPLogger.info("GuardedAstDelegate: AST delegate guard failed for " 171 + "input=\"" + previewForLog(inputSlice) 172 + "\" output=\"" + previewForLog(astOutput) + "\""); 173 return null; 174 } 175 successCount++; 176 return astOutput; 177 } 178 179 /** 180 * Indirect-call seam so tests can subclass and substitute a deterministic 181 * stand-in for {@code FormatterFactory.pp()}. Production code always 182 * goes through the real formatter. {@code protected} so the tests in 183 * a different package can override. 184 */ 185 protected String invokePpFormatter(TGSqlParser parser, GFmtOpt opt) { 186 return FormatterFactory.pp(parser, opt); 187 } 188 189 /** Truncate long inputs/outputs for log messages. */ 190 private static String previewForLog(String s) { 191 if (s == null) return "<null>"; 192 final int max = 120; 193 String oneLine = s.replace('\n', ' ').replace('\r', ' '); 194 if (oneLine.length() <= max) return oneLine; 195 return oneLine.substring(0, max) + "...(" + s.length() + " chars)"; 196 } 197}