001package io.ebean.migration.ddl; 002 003import java.io.BufferedReader; 004import java.io.IOException; 005import java.io.Reader; 006import java.util.ArrayList; 007import java.util.List; 008 009/** 010 * Parses string content into separate SQL/DDL statements. 011 */ 012public class DdlParser { 013 014 private final DdlAutoCommit ddlAutoCommit; 015 016 private final StatementsSeparator parse = new StatementsSeparator(); 017 018 private final List<String> statements = new ArrayList<>(); 019 private final List<String> statementsNonTrans = new ArrayList<>(); 020 021 public DdlParser(DdlAutoCommit ddlAutoCommit) { 022 this.ddlAutoCommit = ddlAutoCommit; 023 } 024 025 void push(String sql) { 026 if (ddlAutoCommit.transactional(sql)) { 027 statements.add(sql); 028 } else { 029 statementsNonTrans.add(sql); 030 } 031 } 032 033 /** 034 * Break up the sql in reader into a list of statements using the semi-colon and $$ delimiters; 035 */ 036 public List<String> parse(Reader reader) { 037 try { 038 BufferedReader br = new BufferedReader(reader); 039 String s; 040 while ((s = br.readLine()) != null) { 041 parse.nextLine(s); 042 } 043 parse.endOfContent(); 044 return statements; 045 } catch (IOException e) { 046 throw new DdlRunnerException(e); 047 } 048 } 049 050 /** 051 * Return the non-transactional statements which run later with auto commit true. 052 */ 053 public List<String> getNonTransactional() { 054 return statementsNonTrans; 055 } 056 057 /** 058 * Local utility used to detect the end of statements / separate statements. 059 * This is often just the semicolon character but for trigger/procedures this 060 * detects the $$ demarcation used in the history DDL generation for MySql and 061 * Postgres. 062 */ 063 class StatementsSeparator { 064 065 private static final String EOL = "\n"; 066 067 private static final String GO = "GO"; 068 private static final String PROCEDURE = " PROCEDURE "; 069 070 private boolean trimDelimiter; 071 private boolean inDbProcedure; 072 073 private StringBuilder sb = new StringBuilder(); 074 075 private int lineCount; 076 private int quoteCount; 077 078 void lineContainsDollars(String line) { 079 if (inDbProcedure) { 080 if (trimDelimiter) { 081 line = line.replace("$$", ""); 082 } 083 endOfStatement(line); 084 } else { 085 // MySql style delimiter needs to be trimmed/removed 086 trimDelimiter = line.equals("delimiter $$"); 087 if (!trimDelimiter) { 088 sb.append(line).append(EOL); 089 } 090 inDbProcedure = true; 091 } 092 } 093 094 void endOfStatement(String line) { 095 // end of Db procedure 096 sb.append(line); 097 push(sb.toString().trim()); 098 newBuffer(); 099 } 100 101 private void newBuffer() { 102 quoteCount = 0; 103 lineCount = 0; 104 inDbProcedure = false; 105 sb = new StringBuilder(); 106 } 107 108 /** 109 * Process the next line of the script. 110 */ 111 void nextLine(String line) { 112 113 if (line.trim().equals(GO)) { 114 endOfStatement(""); 115 return; 116 } 117 118 if (line.contains("$$")) { 119 lineContainsDollars(line); 120 return; 121 } 122 123 if (inDbProcedure) { 124 sb.append(line).append(EOL); 125 return; 126 } 127 128 if (sb.length() == 0 && (line.isEmpty() || line.startsWith("--"))) { 129 // ignore leading empty lines and sql comments 130 return; 131 } 132 133 if (lineCount == 0 && isStartDbProcedure(line)) { 134 inDbProcedure = true; 135 } 136 137 lineCount++; 138 quoteCount += countQuotes(line); 139 if (hasOddQuotes()) { 140 // must continue 141 sb.append(line).append(EOL); 142 return; 143 } 144 145 int semiPos = line.lastIndexOf(';'); 146 if (semiPos == -1) { 147 sb.append(line).append(EOL); 148 149 } else if (semiPos == line.length() - 1) { 150 // semicolon at end of line 151 endOfStatement(line); 152 153 } else { 154 // semicolon in middle of line 155 String remaining = line.substring(semiPos + 1).trim(); 156 if (!remaining.startsWith("--")) { 157 // remaining not an inline sql comment so keep going ... 158 sb.append(line).append(EOL); 159 return; 160 } 161 162 String preSemi = line.substring(0, semiPos + 1); 163 endOfStatement(preSemi); 164 } 165 } 166 167 /** 168 * Return true if the start of DB procedure is detected. 169 */ 170 private boolean isStartDbProcedure(String line) { 171 return line.length() > 26 && line.substring(0, 26).toUpperCase().contains(PROCEDURE); 172 } 173 174 /** 175 * Return true if the count of quotes is odd. 176 */ 177 private boolean hasOddQuotes() { 178 return quoteCount % 2 == 1; 179 } 180 181 /** 182 * Return the count of single quotes in the content. 183 */ 184 private int countQuotes(String content) { 185 int count = 0; 186 for (int i = 0; i < content.length(); i++) { 187 if (content.charAt(i) == '\'') { 188 count++; 189 } 190 } 191 return count; 192 } 193 194 /** 195 * Append trailing non-terminated content as an extra statement. 196 */ 197 void endOfContent() { 198 String remaining = sb.toString().trim(); 199 if (remaining.length() > 0) { 200 push(remaining); 201 newBuffer(); 202 } 203 } 204 } 205}