liquibase.ext.percona.PerconaRawSQLChange Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of liquibase-percona Show documentation
Show all versions of liquibase-percona Show documentation
A Liquibase extension that makes use of the percona toolkit
package liquibase.ext.percona;
/*
* 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.
*/
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import liquibase.Scope;
import liquibase.change.ChangeMetaData;
import liquibase.change.DatabaseChange;
import liquibase.change.DatabaseChangeProperty;
import liquibase.change.core.RawSQLChange;
import liquibase.database.Database;
import liquibase.exception.ValidationErrors;
import liquibase.logging.Logger;
import liquibase.statement.SqlStatement;
import liquibase.util.StringUtil;
@DatabaseChange(
name = PerconaRawSQLChange.NAME,
description = "The 'sql' tag allows you to specify whatever sql you want. It is useful for complex changes that aren't supported through Liquibase's automated refactoring tags and to work around bugs and limitations of Liquibase. The SQL contained in the sql tag can be multi-line.\n\nThe createProcedure refactoring is the best way to create stored procedures.\n\nThe 'sql' tag can also support multiline statements in the same file. Statements can either be split using a ; at the end of the last line of the SQL or a 'GO' on its own on the line between the statements can be used. Multiline SQL statements are also supported and only a ; or GO statement will finish a statement, a new line is not enough. Files containing a single statement do not need to use a ; or GO.\n\nThe sql change can also contain comments of either of the following formats:\n\nA multiline comment that starts with /* and ends with */.\nA single line comment starting with -- and finishing at the end of the line.\nNote: By default it will attempt to split statements on a ';' or 'go' at the end of lines. Because of this, if you have a comment or some other non-statement ending ';' or 'go', don't have it at the end of a line or you will get invalid SQL.",
priority = PerconaRawSQLChange.PRIORITY
)
public class PerconaRawSQLChange extends RawSQLChange implements PerconaChange {
public static final String NAME = "sql";
public static final int PRIORITY = ChangeMetaData.PRIORITY_DEFAULT + 50;
private static Logger log = Scope.getCurrentScope().getLog(PerconaRawSQLChange.class);
private String cachedTargetTableName = null;
private boolean hasCachedTargetTableName = false;
@Override
public SqlStatement[] generateStatements(Database database) {
String table = getTargetTableName();
if (table == null) {
// can't use percona toolkit for whatever reason
return super.generateStatements(database);
}
return PerconaChangeUtil.generateStatements(this,
database,
super.generateStatements(database));
}
/**
* Extracts the alter options from the sql statement. If the sql statement is not an
* alter table statement, then there are no alter options to execute and this method returns
* {@code null}.
*
* @param database the database connection
* @return the alter options to be passed to pt-osc or {@code null} if pt-osc can't be used.
*/
@Override
public String generateAlterStatement(Database database) {
String sql = getSql();
if (sql == null) {
return null;
}
String tableName = getTargetTableName();
if (tableName == null) {
return null;
}
String[] multiLineSQL = StringUtil.processMultiLineSQL(sql, true, true, getEndDelimiter());
assert multiLineSQL.length == 1;
String alterOptions = multiLineSQL[0].trim();
alterOptions = alterOptions.substring(alterOptions.indexOf(tableName) + tableName.length() + 1);
return alterOptions.trim();
}
@Override
public String getTargetDatabaseName() {
// will fall back to Database#getLiquibaseCatalogName(), see PTOnlineSchemaChangeStatement
return null;
}
/**
* Tries to determine the table name from the sql statements. This is only possible, if the statement
* begins with "alter table".
* In case, the table name could not be determined, this method will return {@code null}. That means,
* that we can't use pt-osc.
*
* @return the table name or {@code null} if the sql statement could be parsed.
*/
@Override
public String getTargetTableName() {
if (hasCachedTargetTableName) {
return cachedTargetTableName;
}
String sql = getSql();
if (sql == null) {
return setCachedTargetTableName(null);
}
if (Boolean.FALSE.equals(getUsePercona())) {
// avoids unnecessary warnings
return setCachedTargetTableName(null);
}
String[] multiLineSQL = StringUtil.processMultiLineSQL(sql, true, true, getEndDelimiter());
if (multiLineSQL.length != 1) {
log.warning("Not using percona toolkit, because multiple statements are not supported: " + sql);
return setCachedTargetTableName(null);
}
// warning: this is a very crude way of parsing the SQL statements
// e.g. a table name containing spaces will be determined wrongly: alter table `my table` add foo int null
String[] tokens = multiLineSQL[0].trim().split("\\s+");
if (tokens.length >= 3
&& "alter".equalsIgnoreCase(tokens[0])
&& "table".equalsIgnoreCase(tokens[1])) {
String table = tokens[2];
// escaped?
char firstChar = table.charAt(0);
char lastChar = table.charAt(table.length() - 1);
if (firstChar == '`' && lastChar == '`') {
table = table.substring(1, table.length() - 1);
} else if (firstChar == '`') {
// only beginning escape, no closing. See warning above.
log.warning("Not using percona toolkit, because can't parse sql statement: " + sql);
return setCachedTargetTableName(null);
}
// rename table?
if (tokens.length >= 5 && tokens[3].equalsIgnoreCase("rename")
&& !tokens[4].equalsIgnoreCase("column")
&& !tokens[4].equalsIgnoreCase("index")
&& !tokens[4].equalsIgnoreCase("key")) {
log.warning("Not using percona toolkit, because can't rename table: " + sql);
return setCachedTargetTableName(null);
}
return setCachedTargetTableName(table);
}
log.warning("Not using percona toolkit, because this sql statement is not an alter table: " + sql);
return setCachedTargetTableName(null);
}
private String setCachedTargetTableName(String targetTableName) {
cachedTargetTableName = targetTableName;
hasCachedTargetTableName = true;
return cachedTargetTableName;
}
@Override
public void setSql(String sql) {
super.setSql(sql);
cachedTargetTableName = null;
hasCachedTargetTableName = false;
}
//CPD-OFF - common PerconaChange implementation
private Boolean usePercona;
private String perconaOptions;
@Override
public String getChangeName() {
return NAME;
}
@Override
@DatabaseChangeProperty(requiredForDatabase = {})
public Boolean getUsePercona() {
return usePercona;
}
@Override
public void setUsePercona(Boolean usePercona) {
this.usePercona = usePercona;
}
@Override
@DatabaseChangeProperty(requiredForDatabase = {})
public String getPerconaOptions() {
return perconaOptions;
}
@Override
public void setPerconaOptions(String perconaOptions) {
this.perconaOptions = perconaOptions;
}
@Override
public Set getSerializableFields() {
Set fields = new HashSet<>(super.getSerializableFields());
fields.remove("usePercona");
fields.remove("perconaOptions");
return Collections.unmodifiableSet(fields);
}
@Override
public ValidationErrors validate(Database database) {
return PerconaChangeUtil.validate(super.validate(database), database);
}
//CPD-ON
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy