diff --git a/jOOQ/src/main/java/org/jooq/conf/Settings.java b/jOOQ/src/main/java/org/jooq/conf/Settings.java index 6868345e2c..acc923a5de 100644 --- a/jOOQ/src/main/java/org/jooq/conf/Settings.java +++ b/jOOQ/src/main/java/org/jooq/conf/Settings.java @@ -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.

This is a potentially destructive feature, which should not be turned on in production. 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())); diff --git a/jOOQ/src/main/java/org/jooq/exception/DataMigrationValidationException.java b/jOOQ/src/main/java/org/jooq/exception/DataMigrationValidationException.java index 251b124eac..fac77ab097 100644 --- a/jOOQ/src/main/java/org/jooq/exception/DataMigrationValidationException.java +++ b/jOOQ/src/main/java/org/jooq/exception/DataMigrationValidationException.java @@ -44,7 +44,7 @@ import org.jooq.Migration; * * @author Lukas Eder */ -public class DataMigrationValidationException extends DataAccessException { +public class DataMigrationValidationException extends DataMigrationException { /** * Generated UID diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultMigrationContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultMigrationContext.java index aa45600bdc..09e39fc80f 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultMigrationContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultMigrationContext.java @@ -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 diff --git a/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java b/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java index 2655586c03..e2f9060c96 100644 --- a/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java @@ -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 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 lookup(List 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 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() { @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 run(final ContextTransactionalCallable 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 JOOQ_MIGRATIONS_CHANGELOG.JOOQ_VERSION. The jOOQ version used to migrate to this database version. */ - public final TableField STATUS = createField(DSL.name("STATUS"), org.jooq.impl.SQLDataType.VARCHAR(10).nullable(false), this, "The database version installation status."); + public final TableField 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 JOOQ_MIGRATIONS_CHANGELOG table reference @@ -623,7 +681,7 @@ final class MigrationImpl extends AbstractScope implements Migration { /** * Setter for JOOQ_MIGRATIONS_CHANGELOG.STATUS. 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 JOOQ_MIGRATIONS_CHANGELOG.STATUS. The database version installation status. */ - public String getStatus() { - return (String) get(8); + public Status getStatus() { + return (Status) get(8); } // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/resources/xsd/jooq-runtime-3.13.0.xsd b/jOOQ/src/main/resources/xsd/jooq-runtime-3.13.0.xsd index 997563b0aa..eedd0fdc36 100644 --- a/jOOQ/src/main/resources/xsd/jooq-runtime-3.13.0.xsd +++ b/jOOQ/src/main/resources/xsd/jooq-runtime-3.13.0.xsd @@ -407,6 +407,10 @@ jOOQ queries, for which no specific fetchSize value was specified.]]>This is a potentially destructive feature, which should not be turned on in production. It is useful mostly to quickly switch between branches in a development environment. This feature is available only in commercial distributions.]]> + + This is a potentially destructive feature, which should not be turned on in production. It is useful mostly to quickly revert any elements created in a development environment. This feature is available only in commercial distributions.]]> + +