001package gudusoft.gsqlparser.demos.prettyscore;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.ETokenType;
005import gudusoft.gsqlparser.TSourceToken;
006
007import java.util.ArrayList;
008import java.util.List;
009import java.util.Locale;
010import java.util.Random;
011
012/**
013 * Derives syntax-broken samples from a clean SQL text with a fixed seed so a
014 * corpus of "real-looking" parse failures can be regenerated in CI without
015 * checking the derived files in. Three degradations, applied in rotation:
016 *
017 * <ul>
018 *   <li>{@link Kind#TRUNCATE} — drop the trailing 10-40% of the tokens at a token boundary.</li>
019 *   <li>{@link Kind#DROP_PAREN} — remove one random {@code (} or {@code )}.</li>
020 *   <li>{@link Kind#INSERT_KEYWORD} — insert {@code FOOBAR} before a random clause keyword.</li>
021 * </ul>
022 *
023 * <p>These samples are meant for the pp2 (tolerant) engine; pp is expected
024 * to reject them.
025 */
026public final class SyntaxDegrader {
027
028    public enum Kind { TRUNCATE, DROP_PAREN, INSERT_KEYWORD }
029
030    /** A derived sample. */
031    public static final class Sample {
032        public final Kind kind;
033        public final String sql;
034        public final String description;
035        /** True when the degraded text still parses (a FOOBAR alias, a cut before an optional clause); excluded from recovery corpora. */
036        public final boolean stillParses;
037        Sample(Kind kind, String sql, String description) { this(kind, sql, description, false); }
038        Sample(Kind kind, String sql, String description, boolean stillParses) { this.kind = kind; this.sql = sql; this.description = description; this.stillParses = stillParses; }
039    }
040
041    private SyntaxDegrader() {
042    }
043
044    /**
045     * Degrade {@code sql} with the {@code index}-th kind (rotating) and a seed derived from {@code seed} and
046     * {@code index}, verifying that the result no longer parses; up to five re-seeded attempts rotate through
047     * the kinds, and a sample that still parses after that is returned flagged {@link Sample#stillParses}.
048     */
049    public static Sample degrade(String sql, EDbVendor vendor, long seed, int index) {
050        Sample last = null;
051        for (int attempt = 0; attempt < 5; attempt++) {
052            Kind kind = Kind.values()[Math.abs(index + attempt) % Kind.values().length];
053            Sample s = degrade(sql, vendor, seed * 31 + index + 1000L * attempt, kind);
054            if (!parses(s.sql, vendor)) return s;
055            last = s;
056        }
057        return new Sample(last.kind, last.sql, last.description + " (still parses)", true);
058    }
059
060    static boolean parses(String sql, EDbVendor vendor) {
061        try {
062            gudusoft.gsqlparser.TGSqlParser p = new gudusoft.gsqlparser.TGSqlParser(vendor);
063            p.sqltext = sql;
064            return p.parse() == 0;
065        } catch (Throwable t) {
066            return false;
067        }
068    }
069
070    public static Sample degrade(String sql, EDbVendor vendor, long seed, Kind kind) {
071        Random rnd = new Random(seed);
072        List<TSourceToken> toks = LayoutDegrader.solidAndCommentTokens(sql, vendor);
073        if (toks.size() < 4) return new Sample(kind, sql, "too short to degrade");
074        switch (kind) {
075            case TRUNCATE: {
076                int keep = (int) Math.max(2, toks.size() * (0.6 + 0.3 * rnd.nextDouble()));
077                TSourceToken last = toks.get(keep - 1);
078                int cut = (int) (last.offset + last.toString().length());
079                return new Sample(kind, sql.substring(0, Math.min(cut, sql.length())), "truncated after token " + keep + " of " + toks.size());
080            }
081            case DROP_PAREN: {
082                List<TSourceToken> parens = new ArrayList<TSourceToken>();
083                for (TSourceToken t : toks) {
084                    if (t.tokentype == ETokenType.ttleftparenthesis || t.tokentype == ETokenType.ttrightparenthesis) parens.add(t);
085                }
086                if (parens.isEmpty()) return degrade(sql, vendor, seed, Kind.TRUNCATE);
087                TSourceToken victim = parens.get(rnd.nextInt(parens.size()));
088                int at = (int) victim.offset;
089                return new Sample(kind, sql.substring(0, at) + sql.substring(at + 1), "dropped '" + victim + "' at offset " + at);
090            }
091            case INSERT_KEYWORD:
092            default: {
093                List<TSourceToken> heads = new ArrayList<TSourceToken>();
094                for (TSourceToken t : toks) {
095                    if (t.tokentype != ETokenType.ttkeyword) continue;
096                    String u = t.toString().toUpperCase(Locale.ROOT);
097                    if (u.equals("FROM") || u.equals("WHERE") || u.equals("GROUP") || u.equals("ORDER") || u.equals("JOIN")
098                        || u.equals("HAVING") || u.equals("SET") || u.equals("VALUES")) heads.add(t);
099                }
100                if (heads.isEmpty()) return degrade(sql, vendor, seed, Kind.TRUNCATE);
101                TSourceToken at = heads.get(rnd.nextInt(heads.size()));
102                int off = (int) at.offset;
103                return new Sample(kind, sql.substring(0, off) + "FOOBAR " + sql.substring(off), "inserted FOOBAR before " + at + " at offset " + off);
104            }
105        }
106    }
107}