/*
* Copyright 2010-2017 Boxfuse GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.flywaydb.core.internal.dbsupport;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.internal.util.PlaceholderReplacer;
import org.flywaydb.core.internal.util.StringUtils;
import org.flywaydb.core.internal.util.logging.Log;
import org.flywaydb.core.internal.util.logging.LogFactory;
import org.flywaydb.core.internal.util.scanner.Resource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Sql script containing a series of statements terminated by a delimiter (eg: ;).
* Single-line (--) and multi-line (/* * /) comments are stripped and ignored.
*/
public class SqlScript {
private static final Log LOG = LogFactory.getLog(SqlScript.class);
/**
* The database-specific support.
*/
private final DbSupport dbSupport;
/**
* Whether to allow mixing transactional and non-transactional statements within the same migration.
*/
private final boolean mixed;
/**
* The sql statements contained in this script.
*/
private final List<SqlStatement> sqlStatements;
/**
* The resource containing the statements.
*/
private final Resource resource;
/**
* Whether this SQL script contains at least one transactional statement.
*/
private boolean transactionalStatementFound;
/**
* Whether this SQL script contains at least one non-transactional statement.
*/
private boolean nonTransactionalStatementFound;
/**
* Creates a new sql script from this source.
*
* @param sqlScriptSource The sql script as a text block with all placeholders already replaced.
* @param dbSupport The database-specific support.
*/
public SqlScript(String sqlScriptSource, DbSupport dbSupport) {
this.dbSupport = dbSupport;
this.mixed = false;
this.sqlStatements = parse(sqlScriptSource);
this.resource = null;
}
/**
* Creates a new sql script from this resource.
*
* @param dbSupport The database-specific support.
* @param sqlScriptResource The resource containing the statements.
* @param placeholderReplacer The placeholder replacer.
* @param encoding The encoding to use.
* @param mixed Whether to allow mixing transactional and non-transactional statements within the same migration.
*/
public SqlScript(DbSupport dbSupport, Resource sqlScriptResource, PlaceholderReplacer placeholderReplacer, String encoding, boolean mixed) {
this.dbSupport = dbSupport;
this.mixed = mixed;
String sqlScriptSource = sqlScriptResource.loadAsString(encoding);
this.sqlStatements = parse(placeholderReplacer.replacePlaceholders(sqlScriptSource));
this.resource = sqlScriptResource;
}
/**
* Whether the execution should take place inside a transaction. This is useful for databases
* like PostgreSQL where certain statement can only execute outside a transaction.
*
* @return {@code true} if a transaction should be used (highly recommended), or {@code false} if not.
*/
public boolean executeInTransaction() {
return !nonTransactionalStatementFound;
}
/**
* For increased testability.
*
* @return The sql statements contained in this script.
*/
public List<SqlStatement> getSqlStatements() {
return sqlStatements;
}
/**
* @return The resource containing the statements.
*/
public Resource getResource() {
return resource;
}
/**
* Executes this script against the database.
*
* @param jdbcTemplate The jdbc template to use to execute this script.
*/
public void execute(final JdbcTemplate jdbcTemplate) {
for (SqlStatement sqlStatement : sqlStatements) {
String sql = sqlStatement.getSql();
LOG.debug("Executing SQL: " + sql);
try {
if (sqlStatement.isPgCopy()) {
dbSupport.executePgCopy(jdbcTemplate.getConnection(), sql);
} else {
jdbcTemplate.executeStatement(sql);
}
} catch (SQLException e) {
throw new FlywaySqlScriptException(resource, sqlStatement, e);
}
}
}
/**
* Parses this script source into statements.
*
* @param sqlScriptSource The script source to parse.
* @return The parsed statements.
*/
/* private -> for testing */
List<SqlStatement> parse(String sqlScriptSource) {
return linesToStatements(readLines(new StringReader(sqlScriptSource)));
}
/**
* Turns these lines in a series of statements.
*
* @param lines The lines to analyse.
* @return The statements contained in these lines (in order).
*/
/* private -> for testing */
List<SqlStatement> linesToStatements(List<String> lines) {
List<SqlStatement> statements = new ArrayList<SqlStatement>();
Delimiter nonStandardDelimiter = null;
SqlStatementBuilder sqlStatementBuilder = dbSupport.createSqlStatementBuilder();
for (int lineNumber = 1; lineNumber <= lines.size(); lineNumber++) {
String line = lines.get(lineNumber - 1);
if (sqlStatementBuilder.isEmpty()) {
if (!StringUtils.hasText(line)) {
// Skip empty line between statements.
continue;
}
Delimiter newDelimiter = sqlStatementBuilder.extractNewDelimiterFromLine(line);
if (newDelimiter != null) {
nonStandardDelimiter = newDelimiter;
// Skip this line as it was an explicit delimiter change directive outside of any statements.
continue;
}
sqlStatementBuilder.setLineNumber(lineNumber);
// Start a new statement, marking it with this line number.
if (nonStandardDelimiter != null) {
sqlStatementBuilder.setDelimiter(nonStandardDelimiter);
}
}
sqlStatementBuilder.addLine(line);
if (sqlStatementBuilder.canDiscard()) {
sqlStatementBuilder = dbSupport.createSqlStatementBuilder();
} else if (sqlStatementBuilder.isTerminated()) {
addStatement(statements, sqlStatementBuilder);
sqlStatementBuilder = dbSupport.createSqlStatementBuilder();
}
}
// Catch any statements not followed by delimiter.
if (!sqlStatementBuilder.isEmpty()) {
addStatement(statements, sqlStatementBuilder);
}
return statements;
}
private void addStatement(List<SqlStatement> statements, SqlStatementBuilder sqlStatementBuilder) {
SqlStatement sqlStatement = sqlStatementBuilder.getSqlStatement();
statements.add(sqlStatement);
if (sqlStatementBuilder.executeInTransaction()) {
transactionalStatementFound = true;
} else {
nonTransactionalStatementFound = true;
}
if (!mixed && transactionalStatementFound && nonTransactionalStatementFound) {
throw new FlywayException(
"Detected both transactional and non-transactional statements within the same migration"
+ " (even though mixed is false). Offending statement found at line "
+ sqlStatement.getLineNumber() + ": " + sqlStatement.getSql()
+ (sqlStatementBuilder.executeInTransaction() ? "" : " [non-transactional]"));
}
LOG.debug("Found statement at line " + sqlStatement.getLineNumber() + ": " + sqlStatement.getSql() + (sqlStatementBuilder.executeInTransaction() ? "" : " [non-transactional]"));
}
/**
* Parses the textual data provided by this reader into a list of lines.
*
* @param reader The reader for the textual data.
* @return The list of lines (in order).
* @throws IllegalStateException Thrown when the textual data parsing failed.
*/
private List<String> readLines(Reader reader) {
List<String> lines = new ArrayList<String>();
BufferedReader bufferedReader = new BufferedReader(reader);
String line;
try {
while ((line = bufferedReader.readLine()) != null) {
lines.add(line);
}
} catch (IOException e) {
String message = resource == null ?
"Unable to parse lines" :
"Unable to parse " + resource.getLocation() + " (" + resource.getLocationOnDisk() + ")";
throw new FlywayException(message, e);
}
return lines;
}
}