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