[jOOQ/jOOQ#9506] Implement Settings.migrationRevertUntracked

This commit is contained in:
Lukas Eder 2019-12-17 14:58:08 +01:00
parent 5d0e9226ab
commit a5ea43923e
5 changed files with 187 additions and 81 deletions

View File

@ -198,6 +198,8 @@ public class Settings
protected Locale interpreterLocale;
@XmlElement(defaultValue = "false")
protected Boolean migrationAllowsUndo = false;
@XmlElement(defaultValue = "false")
protected Boolean migrationRevertUntracked = false;
@XmlElement(defaultValue = "true")
protected Boolean migrationAutoValidation = true;
@XmlElement(type = String.class)
@ -1685,6 +1687,30 @@ public class Settings
this.migrationAllowsUndo = value;
}
/**
* Whether migrations revert any untracked changes in the schemas that are being migrated.<p><strong>This is a potentially destructive feature, which should not be turned on in production</strong>. It is useful mostly to quickly revert any elements created in a development environment. This feature is available only in commercial distributions.
*
* @return
* possible object is
* {@link Boolean }
*
*/
public Boolean isMigrationRevertUntracked() {
return migrationRevertUntracked;
}
/**
* Sets the value of the migrationRevertUntracked property.
*
* @param value
* allowed object is
* {@link Boolean }
*
*/
public void setMigrationRevertUntracked(Boolean value) {
this.migrationRevertUntracked = value;
}
/**
* Whether a migration automatically runs a validation first.
*
@ -2438,6 +2464,11 @@ public class Settings
return this;
}
public Settings withMigrationRevertUntracked(Boolean value) {
setMigrationRevertUntracked(value);
return this;
}
public Settings withMigrationAutoValidation(Boolean value) {
setMigrationAutoValidation(value);
return this;
@ -2640,6 +2671,7 @@ public class Settings
builder.append("interpreterNameLookupCaseSensitivity", interpreterNameLookupCaseSensitivity);
builder.append("interpreterLocale", interpreterLocale);
builder.append("migrationAllowsUndo", migrationAllowsUndo);
builder.append("migrationRevertUntracked", migrationRevertUntracked);
builder.append("migrationAutoValidation", migrationAutoValidation);
builder.append("locale", locale);
builder.append("parseDialect", parseDialect);
@ -3277,6 +3309,15 @@ public class Settings
return false;
}
}
if (migrationRevertUntracked == null) {
if (other.migrationRevertUntracked!= null) {
return false;
}
} else {
if (!migrationRevertUntracked.equals(other.migrationRevertUntracked)) {
return false;
}
}
if (migrationAutoValidation == null) {
if (other.migrationAutoValidation!= null) {
return false;
@ -3468,6 +3509,7 @@ public class Settings
result = ((prime*result)+((interpreterNameLookupCaseSensitivity == null)? 0 :interpreterNameLookupCaseSensitivity.hashCode()));
result = ((prime*result)+((interpreterLocale == null)? 0 :interpreterLocale.hashCode()));
result = ((prime*result)+((migrationAllowsUndo == null)? 0 :migrationAllowsUndo.hashCode()));
result = ((prime*result)+((migrationRevertUntracked == null)? 0 :migrationRevertUntracked.hashCode()));
result = ((prime*result)+((migrationAutoValidation == null)? 0 :migrationAutoValidation.hashCode()));
result = ((prime*result)+((locale == null)? 0 :locale.hashCode()));
result = ((prime*result)+((parseDialect == null)? 0 :parseDialect.hashCode()));

View File

@ -44,7 +44,7 @@ import org.jooq.Migration;
*
* @author Lukas Eder
*/
public class DataMigrationValidationException extends DataAccessException {
public class DataMigrationValidationException extends DataMigrationException {
/**
* Generated UID

View File

@ -50,18 +50,20 @@ import org.jooq.Version;
*/
final class DefaultMigrationContext extends AbstractScope implements MigrationContext {
private final Version migrationFrom;
private final Version migrationTo;
private final Queries migrationQueries;
final Version migrationFrom;
final Version migrationTo;
final Queries migrationQueries;
final Queries revertUntrackedQueries;
private Query query;
Query query;
DefaultMigrationContext(Configuration configuration, Version migrationFrom, Version migrationTo, Queries migrationQueries) {
DefaultMigrationContext(Configuration configuration, Version migrationFrom, Version migrationTo, Queries migrationQueries, Queries revertUntrackedQueries) {
super(configuration);
this.migrationFrom = migrationFrom;
this.migrationTo = migrationTo;
this.migrationQueries = migrationQueries;
this.revertUntrackedQueries = revertUntrackedQueries;
}
@Override

View File

@ -38,11 +38,18 @@
package org.jooq.impl;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.jooq.impl.DSL.createSchemaIfNotExists;
import static org.jooq.impl.DSL.dropSchemaIfExists;
import static org.jooq.impl.DSL.dropTableIfExists;
import static org.jooq.impl.DSL.inline;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.schema;
import static org.jooq.impl.MigrationImpl.Status.FAILURE;
import static org.jooq.impl.MigrationImpl.Status.MIGRATING;
import static org.jooq.impl.MigrationImpl.Status.REVERTING;
import static org.jooq.impl.MigrationImpl.Status.STARTING;
import static org.jooq.impl.MigrationImpl.Status.SUCCESS;
import java.sql.Timestamp;
import java.util.Arrays;
@ -133,6 +140,10 @@ final class MigrationImpl extends AbstractScope implements Migration {
@Override
public final void validate() {
validate0(migrationContext());
}
private final void validate0(DefaultMigrationContext ctx) {
JooqMigrationsChangelogRecord currentRecord = currentChangelogRecord();
if (currentRecord != null) {
@ -144,7 +155,7 @@ final class MigrationImpl extends AbstractScope implements Migration {
validateVersionProvider(from());
validateVersionProvider(to());
validateUnexpectedObjects();
revertUntracked(ctx, null);
}
private final void validateVersionProvider(Version version) {
@ -152,34 +163,6 @@ final class MigrationImpl extends AbstractScope implements Migration {
throw new DataMigrationValidationException("Version is not available from VersionProvider: " + version.id());
}
private final void validateUnexpectedObjects() {
Version currentVersion = currentVersion();
Meta currentMeta = currentVersion.meta();
Set<Schema> expectedSchemas = new HashSet<>();
expectedSchemas.addAll(lookup(from().meta().getSchemas()));
expectedSchemas.addAll(lookup(to().meta().getSchemas()));
// TODO Add a settings governing what schemas we're including in the migration
// The current implementation will default to migrating all schemas that are
// touched by the from() or to() version
Meta existingMeta = dsl().meta();
for (Schema schema : existingMeta.getSchemas()) {
// TODO Why is this qualification necessary?
existingMeta = existingMeta.apply(dropTableIfExists(schema.getQualifiedName().append(CHANGELOG.getUnqualifiedName())).cascade());
if (!expectedSchemas.contains(schema))
existingMeta = existingMeta.apply(dropSchemaIfExists(schema).cascade());
else
currentMeta = currentMeta.apply(createSchemaIfNotExists(schema));
}
Queries diff = currentMeta.migrateTo(existingMeta);
if (diff.queries().length > 0)
throw new DataMigrationValidationException("Non-empty difference between actual schema and migration from schema: " + diff);
}
private final Collection<Schema> lookup(List<Schema> schemas) {
// TODO: Refactor usages of getInterpreterSearchPath()
@ -200,6 +183,44 @@ final class MigrationImpl extends AbstractScope implements Migration {
return result;
}
private final Queries revertUntrackedQueries() {
Version currentVersion = currentVersion();
Meta currentMeta = currentVersion.meta();
Set<Schema> expectedSchemas = new HashSet<>();
expectedSchemas.addAll(lookup(from().meta().getSchemas()));
expectedSchemas.addAll(lookup(to().meta().getSchemas()));
// TODO Add a settings governing what schemas we're including in the migration
// The current implementation will default to migrating all schemas that are
// touched by the from() or to() version
Meta existingMeta = dsl().meta();
for (Schema schema : existingMeta.getSchemas()) {
// TODO Why is this qualification necessary?
existingMeta = existingMeta.apply(dropTableIfExists(schema.getQualifiedName().append(CHANGELOG.getUnqualifiedName())).cascade());
if (!expectedSchemas.contains(schema))
existingMeta = existingMeta.apply(dropSchemaIfExists(schema).cascade());
else
currentMeta = currentMeta.apply(createSchemaIfNotExists(schema));
}
return existingMeta.migrateTo(currentMeta);
}
private final void revertUntracked(DefaultMigrationContext ctx, MigrationListener listener) {
if (ctx.revertUntrackedQueries.queries().length > 0)
if (!TRUE.equals(dsl().settings().isMigrationRevertUntracked()))
throw new DataMigrationValidationException("Non-empty difference between actual schema and migration from schema: " + ctx.revertUntrackedQueries);
else if (listener != null)
execute(ctx, listener, ctx.revertUntrackedQueries);
}
private final DefaultMigrationContext migrationContext() {
return new DefaultMigrationContext(configuration(), from(), to(), queries(), revertUntrackedQueries());
}
private static final MigrationResult MIGRATION_RESULT = new MigrationResult() {};
@Override
@ -210,12 +231,12 @@ final class MigrationImpl extends AbstractScope implements Migration {
return run(new ContextTransactionalCallable<MigrationResult>() {
@Override
public MigrationResult run() {
if (!FALSE.equals(dsl().settings().isMigrationAutoValidation()))
validate();
DefaultMigrationContext ctx = new DefaultMigrationContext(configuration(), from(), to(), queries());
DefaultMigrationContext ctx = migrationContext();
MigrationListener listener = new MigrationListeners(configuration);
if (!FALSE.equals(dsl().settings().isMigrationAutoValidation()))
validate0(ctx);
try {
listener.migrationStart(ctx);
@ -245,50 +266,20 @@ final class MigrationImpl extends AbstractScope implements Migration {
for (Query query : queries())
log.debug("jOOQ Migrations", dsl().renderInlined(query));
JooqMigrationsChangelogRecord newRecord = dsl().newRecord(CHANGELOG);
newRecord
.setJooqVersion(Constants.VERSION)
.setMigratedAt(new Timestamp(System.currentTimeMillis()))
.setMigratedFrom(from().id())
.setMigratedTo(to().id())
.setMigrationTime(0L)
.setSql(queries().toString())
.setSqlCount(queries().queries().length)
.setStatus("PENDING")
.insert();
JooqMigrationsChangelogRecord record = createRecord(STARTING);
try {
// TODO: Can we access the individual Queries from Version, if applicable?
// TODO: Set the ctx.queriesFrom(), ctx.queriesTo(), and ctx.queries() values
listener.queriesStart(ctx);
// TODO: Make batching an option: queries().executeBatch();
for (Query query : queries().queries()) {
ctx.query(query);
listener.queryStart(ctx);
query.execute();
listener.queryEnd(ctx);
ctx.query(null);
}
listener.queriesEnd(ctx);
newRecord
.setMigrationTime(watch.split() / 1000000L)
.setStatus("SUCCESS")
.update();
log(watch, record, REVERTING);
revertUntracked(ctx, listener);
log(watch, record, MIGRATING);
execute(ctx, listener, queries());
log(watch, record, SUCCESS);
}
catch (DataAccessException e) {
// TODO: Make sure this is committed, given that we're re-throwing the exception.
// TODO: How can we recover from failure?
newRecord
.setMigrationTime(watch.split() / 1000000L)
.setStatus("FAILURE")
.update();
log(watch, record, FAILURE);
throw e;
}
@ -298,9 +289,49 @@ final class MigrationImpl extends AbstractScope implements Migration {
listener.migrationEnd(ctx);
}
}
private final JooqMigrationsChangelogRecord createRecord(Status status) {
JooqMigrationsChangelogRecord record = dsl().newRecord(CHANGELOG);
record
.setJooqVersion(Constants.VERSION)
.setMigratedAt(new Timestamp(System.currentTimeMillis()))
.setMigratedFrom(from().id())
.setMigratedTo(to().id())
.setMigrationTime(0L)
.setSql(queries().toString())
.setSqlCount(queries().queries().length)
.setStatus(status)
.insert();
return record;
}
private final void log(StopWatch watch, JooqMigrationsChangelogRecord record, Status status) {
record.setMigrationTime(watch.split() / 1000000L)
.setStatus(status)
.update();
}
});
}
private final void execute(DefaultMigrationContext ctx, MigrationListener listener, Queries q) {
// TODO: Can we access the individual Queries from Version, if applicable?
// TODO: Set the ctx.queriesFrom(), ctx.queriesTo(), and ctx.queries() values
listener.queriesStart(ctx);
// TODO: Make batching an option: queries().executeBatch();
for (Query query : q.queries()) {
ctx.query(query);
listener.queryStart(ctx);
query.execute();
listener.queryEnd(ctx);
ctx.query(null);
}
listener.queriesEnd(ctx);
}
/**
* Initialise the underlying {@link Configuration} with the jOOQ Migrations
* Changelog.
@ -329,6 +360,9 @@ final class MigrationImpl extends AbstractScope implements Migration {
private final JooqMigrationsChangelogRecord currentChangelogRecord() {
return existsChangelog()
? dsl().selectFrom(CHANGELOG)
// TODO: How to recover from failure?
.where(CHANGELOG.STATUS.eq(inline(SUCCESS)))
.orderBy(CHANGELOG.MIGRATED_AT.desc(), CHANGELOG.ID.desc())
.limit(1)
.fetchOne()
@ -337,7 +371,23 @@ final class MigrationImpl extends AbstractScope implements Migration {
private final Version currentVersion() {
JooqMigrationsChangelogRecord currentRecord = currentChangelogRecord();
return currentRecord == null ? to().root() : versions().get(currentRecord.getMigratedTo());
if (currentRecord == null) {
Version result = to().root();
if (result == null)
throw new DataMigrationValidationException("VersionProvider did not provide a root version for " + to().id());
return result;
}
else {
Version result = versions().get(currentRecord.getMigratedTo());
if (result == null)
throw new DataMigrationValidationException("VersionProvider did not provide a version for " + currentRecord.getMigratedTo());
return result;
}
}
private final <T> T run(final ContextTransactionalCallable<T> runnable) {
@ -364,6 +414,14 @@ final class MigrationImpl extends AbstractScope implements Migration {
return sb.toString();
}
enum Status {
STARTING,
REVERTING,
MIGRATING,
SUCCESS,
FAILURE
}
// -------------------------------------------------------------------------
// XXX: Generated code
// -------------------------------------------------------------------------
@ -438,7 +496,7 @@ final class MigrationImpl extends AbstractScope implements Migration {
/**
* The column <code>JOOQ_MIGRATIONS_CHANGELOG.JOOQ_VERSION</code>. The jOOQ version used to migrate to this database version.
*/
public final TableField<JooqMigrationsChangelogRecord, String> STATUS = createField(DSL.name("STATUS"), org.jooq.impl.SQLDataType.VARCHAR(10).nullable(false), this, "The database version installation status.");
public final TableField<JooqMigrationsChangelogRecord, Status> STATUS = createField(DSL.name("STATUS"), org.jooq.impl.SQLDataType.VARCHAR(10).nullable(false).asConvertedDataType(new EnumConverter(String.class, Status.class)), this, "The database version installation status.");
/**
* Create a <code>JOOQ_MIGRATIONS_CHANGELOG</code> table reference
@ -623,7 +681,7 @@ final class MigrationImpl extends AbstractScope implements Migration {
/**
* Setter for <code>JOOQ_MIGRATIONS_CHANGELOG.STATUS</code>. The database version installation status.
*/
public JooqMigrationsChangelogRecord setStatus(String value) {
public JooqMigrationsChangelogRecord setStatus(Status value) {
set(8, value);
return this;
}
@ -631,8 +689,8 @@ final class MigrationImpl extends AbstractScope implements Migration {
/**
* Getter for <code>JOOQ_MIGRATIONS_CHANGELOG.STATUS</code>. The database version installation status.
*/
public String getStatus() {
return (String) get(8);
public Status getStatus() {
return (Status) get(8);
}
// -------------------------------------------------------------------------

View File

@ -407,6 +407,10 @@ jOOQ queries, for which no specific fetchSize value was specified.]]></jxb:javad
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Whether migrations are allowed to be executed in inverse order.<p><strong>This is a potentially destructive feature, which should not be turned on in production</strong>. It is useful mostly to quickly switch between branches in a development environment. This feature is available only in commercial distributions.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="migrationRevertUntracked" type="boolean" minOccurs="0" maxOccurs="1" default="false">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Whether migrations revert any untracked changes in the schemas that are being migrated.<p><strong>This is a potentially destructive feature, which should not be turned on in production</strong>. It is useful mostly to quickly revert any elements created in a development environment. This feature is available only in commercial distributions.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="migrationAutoValidation" type="boolean" minOccurs="0" maxOccurs="1" default="true">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Whether a migration automatically runs a validation first.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>