liquibase.diff.output.changelog.DiffToChangeLog Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of liquibase-core Show documentation
Show all versions of liquibase-core Show documentation
Liquibase is a tool for managing and executing database changes.
The newest version!
package liquibase.diff.output.changelog;
import liquibase.GlobalConfiguration;
import liquibase.Labels;
import liquibase.Scope;
import liquibase.change.Change;
import liquibase.change.ReplaceIfExists;
import liquibase.change.core.*;
import liquibase.changelog.ChangeSet;
import liquibase.changeset.ChangeSetService;
import liquibase.changeset.ChangeSetServiceFactory;
import liquibase.command.core.helpers.AbstractChangelogCommandStep;
import liquibase.configuration.core.DeprecatedConfigurationValueProvider;
import liquibase.database.*;
import liquibase.database.core.*;
import liquibase.diff.DiffResult;
import liquibase.diff.ObjectDifferences;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.compare.DatabaseObjectCollectionComparator;
import liquibase.diff.output.DiffOutputControl;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.resource.OpenOptions;
import liquibase.resource.PathHandlerFactory;
import liquibase.resource.Resource;
import liquibase.serializer.ChangeLogSerializer;
import liquibase.serializer.ChangeLogSerializerFactory;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.EmptyDatabaseSnapshot;
import liquibase.statement.core.RawParameterizedSqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Column;
import liquibase.structure.core.StoredDatabaseLogic;
import liquibase.structure.core.Table;
import liquibase.util.DependencyUtil;
import liquibase.util.StreamUtil;
import liquibase.util.StringUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemProperties;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class DiffToChangeLog {
public static final String ORDER_ATTRIBUTE = "order";
public static final String DATABASE_CHANGE_LOG_CLOSING_XML_TAG = "";
public static final String EXTERNAL_FILE_DIR_SCOPE_KEY = "DiffToChangeLog.externalFilesDir";
public static final String DIFF_OUTPUT_CONTROL_SCOPE_KEY = "diffOutputControl";
public static final String DIFF_SNAPSHOT_DATABASE = "snapshotDatabase";
private String idRoot = String.valueOf(new Date().getTime());
private boolean overriddenIdRoot;
private int changeNumber = 1;
private String changeSetContext;
private String changeSetLabels;
private String changeSetAuthor;
private String changeSetPath;
private String[] changeSetRunOnChangeTypes;
private String[] changeReplaceIfExistsTypes;
private DiffResult diffResult;
private final DiffOutputControl diffOutputControl;
private boolean tryDbaDependencies = true;
private boolean skipObjectSorting = false;
private static final Set loggedOrderFor = new HashSet<>();
/**
* Creates a new DiffToChangeLog with the given DiffResult and default DiffOutputControl
* @param diffResult the DiffResult to convert to a ChangeLog
* @param diffOutputControl the DiffOutputControl to use to control the output
* @param skipObjectSorting if true, will skip dependency object sorting. This can be useful on databases that have a lot of packages/procedures that are linked to each other
*/
public DiffToChangeLog(DiffResult diffResult, DiffOutputControl diffOutputControl, boolean skipObjectSorting) {
this(diffResult, diffOutputControl);
this.skipObjectSorting = skipObjectSorting;
}
public DiffToChangeLog(DiffResult diffResult, DiffOutputControl diffOutputControl) {
this.diffResult = diffResult;
this.diffOutputControl = diffOutputControl;
respectSchemaAndCatalogCaseIfNeeded(diffOutputControl);
}
public DiffToChangeLog(DiffOutputControl diffOutputControl) {
this.diffOutputControl = diffOutputControl;
}
private void respectSchemaAndCatalogCaseIfNeeded(DiffOutputControl diffOutputControl) {
if (this.diffResult.getComparisonSnapshot().getDatabase() instanceof AbstractDb2Database) {
diffOutputControl.setRespectSchemaAndCatalogCase(true);
}
}
public void setDiffResult(DiffResult diffResult) {
this.diffResult = diffResult;
}
public void setChangeSetContext(String changeSetContext) {
this.changeSetContext = changeSetContext;
}
public void setChangeSetLabels(String changeSetLabels) {
this.changeSetLabels = changeSetLabels;
}
public void print(String changeLogFile) throws ParserConfigurationException, IOException, DatabaseException {
this.print(changeLogFile, false);
}
public void print(String changeLogFile, Boolean overwriteOutputFile) throws ParserConfigurationException, IOException, DatabaseException {
this.changeSetPath = changeLogFile;
ChangeLogSerializer changeLogSerializer = ChangeLogSerializerFactory.getInstance().getSerializer(changeLogFile);
this.print(changeLogFile, changeLogSerializer, overwriteOutputFile);
}
public void print(PrintStream out) throws ParserConfigurationException, IOException, DatabaseException {
this.print(out, ChangeLogSerializerFactory.getInstance().getSerializer("xml"));
}
public void print(String changeLogFile, ChangeLogSerializer changeLogSerializer) throws ParserConfigurationException, IOException, DatabaseException {
this.print(changeLogFile, changeLogSerializer, false);
}
public void print(String changeLogFile, ChangeLogSerializer changeLogSerializer, Boolean overwriteOutputFile) throws ParserConfigurationException, IOException, DatabaseException {
this.changeSetPath = changeLogFile;
final PathHandlerFactory pathHandlerFactory = Scope.getCurrentScope().getSingleton(PathHandlerFactory.class);
Resource file = pathHandlerFactory.getResource(changeLogFile);
final Map newScopeObjects = new HashMap<>();
Resource objectsDir = null;
if (changeLogFile.toLowerCase().endsWith("sql")) {
DeprecatedConfigurationValueProvider.setData("liquibase.pro.sql.inline", "true");
} else if (this.diffResult.getComparisonSnapshot() instanceof EmptyDatabaseSnapshot) {
objectsDir = file.resolveSibling("objects");
} else {
objectsDir = file.resolveSibling("objects-" + new Date().getTime());
}
if (objectsDir != null) {
if (objectsDir.exists()) {
throw new UnexpectedLiquibaseException("The generatechangelog command would overwrite your existing stored logic files. To run this command please remove or rename the '"+objectsDir+"' dir");
}
newScopeObjects.put(EXTERNAL_FILE_DIR_SCOPE_KEY, objectsDir);
}
newScopeObjects.put(DIFF_OUTPUT_CONTROL_SCOPE_KEY, diffOutputControl);
try {
//
// Get a Database instance and save it in the scope for later use
//
Database database = determineDatabase(diffResult.getReferenceSnapshot());
if (database == null) {
database = determineDatabase(diffResult.getComparisonSnapshot());
}
newScopeObjects.put(DIFF_SNAPSHOT_DATABASE, database);
Scope.child(newScopeObjects, new Scope.ScopedRunner() {
@Override
public void run() {
try {
if (!file.exists()) {
//print changeLog only if there are available changeSets to print instead of printing it always
printNew(changeLogSerializer, file);
} else {
StringBuilder fileContents = new StringBuilder();
ByteArrayOutputStream out = new ByteArrayOutputStream();
print(new PrintStream(out, true, GlobalConfiguration.OUTPUT_FILE_ENCODING.getCurrentValue()), changeLogSerializer);
String xml = new String(out.toByteArray(), GlobalConfiguration.OUTPUT_FILE_ENCODING.getCurrentValue());
if (overwriteOutputFile) {
// write xml contents to file
Scope.getCurrentScope().getLog(getClass()).info(file.getUri() + " exists, overwriting");
fileContents.append(xml);
} else {
// read existing file
Scope.getCurrentScope().getLog(getClass()).info(file.getUri() + " exists, appending");
fileContents = new StringBuilder(StreamUtil.readStreamAsString(file.openInputStream()));
String innerXml = xml.replaceFirst("(?ms).*]*>", "");
innerXml = innerXml.replaceFirst(DATABASE_CHANGE_LOG_CLOSING_XML_TAG, "");
innerXml = innerXml.trim();
if (innerXml.isEmpty()) {
Scope.getCurrentScope().getLog(getClass()).info("No changes found, nothing to do");
return;
}
// insert new XML
int endTagIndex = fileContents.indexOf(DATABASE_CHANGE_LOG_CLOSING_XML_TAG);
if (endTagIndex == -1) {
fileContents.append(xml);
} else {
String lineSeparator = GlobalConfiguration.OUTPUT_LINE_SEPARATOR.getCurrentValue();
String toInsert = " " + innerXml + lineSeparator;
fileContents.insert(endTagIndex, toInsert);
}
}
try (OutputStream outputStream = file.openOutputStream(new OpenOptions())) {
outputStream.write(fileContents.toString().getBytes());
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
} catch (Exception e) {
//rethrow known exceptions. TODO: Fix this up with final Scope API
final Throwable cause = e.getCause();
if (cause instanceof ParserConfigurationException) {
throw (ParserConfigurationException) cause;
}
if (cause instanceof IOException) {
throw (IOException) cause;
}
if (cause instanceof DatabaseException) {
throw (DatabaseException) cause;
}
throw new RuntimeException(e);
}
}
//
// Return the Database from this snapshot
// if it is not offline
//
private Database determineDatabase(DatabaseSnapshot snapshot) {
Database database = snapshot.getDatabase();
DatabaseConnection connection = database.getConnection();
if (! (connection instanceof OfflineConnection) && database instanceof PostgresDatabase) {
return database;
}
return null;
}
/**
* Prints changeLog that would bring the target database to be the same as
* the reference database
*/
public void printNew(ChangeLogSerializer changeLogSerializer, Resource file) throws ParserConfigurationException, IOException, DatabaseException {
List changeSets = generateChangeSets();
Scope.getCurrentScope().getLog(getClass()).info("changeSets count: " + changeSets.size());
if (changeSets.isEmpty()) {
Scope.getCurrentScope().getLog(getClass()).info("No changesets to add to the changelog output.");
} else {
Scope.getCurrentScope().getLog(getClass()).info(file + " does not exist, creating and adding " + changeSets.size() + " changesets.");
try (OutputStream stream = file.openOutputStream(new OpenOptions());
PrintStream out = new PrintStream(stream, true, GlobalConfiguration.OUTPUT_FILE_ENCODING.getCurrentValue())) {
changeLogSerializer.write(changeSets, out);
}
}
}
/**
* Prints changeLog that would bring the target database to be the same as
* the reference database
*/
public void print(final PrintStream out, final ChangeLogSerializer changeLogSerializer) throws ParserConfigurationException, IOException, DatabaseException {
List changeSets = generateChangeSets();
changeLogSerializer.write(changeSets, out);
out.flush();
}
public List generateChangeSets() {
final ChangeGeneratorFactory changeGeneratorFactory = ChangeGeneratorFactory.getInstance();
DatabaseObjectCollectionComparator comparator = new DatabaseObjectCollectionComparator();
String created = null;
if (GlobalConfiguration.GENERATE_CHANGESET_CREATED_VALUES.getCurrentValue()) {
created = new SimpleDateFormat("yyyy-MM-dd HH:mmZ").format(new Date());
}
List> types = getOrderedOutputTypes(ChangedObjectChangeGenerator.class);
List updateChangeSets = new ArrayList<>();
// Keep a reference to DiffResult in the comparision database so that it can be retrieved later
// This is to avoid changing the MissingObjectChangeGenerator API and still be able to pass the
// initial DiffResult Object which can be used to check for the objects available in the database
// without doing any expensive db calls. Example usage is in MissingUniqueConstraintChangeGenerator#alreadyExists()
Database comparisonDatabase = diffResult.getComparisonSnapshot().getDatabase();
if (comparisonDatabase instanceof AbstractJdbcDatabase) {
((AbstractJdbcDatabase) comparisonDatabase).set("diffResult", diffResult);
}
for (Class extends DatabaseObject> type : types) {
ObjectQuotingStrategy quotingStrategy = diffOutputControl.getObjectQuotingStrategy();
for (Map.Entry extends DatabaseObject, ObjectDifferences> entry : diffResult.getChangedObjects(type, comparator).entrySet()) {
if (!diffResult.getReferenceSnapshot().getDatabase().isLiquibaseObject(entry.getKey()) && !diffResult.getReferenceSnapshot().getDatabase().isSystemObject(entry.getKey())) {
Change[] changes = changeGeneratorFactory.fixChanged(entry.getKey(), entry.getValue(), diffOutputControl, diffResult.getReferenceSnapshot().getDatabase(), diffResult.getComparisonSnapshot().getDatabase());
setReplaceIfExistsTrueIfApplicable(changes);
addToChangeSets(changes, updateChangeSets, quotingStrategy, created);
}
}
}
types = getOrderedOutputTypes(MissingObjectChangeGenerator.class);
List missingObjects = new ArrayList<>();
for (Class extends DatabaseObject> type : types) {
for (DatabaseObject object : diffResult.getMissingObjects(type, getDatabaseObjectCollectionComparator())) {
if (object == null) {
continue;
}
if (!diffResult.getReferenceSnapshot().getDatabase().isLiquibaseObject(object) && !diffResult.getReferenceSnapshot().getDatabase().isSystemObject(object)) {
missingObjects.add(object);
}
}
}
List createChangeSets = new ArrayList<>();
for (DatabaseObject object : sortMissingObjects(missingObjects, diffResult.getReferenceSnapshot().getDatabase())) {
ObjectQuotingStrategy quotingStrategy = diffOutputControl.getObjectQuotingStrategy();
Change[] changes = changeGeneratorFactory.fixMissing(object, diffOutputControl, diffResult.getReferenceSnapshot().getDatabase(), diffResult.getComparisonSnapshot().getDatabase());
setReplaceIfExistsTrueIfApplicable(changes);
addToChangeSets(changes, createChangeSets, quotingStrategy, created);
}
List deleteChangeSets = new ArrayList<>();
types = getOrderedOutputTypes(UnexpectedObjectChangeGenerator.class);
for (Class extends DatabaseObject> type : types) {
ObjectQuotingStrategy quotingStrategy = diffOutputControl.getObjectQuotingStrategy();
for (DatabaseObject object : sortUnexpectedObjects(diffResult.getUnexpectedObjects(type, comparator), diffResult.getReferenceSnapshot().getDatabase())) {
if (!diffResult.getComparisonSnapshot().getDatabase().isLiquibaseObject(object) && !diffResult.getComparisonSnapshot().getDatabase().isSystemObject(object)) {
Change[] changes = changeGeneratorFactory.fixUnexpected(object, diffOutputControl, diffResult.getReferenceSnapshot().getDatabase(), diffResult.getComparisonSnapshot().getDatabase());
setReplaceIfExistsTrueIfApplicable(changes);
addToChangeSets(changes, deleteChangeSets, quotingStrategy, created);
}
}
}
// remove the diffResult from the database object
if (comparisonDatabase instanceof AbstractJdbcDatabase) {
((AbstractJdbcDatabase) comparisonDatabase).set("diffResult", null);
}
List changeSets = new ArrayList<>();
changeSets.addAll(createChangeSets);
changeSets.addAll(deleteChangeSets);
changeSets.addAll(updateChangeSets);
changeSets = bringDropFKToTop(changeSets);
return changeSets;
}
private void setReplaceIfExistsTrueIfApplicable(Change[] changes) {
if (changes !=null && diffOutputControl.isReplaceIfExistsSet()) {
for (Change change : changes) {
if (change instanceof ReplaceIfExists) {
((ReplaceIfExists) change).setReplaceIfExists(true);
}
}
}
}
//
// Because the generated changeset list can contain both add and drop
// FK changes with the same constraint name, we make sure that the
// drop FK goes first
//
private List bringDropFKToTop(List changeSets) {
List dropFk = changeSets.stream().filter(cs ->
cs.getChanges().stream().anyMatch(DropForeignKeyConstraintChange.class::isInstance)
).collect(Collectors.toList());
if (dropFk.isEmpty()) {
return changeSets;
}
List returnList = new ArrayList<>();
changeSets.stream().forEach(cs -> {
if (dropFk.contains(cs)) {
returnList.add(cs);
}
});
changeSets.stream().forEach(cs -> {
if (! dropFk.contains(cs)) {
returnList.add(cs);
}
});
return returnList;
}
private DatabaseObjectCollectionComparator getDatabaseObjectCollectionComparator() {
return new DatabaseObjectCollectionComparator() {
@Override
public int compare(DatabaseObject o1, DatabaseObject o2) {
if (o1 instanceof Column && o1.getAttribute(ORDER_ATTRIBUTE, Integer.class) != null && o2.getAttribute(ORDER_ATTRIBUTE, Integer.class) != null) {
int i = o1.getAttribute(ORDER_ATTRIBUTE, Integer.class).compareTo(o2.getAttribute(ORDER_ATTRIBUTE, Integer.class));
if (i != 0) {
return i;
}
} else if (o1 instanceof StoredDatabaseLogic) {
if (o1.getAttribute(ORDER_ATTRIBUTE, Integer.class) != null && o2.getAttribute(ORDER_ATTRIBUTE, Integer.class) != null) {
int order = o1.getAttribute(ORDER_ATTRIBUTE, Long.class).compareTo(o2.getAttribute(ORDER_ATTRIBUTE, Long.class));
if (order != 0) {
return order;
}
}
}
return super.compare(o1, o2);
}
};
}
private List sortUnexpectedObjects(Collection extends DatabaseObject> unexpectedObjects, Database database) {
return sortObjects("unexpected", (Collection) unexpectedObjects, database);
}
private List sortMissingObjects(Collection missingObjects, Database database) {
return sortObjects("missing", missingObjects, database);
}
private List sortObjects(final String type, Collection objects, Database database) {
if (!objects.isEmpty() && supportsSortingObjects(database) && (database.getConnection() != null) && !(database.getConnection() instanceof OfflineConnection)) {
List schemas = new ArrayList<>();
CompareControl.SchemaComparison[] schemaComparisons = this.diffOutputControl.getSchemaComparisons();
if (schemaComparisons != null) {
for (CompareControl.SchemaComparison comparison : schemaComparisons) {
String schemaName = comparison.getReferenceSchema().getSchemaName();
if (schemaName == null) {
schemaName = database.getDefaultSchemaName();
}
schemas.add(schemaName);
}
}
if (schemas.isEmpty()) {
schemas.add(database.getDefaultSchemaName());
}
try {
final List dependencyOrder = new ArrayList<>();
DependencyUtil.NodeValueListener nameListener = dependencyOrder::add;
DependencyUtil.DependencyGraph graph = new DependencyUtil.DependencyGraph<>(nameListener);
addDependencies(graph, schemas, database);
graph.computeDependencies();
if (!dependencyOrder.isEmpty()) {
final List toSort = new ArrayList<>();
final List toNotSort = new ArrayList<>();
for (DatabaseObject obj : objects) {
if (!(obj instanceof Column)) {
String schemaName = null;
if (obj.getSchema() != null) {
schemaName = obj.getSchema().getName();
}
String objectName = obj.getName();
String name = schemaName + "." + objectName;
if (dependencyOrder.contains(name) ||
dependencyOrder.contains(convertStoredLogicObjectName(schemaName, objectName, database))) {
toSort.add(obj);
} else {
toNotSort.add(obj);
}
} else {
toNotSort.add(obj);
}
}
toSort.sort((o1, o2) -> {
//
// For Postgres, make tables appear before stored logic
//
if (database instanceof PostgresDatabase) {
Integer x = determineOrderingForTablesAndStoredLogic(o1, o2);
if (x != null) {
return x;
}
}
String o1Schema = null;
if (o1.getSchema() != null) {
o1Schema = o1.getSchema().getName();
}
String o2Schema = null;
if (o2.getSchema() != null) {
o2Schema = o2.getSchema().getName();
}
Integer o1Order = dependencyOrder.indexOf(o1Schema + "." + o1.getName());
int o2Order = dependencyOrder.indexOf(o2Schema + "." + o2.getName());
int order = o1Order.compareTo(o2Order);
if ("unexpected".equals(type)) {
order = order * -1;
}
return order;
});
toSort.addAll(toNotSort);
return toSort;
}
} catch (DatabaseException e) {
Scope.getCurrentScope().getLog(getClass()).fine("Cannot get object dependencies: " + e.getMessage());
} catch (StackOverflowError e) {
Scope.getCurrentScope().getLog(getClass()).warning("You have too many or recursive database object dependencies! " +
"Liquibase is going to ignore dependency sorting and resume processing. To skip this message " +
"(and save a lot of processing time) use flag " + AbstractChangelogCommandStep.SKIP_OBJECT_SORTING.getName(), e);
}
}
return new ArrayList<>(objects);
}
private static Integer determineOrderingForTablesAndStoredLogic(DatabaseObject o1, DatabaseObject o2) {
if (o1 instanceof Table && o2 instanceof StoredDatabaseLogic) {
return -1;
}
if (o1 instanceof StoredDatabaseLogic && o2 instanceof Table) {
return 1;
}
return null;
}
/**
*
* POSTGRES ONLY:
*
* If we have a stored logic object then we edit the name
* to replace the parameter list with a list of just the types.
* This is the format that the dependency computation puts out.
*
* Example: calculate_bonus(emp_salary numeric, emp_name character varying) becomes
* calculate_bonus(numeric, character varying)
*
* @param objectName The input object name to work on
* @return String
*
*/
private static String convertStoredLogicObjectName(String schemaName, String objectName, Database database) {
String name = schemaName + "." + objectName;
if (! (database instanceof PostgresDatabase) || ! (objectName.contains("(") && objectName.contains(")"))) {
return name;
}
Pattern p = Pattern.compile(".*?[(]+(.*)?[)]+[\\s]*?$");
Matcher m = p.matcher(objectName);
if (m.matches()) {
String originalParameters = m.group(1);
String editedParameters = originalParameters;
String[] parameters = m.group(1).split(",");
for (String parameter : parameters) {
parameter = parameter.trim();
String[] parts = parameter.split(" ");
String[] rest = Arrays.copyOfRange(parts, 1, parts.length);
String part = StringUtil.join(rest, " ");
editedParameters = editedParameters.replace(parameter, part);
}
name = schemaName + "." + objectName.replace(originalParameters, editedParameters)
.replace(", ",",");
}
return name;
}
private List