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}