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