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 DdlParser ddlParser = new DdlParser();
021
022  private final String scriptName;
023
024  private final boolean expectErrors;
025
026  private boolean commitOnCreateIndex;
027
028  /**
029   * Construct with a script name (for logging) and flag indicating if errors are expected.
030   */
031  public DdlRunner(boolean expectErrors, String scriptName) {
032    this.expectErrors = expectErrors;
033    this.scriptName = scriptName;
034  }
035
036  /**
037   * Needed for Cockroach DB. Needs commit after create index and before alter table add FK.
038   */
039  public void setCommitOnCreateIndex() {
040    commitOnCreateIndex = true;
041  }
042
043  /**
044   * Parse the content into sql statements and execute them in a transaction.
045   */
046  public int runAll(String content, Connection connection) throws SQLException {
047
048    List<String> statements = ddlParser.parse(new StringReader(content));
049    return runStatements(statements, connection);
050  }
051
052  /**
053   * Execute all the statements in a single transaction.
054   */
055  private int runStatements(List<String> statements, Connection connection) throws SQLException {
056
057    List<String> noDuplicates = new ArrayList<>();
058
059    for (String statement : statements) {
060      if (!noDuplicates.contains(statement)) {
061        noDuplicates.add(statement);
062      }
063    }
064
065    logger.info("Executing {} - {} statements", scriptName, noDuplicates.size());
066
067    for (int i = 0; i < noDuplicates.size(); i++) {
068      String xOfy = (i + 1) + " of " + noDuplicates.size();
069      String ddl = noDuplicates.get(i);
070      runStatement(expectErrors, xOfy, ddl, connection);
071      if (commitOnCreateIndex && ddl.startsWith("create index ")) {
072        logger.debug("commit on create index ...");
073        connection.commit();
074      }
075    }
076
077    return noDuplicates.size();
078  }
079
080  /**
081   * Execute the statement.
082   */
083  private void runStatement(boolean expectErrors, String oneOf, String stmt, Connection c) throws SQLException {
084
085    PreparedStatement pstmt = null;
086    try {
087
088      // trim and remove trailing ; or /
089      stmt = stmt.trim();
090      if (stmt.endsWith(";")) {
091        stmt = stmt.substring(0, stmt.length() - 1);
092      } else if (stmt.endsWith("/")) {
093        stmt = stmt.substring(0, stmt.length() - 1);
094      }
095
096      if (stmt.isEmpty()) {
097        logger.debug("skip empty statement at " + oneOf);
098        return;
099      }
100
101      if (logger.isDebugEnabled()) {
102        logger.debug("executing " + oneOf + " " + getSummary(stmt));
103      }
104
105      pstmt = c.prepareStatement(stmt);
106      pstmt.execute();
107
108    } catch (SQLException e) {
109      if (expectErrors) {
110        logger.debug(" ... ignoring error executing " + getSummary(stmt) + "  error: " + e.getMessage());
111      } else {
112        String msg = "Error executing stmt[" + stmt + "] error[" + e.getMessage() + "]";
113        throw new SQLException(msg, e);
114      }
115
116    } finally {
117      if (pstmt != null) {
118        try {
119          pstmt.close();
120        } catch (SQLException e) {
121          logger.error("Error closing pstmt", e);
122        }
123      }
124    }
125  }
126
127  private String getSummary(String s) {
128    if (s.length() > 80) {
129      return s.substring(0, 80).trim() + "...";
130    }
131    return s;
132  }
133
134}