001package io.ebean.migration.ddl;
002
003import java.io.BufferedReader;
004import java.io.IOException;
005import java.io.StringReader;
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(StringReader 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    ArrayList<String> statements = new ArrayList<>();
047
048    boolean trimDelimiter;
049
050    boolean inDbProcedure;
051
052    StringBuilder sb = new StringBuilder();
053
054    void lineContainsDollars(String line) {
055      if (inDbProcedure) {
056        if (trimDelimiter) {
057          line = line.replace("$$","");
058        }
059        endOfStatement(line);
060      } else {
061        // MySql style delimiter needs to be trimmed/removed
062        trimDelimiter = line.equals("delimiter $$");
063        if (!trimDelimiter) {
064          sb.append(line).append(EOL);
065        }
066      }
067      inDbProcedure = !inDbProcedure;
068    }
069
070    void endOfStatement(String line) {
071      // end of Db procedure
072      sb.append(line);
073      statements.add(sb.toString().trim());
074      sb = new StringBuilder();
075    }
076
077    void nextLine(String line) {
078
079      if (line.contains("$$")) {
080        lineContainsDollars(line);
081        return;
082      }
083
084      if (inDbProcedure) {
085        sb.append(line).append(EOL);
086        return;
087      }
088
089      if (sb.length() == 0 && (line.isEmpty() || line.startsWith("--"))) {
090        // ignore leading empty lines and sql comments
091        return;
092      }
093      int semiPos = line.lastIndexOf(';');
094      if (semiPos == -1) {
095        sb.append(line).append(EOL);
096
097      } else if (semiPos == line.length() - 1) {
098        // semicolon at end of line
099        endOfStatement(line);
100
101      } else {
102        // semicolon in middle of line
103        String preSemi = line.substring(0, semiPos + 1);
104        endOfStatement(preSemi);
105
106        String remaining = line.substring(semiPos + 1).trim();
107        if (!remaining.startsWith("--")) {
108          sb.append(remaining).append("\n");
109        }
110      }
111    }
112
113    /**
114     * Append trailing non-terminated content as an extra statement.
115     */
116    void endOfContent() {
117      String remaining = sb.toString().trim();
118      if (remaining.length() > 0) {
119        statements.add(remaining);
120        sb = new StringBuilder();
121      }
122    }
123  }
124}