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}