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}