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}