001package io.ebean.migration.ddl;
002
003import org.slf4j.Logger;
004import org.slf4j.LoggerFactory;
005
006import java.io.StringReader;
007import java.sql.Connection;
008import java.sql.PreparedStatement;
009import java.sql.SQLException;
010import java.util.ArrayList;
011import java.util.List;
012
013/**
014 * Runs DDL scripts.
015 */
016public class DdlRunner {
017
018  protected static final Logger logger = LoggerFactory.getLogger("io.ebean.DDL");
019
020  private final DdlParser parser;
021
022  private final String scriptName;
023
024  private final boolean useAutoCommit;
025
026  /**
027   * Construct with a script name (for logging) and flag indicating if
028   * auto commit is used (in which case errors are allowed).
029   */
030  public DdlRunner(boolean useAutoCommit, String scriptName) {
031    this(useAutoCommit, scriptName, DdlAutoCommit.NONE);
032  }
033
034  /**
035   * Create specifying the database platform by name.
036   */
037  public DdlRunner(boolean useAutoCommit, String scriptName, String platformName) {
038    this(useAutoCommit, scriptName, DdlAutoCommit.forPlatform(platformName));
039  }
040
041  /**
042   * Create specifying the auto commit behaviour (for the database platform).
043   */
044  public DdlRunner(boolean useAutoCommit, String scriptName, DdlAutoCommit ddlAutoCommit) {
045    this.useAutoCommit = useAutoCommit || ddlAutoCommit.isAutoCommit();
046    this.scriptName = scriptName;
047    this.parser = new DdlParser(this.useAutoCommit ? DdlAutoCommit.NONE : ddlAutoCommit);
048  }
049
050  /**
051   * Parse the content into sql statements and execute them in a transaction.
052   *
053   * @return The non-transactional statements that should execute later.
054   */
055  public List<String> runAll(String content, Connection connection) throws SQLException {
056    List<String> statements = parser.parse(new StringReader(content));
057    runStatements(statements, connection);
058    return parser.getNonTransactional();
059  }
060
061  /**
062   * Execute all the statements in a single transaction.
063   */
064  private void runStatements(List<String> statements, Connection connection) throws SQLException {
065
066    List<String> noDuplicates = new ArrayList<>();
067    for (String statement : statements) {
068      if (!noDuplicates.contains(statement)) {
069        noDuplicates.add(statement);
070      }
071    }
072    if (noDuplicates.isEmpty()) {
073      return;
074    }
075    boolean setAutoCommit = useAutoCommit && !connection.getAutoCommit();
076    if (setAutoCommit) {
077      connection.setAutoCommit(true);
078    }
079    try {
080      logger.info("Executing {} - {} statements, autoCommit:{}", scriptName, noDuplicates.size(), useAutoCommit);
081      for (int i = 0; i < noDuplicates.size(); i++) {
082        String xOfy = (i + 1) + " of " + noDuplicates.size();
083        String ddl = noDuplicates.get(i);
084        runStatement(xOfy, ddl, connection);
085      }
086    } finally {
087      if (setAutoCommit) {
088        connection.setAutoCommit(false);
089      }
090    }
091  }
092
093  /**
094   * Execute the statement.
095   */
096  private void runStatement(String oneOf, String stmt, Connection c) throws SQLException {
097    // trim and remove trailing ; or /
098    stmt = stmt.trim();
099    if (stmt.endsWith(";")) {
100      stmt = stmt.substring(0, stmt.length() - 1);
101    } else if (stmt.endsWith("/")) {
102      stmt = stmt.substring(0, stmt.length() - 1);
103    }
104    if (stmt.isEmpty()) {
105      logger.debug("skip empty statement at " + oneOf);
106      return;
107    }
108    if (logger.isDebugEnabled()) {
109      logger.debug("executing " + oneOf + " " + getSummary(stmt));
110    }
111
112    try (PreparedStatement statement = c.prepareStatement(stmt)) {
113      statement.execute();
114    } catch (SQLException e) {
115      if (useAutoCommit) {
116        logger.debug(" ... ignoring error executing " + getSummary(stmt) + "  error: " + e.getMessage());
117      } else {
118        throw new SQLException("Error executing stmt[" + stmt + "] error[" + e.getMessage() + "]", e);
119      }
120    }
121  }
122
123  private String getSummary(String s) {
124    if (s.length() > 80) {
125      return s.substring(0, 80).trim().replace('\n', ' ') + "...";
126    }
127    return s.replace('\n', ' ');
128  }
129
130  /**
131   * Run any non-transactional statements from the just parsed script.
132   */
133  public int runNonTransactional(Connection connection) {
134    final List<String> nonTransactional = parser.getNonTransactional();
135    return !nonTransactional.isEmpty() ? runNonTransactional(connection, nonTransactional) : 0;
136  }
137
138  /**
139   * Run the non-transactional statements with auto commit true.
140   */
141  public int runNonTransactional(Connection connection, List<String> nonTransactional) {
142    int count = 0;
143    String sql = null;
144    try {
145      logger.debug("running {} non-transactional migration statements", nonTransactional.size());
146      connection.setAutoCommit(true);
147      for (int i = 0; i < nonTransactional.size(); i++) {
148        sql = nonTransactional.get(i);
149        try (PreparedStatement statement = connection.prepareStatement(sql)) {
150          logger.debug("executing - {}", sql);
151          statement.execute();
152          count++;
153        }
154      }
155      return count;
156
157    } catch (SQLException e) {
158      logger.error("Error running non-transaction migration: " + sql, e);
159      return count;
160    } finally {
161      try {
162        connection.setAutoCommit(false);
163      } catch (SQLException e) {
164        logger.error("Error resetting connection autoCommit to false", e);
165      }
166    }
167  }
168}