001package gudusoft.gsqlparser.parser;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.ESqlStatementType;
005import gudusoft.gsqlparser.ETokenType;
006import gudusoft.gsqlparser.TBaseType;
007import gudusoft.gsqlparser.TCustomSqlStatement;
008import gudusoft.gsqlparser.TGSqlParser;
009import gudusoft.gsqlparser.TSourceToken;
010import gudusoft.gsqlparser.TSourceTokenList;
011import gudusoft.gsqlparser.TStatementList;
012import gudusoft.gsqlparser.TSyntaxError;
013
014import java.util.ArrayDeque;
015import java.util.ArrayList;
016import java.util.Deque;
017import java.util.HashMap;
018import java.util.LinkedHashSet;
019import java.util.List;
020import java.util.Locale;
021import java.util.Map;
022import java.util.Set;
023
024/**
025 * Locates the statements to skip in a T-SQL routine ({@code CREATE}/{@code ALTER PROCEDURE}, {@code FUNCTION} or
026 * {@code TRIGGER}) whose body holds statements the grammar rejects. Without recovery such a routine fails as a whole
027 * and keeps no body statement, so one statement GSP cannot parse loses the lineage of every other statement in it.
028 *
029 * <p>For each syntax error the statement around it is found -- the innermost statement inside the innermost
030 * {@code BEGIN ... END} (or {@code BEGIN TRY/CATCH}) block that holds the error, delimited by the vendor's own raw
031 * statement splitter -- and its tokens are returned. {@link TGSqlParser} makes them ignorable
032 * ({@link TBaseType#sqlpluscmd}, as CREATE TABLE recovery does for options it cannot parse) and parses the routine
033 * again: the same statement over the same tokens, so every position stays the original one. Whenever something is
034 * uncertain -- an error in the routine's header, a block structure that does not balance, an error no statement
035 * holds -- no token is returned and the routine fails as it always did.
036 */
037public final class RoutineBodyRecovery {
038
039    private RoutineBodyRecovery() {}
040
041    /** The vendors and statement types the recovery applies to: T-SQL routines. */
042    public static boolean appliesTo(EDbVendor vendor, ESqlStatementType type) {
043        if (vendor != EDbVendor.dbvmssql && vendor != EDbVendor.dbvazuresql) return false;
044        switch (type) {
045            case sstmssqlcreateprocedure:
046            case sstmssqlalterprocedure:
047            case sstmssqlcreatefunction:
048            case sstmssqlalterfunction:
049            case sstcreatetrigger:              // CREATE [OR ALTER] TRIGGER
050            case sstmssqlcreatetrigger:
051            case sstmssqlaltertrigger:
052                return true;
053            default:
054                return false;
055        }
056    }
057
058    /**
059     * The tokens of the statements that hold {@code errors}, to make ignorable so that the rest of {@code routine}
060     * parses; empty when any error cannot be placed in a statement of the routine's body, or when the body's blocks do
061     * not balance (no region is certain then, and the whole body is no statement to skip).
062     */
063    public static List<TSourceToken> tokensToIgnore(TCustomSqlStatement routine, List<TSyntaxError> errors,
064                                                    EDbVendor vendor) {
065        List<TSourceToken> none = new ArrayList<TSourceToken>();
066        TSourceTokenList tokens = routine.sourcetokenlist;
067        if (tokens == null || tokens.size() == 0 || errors == null || errors.isEmpty()) return none;
068        int bodyStart = bodyStart(tokens);
069        if (bodyStart < 0) return none;
070        List<int[]> blocks = blocks(tokens, bodyStart);
071        if (blocks == null) return none;
072        Map<Long, Integer> solidTokens = solidTokensByPosition(tokens);
073        // each region is split once, however many errors it holds: {first, last token index} per statement
074        Map<Long, List<int[]>> regionStatements = new HashMap<Long, List<int[]>>();
075        Set<TSourceToken> ignore = new LinkedHashSet<TSourceToken>();
076        for (TSyntaxError error : errors) {
077            Integer at = solidTokens.get(position(error.lineNo, error.columnNo));
078            if (at == null || at <= bodyStart) return none;   // not in the body: the header cannot be skipped
079            int regionStart = bodyStart + 1, regionEnd = tokens.size() - 1;
080            // {BEGIN, first content token, END}, outer blocks first; blocks nest or are disjoint, so the last one
081            // holding the error is the innermost
082            for (int[] block : blocks) {
083                if (block[1] <= at && at < block[2]) {
084                    regionStart = block[1];
085                    regionEnd = block[2] - 1;
086                }
087            }
088            long region = ((long) regionStart << 32) | regionEnd;
089            List<int[]> statements = regionStatements.get(region);
090            if (statements == null) {
091                statements = statements(tokens, regionStart, regionEnd, vendor);
092                regionStatements.put(region, statements);
093            }
094            int[] statement = null;
095            for (int[] candidate : statements) {
096                if (candidate[0] <= at && at <= candidate[1]) {
097                    statement = candidate;
098                    break;
099                }
100            }
101            if (statement == null || holdsBlock(statement, blocks) || runsOnPastItsEnd(tokens, statement)) return none;
102            for (int i = statement[0]; i <= statement[1]; i++) {
103                if (tokens.get(i).issolidtoken()) ignore.add(tokens.get(i));
104            }
105        }
106        return new ArrayList<TSourceToken>(ignore);
107    }
108
109    /**
110     * Index of the {@code AS} that ends the routine's header, or -1: the first {@code AS} outside parentheses that is
111     * neither {@code EXECUTE AS} (a WITH option) nor {@code @parameter AS type}.
112     */
113    static int bodyStart(TSourceTokenList tokens) {
114        int depth = 0;
115        TSourceToken previous = null;
116        for (int i = 0; i < tokens.size(); i++) {
117            TSourceToken t = tokens.get(i);
118            if (!t.issolidtoken()) continue;
119            if (t.tokencode == '(' || t.tokencode == TBaseType.left_parenthesis_2) depth++;
120            else if (t.tokencode == ')') depth--;
121            else if (depth == 0 && t.tokencode == TBaseType.rrw_as && previous != null
122                    && previous.tokencode != TBaseType.rrw_exec && previous.tokencode != TBaseType.rrw_execute
123                    && !previous.toString().startsWith("@")) {
124                return i;
125            }
126            previous = t;
127        }
128        return -1;
129    }
130
131    /**
132     * The routine body's statement blocks as {BEGIN index, first content index, END index}, in the order they open;
133     * null when BEGIN and END do not balance (an error may have eaten one, or left a CASE open). {@code CASE ... END}
134     * is an expression, {@code BEGIN TRAN}/{@code TRANSACTION}/{@code DISTRIBUTED}/{@code DIALOG}/{@code CONVERSATION}
135     * open no block, and {@code END CONVERSATION} ends none.
136     */
137    static List<int[]> blocks(TSourceTokenList tokens, int bodyStart) {
138        List<int[]> blocks = new ArrayList<int[]>();
139        Deque<int[]> open = new ArrayDeque<int[]>();      // {BEGIN index or -1 for CASE, first content index}
140        for (int i = bodyStart + 1; i < tokens.size(); i++) {
141            TSourceToken t = tokens.get(i);
142            if (!t.issolidtoken()) continue;
143            if (t.tokencode == TBaseType.rrw_case) {
144                open.push(new int[]{-1, i + 1});
145            } else if (t.tokencode == TBaseType.rrw_begin) {
146                int next = nextSolid(tokens, i + 1);
147                TSourceToken n = next < 0 ? null : tokens.get(next);
148                if (n != null && opensNoBlock(n)) continue;
149                if (n != null && (n.tokencode == TBaseType.rrw_try || n.tokencode == TBaseType.rrw_catch)) {
150                    open.push(new int[]{i, next + 1});
151                    i = next;
152                } else {
153                    open.push(new int[]{i, i + 1});
154                }
155            } else if (t.tokencode == TBaseType.rrw_end) {
156                int next = nextSolid(tokens, i + 1);
157                if (next >= 0 && endsNoBlock(tokens.get(next))) continue;
158                if (open.isEmpty()) return null;
159                int[] top = open.pop();
160                if (top[0] >= 0) blocks.add(new int[]{top[0], top[1], i});
161                if (next >= 0 && top[0] >= 0 && (tokens.get(next).tokencode == TBaseType.rrw_try
162                        || tokens.get(next).tokencode == TBaseType.rrw_catch)) {
163                    i = next;                                      // END TRY / END CATCH
164                }
165            }
166        }
167        if (!open.isEmpty()) return null;
168        blocks.sort((a, b) -> Integer.compare(a[1], b[1]));        // outer blocks first
169        return blocks;
170    }
171
172    /** {@code BEGIN TRAN}, {@code BEGIN DISTRIBUTED ...}, {@code BEGIN DIALOG}, {@code BEGIN CONVERSATION TIMER}. */
173    private static boolean opensNoBlock(TSourceToken next) {
174        // non-identifier-compare: T-SQL keywords, not object names
175        String word = next.toString().toUpperCase(Locale.ROOT);
176        return word.equals("TRAN") || word.equals("TRANSACTION") || word.equals("DISTRIBUTED")
177                || word.equals("DIALOG") || word.equals("CONVERSATION");
178    }
179
180    /** {@code END CONVERSATION}, a Service Broker statement: it ends no block. */
181    private static boolean endsNoBlock(TSourceToken next) {
182        // non-identifier-compare: a T-SQL keyword, not an object name
183        return next.toString().toUpperCase(Locale.ROOT).equals("CONVERSATION");
184    }
185
186    private static int nextSolid(TSourceTokenList tokens, int from) {
187        for (int i = from; i < tokens.size(); i++) if (tokens.get(i).issolidtoken()) return i;
188        return -1;
189    }
190
191    /**
192     * Whether a statement holds a whole {@code BEGIN ... END} block: an {@code IF} or {@code WHILE} whose condition is
193     * broken, or a statement the raw splitter ran on past its end (an open parenthesis takes in what follows). Skipping
194     * it would drop statements that parse, so such an error is not recovered.
195     */
196    private static boolean holdsBlock(int[] statement, List<int[]> blocks) {
197        for (int[] block : blocks) {
198            if (statement[0] <= block[0] && block[0] <= statement[1]) return true;
199        }
200        return false;
201    }
202
203    /**
204     * Whether a statement goes on past a semicolon that ends a statement: the raw splitter took a broken statement and
205     * the next for one ({@code INSERT INTO t (a); EXEC (...)} reads as INSERT ... EXEC). Skipping it would drop the
206     * statement after the semicolon, which parses, so such an error is not recovered.
207     */
208    private static boolean runsOnPastItsEnd(TSourceTokenList tokens, int[] statement) {
209        for (int i = statement[0]; i < statement[1]; i++) {
210            if (tokens.get(i).tokentype != ETokenType.ttsemicolon) continue;
211            for (int j = i + 1; j <= statement[1]; j++) {
212                if (tokens.get(j).issolidtoken()) return true;
213            }
214        }
215        return false;
216    }
217
218    private static long position(long line, long column) {
219        return (line << 32) | (column & 0xffffffffL);
220    }
221
222    /** Index of every solid token by its line and column (a syntax error names the token it points at so). */
223    private static Map<Long, Integer> solidTokensByPosition(TSourceTokenList tokens) {
224        Map<Long, Integer> positions = new HashMap<Long, Integer>();
225        for (int i = 0; i < tokens.size(); i++) {
226            TSourceToken t = tokens.get(i);
227            if (t.issolidtoken() && !positions.containsKey(position(t.lineNo, t.columnNo))) {
228                positions.put(position(t.lineNo, t.columnNo), i);
229            }
230        }
231        return positions;
232    }
233
234    /**
235     * The statements of a region as {first, last token index}, found by the vendor's raw statement splitter over the
236     * region's text; empty when the splitter fails. The region's tokens are contiguous, so a position in its text
237     * maps back to the original token by offset.
238     */
239    static List<int[]> statements(TSourceTokenList tokens, int regionStart, int regionEnd, EDbVendor vendor) {
240        List<int[]> found = new ArrayList<int[]>();
241        if (regionStart > regionEnd) return found;
242        StringBuilder text = new StringBuilder();
243        for (int i = regionStart; i <= regionEnd; i++) text.append(tokens.get(i).getAstext());
244        long base = tokens.get(regionStart).offset;
245        TGSqlParser splitter = new TGSqlParser(vendor);
246        splitter.sqltext = text.toString();
247        if (splitter.getrawsqlstatements() != 0) return found;
248        TStatementList statements = splitter.sqlstatements;
249        int i = regionStart;
250        for (int s = 0; s < statements.size(); s++) {
251            TCustomSqlStatement statement = statements.get(s);
252            if (statement.getStartToken() == null || statement.getEndToken() == null) continue;
253            long from = statement.getStartToken().offset;
254            long to = statement.getEndToken().offset + statement.getEndToken().getAstext().length();
255            while (i <= regionEnd && tokens.get(i).offset - base < from) i++;
256            int first = i;
257            while (i <= regionEnd && tokens.get(i).offset - base < to) i++;
258            if (i > first) found.add(new int[]{first, i - 1});
259        }
260        return found;
261    }
262}