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}