001package io.ebean.dbmigration.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        s = s.trim();
026        statements.nextLine(s);
027      }
028
029      return statements.statements;
030
031    } catch (IOException e) {
032      throw new DdlRunnerException(e);
033    }
034  }
035
036
037  /**
038   * Local utility used to detect the end of statements / separate statements.
039   * This is often just the semicolon character but for trigger/procedures this
040   * detects the $$ demarcation used in the history DDL generation for MySql and
041   * Postgres.
042   */
043  static class StatementsSeparator {
044
045    ArrayList<String> statements = new ArrayList<>();
046
047    boolean trimDelimiter;
048
049    boolean inDbProcedure;
050
051    StringBuilder sb = new StringBuilder();
052
053    void lineContainsDollars(String line) {
054      if (inDbProcedure) {
055        if (trimDelimiter) {
056          line = line.replace("$$","");
057        }
058        endOfStatement(line);
059      } else {
060        // MySql style delimiter needs to be trimmed/removed
061        trimDelimiter = line.equals("delimiter $$");
062        if (!trimDelimiter) {
063          sb.append(line).append(" ");
064        }
065      }
066      inDbProcedure = !inDbProcedure;
067    }
068
069    void endOfStatement(String line) {
070      // end of Db procedure
071      sb.append(line);
072      statements.add(sb.toString().trim());
073      sb = new StringBuilder();
074    }
075
076    void nextLine(String line) {
077
078      if (line.contains("$$")) {
079        lineContainsDollars(line);
080        return;
081      }
082
083      if (sb.length() == 0 && (line.isEmpty() || line.startsWith("--"))) {
084        // ignore leading empty lines and sql comments
085        return;
086      }
087
088      if (inDbProcedure) {
089        sb.append(line).append(" ");
090        return;
091      }
092
093      int semiPos = line.lastIndexOf(';');
094      if (semiPos == -1) {
095        sb.append(line).append(" ");
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}