diff --git a/jOOQ-migrations-maven/src/main/java/org/jooq/codegen/maven/ResolveMojo.java b/jOOQ-migrations-maven/src/main/java/org/jooq/codegen/maven/ResolveMojo.java new file mode 100644 index 0000000000..9644a91713 --- /dev/null +++ b/jOOQ-migrations-maven/src/main/java/org/jooq/codegen/maven/ResolveMojo.java @@ -0,0 +1,64 @@ +/* + * 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 + * + * https://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. + * + * Other licenses: + * ----------------------------------------------------------------------------- + * Commercial licenses for this work are available. These replace the above + * ASL 2.0 and offer limited warranties, support, maintenance, and commercial + * database integrations. + * + * For more information, please visit: https://www.jooq.org/legal/licensing + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.codegen.maven; + +import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES; +import static org.apache.maven.plugins.annotations.ResolutionScope.TEST; + +import org.jooq.Configuration; + +import org.apache.maven.plugins.annotations.Mojo; + +/** + * The jOOQ Migrations resolve mojo + * + * @author Lukas Eder + */ +@Mojo( + name = "resolve", + defaultPhase = GENERATE_SOURCES, + requiresDependencyResolution = TEST, + threadSafe = true +) +public class ResolveMojo extends AbstractMigrationsMojo { + + @Override + final void execute0(Configuration configuration) throws Exception { + configuration.dsl().migrations().history().resolve("Resolved"); + } +} diff --git a/jOOQ/src/main/java/org/jooq/History.java b/jOOQ/src/main/java/org/jooq/History.java index ecd2c92981..7deacb31ec 100644 --- a/jOOQ/src/main/java/org/jooq/History.java +++ b/jOOQ/src/main/java/org/jooq/History.java @@ -83,4 +83,14 @@ public interface History extends Iterable { @NotNull @Experimental Version current(); + + /** + * Resolve any previous failures in the {@link History}. + *

+ * This is EXPERIMENTAL functionality and subject to change in future jOOQ + * versions. + */ + @Experimental + void resolve(String message); + } diff --git a/jOOQ/src/main/java/org/jooq/impl/History.java b/jOOQ/src/main/java/org/jooq/impl/History.java index 993b2ed634..b5b6cd7356 100644 --- a/jOOQ/src/main/java/org/jooq/impl/History.java +++ b/jOOQ/src/main/java/org/jooq/impl/History.java @@ -17,6 +17,7 @@ import org.jooq.Table; import org.jooq.TableField; import org.jooq.TableOptions; import org.jooq.UniqueKey; +import org.jooq.impl.MigrationImpl.Resolution; import org.jooq.impl.MigrationImpl.Status; @@ -94,6 +95,24 @@ class History extends TableImpl { */ final TableField STATUS = createField(DSL.name("STATUS"), SQLDataType.VARCHAR(10).nullable(false), this, "The database version installation status.", new EnumConverter(String.class, Status.class)); + /** + * The column JOOQ_MIGRATION_HISTORY.STATUS_MESSAGE. Any info + * or error message explaining the status. + */ + final TableField STATUS_MESSAGE = createField(DSL.name("STATUS_MESSAGE"), SQLDataType.CLOB, this, "Any info or error message explaining the status."); + + /** + * The column JOOQ_MIGRATION_HISTORY.RESOLUTION. The error + * resolution, if any. + */ + final TableField RESOLUTION = createField(DSL.name("RESOLUTION"), SQLDataType.VARCHAR(10), this, "The error resolution, if any.", new EnumConverter(String.class, Resolution.class)); + + /** + * The column JOOQ_MIGRATION_HISTORY.RESOLUTION_MESSAGE. Any + * info or error message explaining the resolution. + */ + final TableField RESOLUTION_MESSAGE = createField(DSL.name("RESOLUTION_MESSAGE"), SQLDataType.CLOB, this, "Any info or error message explaining the resolution."); + private History(Name alias, Table aliased) { this(alias, aliased, (Field[]) null, null); } @@ -136,7 +155,8 @@ class History extends TableImpl { @Override public List> getChecks() { return Arrays.asList( - Internal.createCheck(this, DSL.name("JOOQ_MIGR_HIST_CHK1"), "\"STATUS\" IN('STARTING', 'REVERTING', 'MIGRATING', 'SUCCESS', 'FAILURE')", true) + Internal.createCheck(this, DSL.name("JOOQ_MIGR_HIST_CHK1"), "\"STATUS\" IN('STARTING', 'REVERTING', 'MIGRATING', 'SUCCESS', 'FAILURE')", true), + Internal.createCheck(this, DSL.name("JOOQ_MIGR_HIST_CHK2"), "\"RESOLUTION\" IN('OPEN', 'RESOLVED', 'IGNORED')", true) ); } diff --git a/jOOQ/src/main/java/org/jooq/impl/HistoryImpl.java b/jOOQ/src/main/java/org/jooq/impl/HistoryImpl.java index 4d93ab9a23..f43b0f21a6 100644 --- a/jOOQ/src/main/java/org/jooq/impl/HistoryImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/HistoryImpl.java @@ -42,8 +42,11 @@ import static java.util.Arrays.asList; import static java.util.Collections.unmodifiableList; import static org.jooq.impl.DSL.inline; import static org.jooq.impl.DSL.name; +import static org.jooq.impl.DSL.noCondition; import static org.jooq.impl.DSL.schema; import static org.jooq.impl.History.HISTORY; +import static org.jooq.impl.MigrationImpl.Resolution.OPEN; +import static org.jooq.impl.MigrationImpl.Resolution.RESOLVED; import static org.jooq.impl.MigrationImpl.Status.SUCCESS; import static org.jooq.impl.Tools.isEmpty; import static org.jooq.tools.StringUtils.isBlank; @@ -70,8 +73,10 @@ import org.jooq.conf.MigrationSchema; import org.jooq.conf.RenderMapping; import org.jooq.exception.DataAccessException; import org.jooq.exception.DataMigrationVerificationException; +import org.jooq.impl.MigrationImpl.Resolution; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.ApiStatus.Experimental; /** * @author Lukas Eder @@ -86,6 +91,8 @@ class HistoryImpl extends AbstractScope implements History { HistoryImpl(Configuration configuration) { super(configuration); + // [#9506] TODO: What's the best way to spawn a new JDBC connection from an existing one? + // The history interactions should run in an autonomous transaction of the system connection provider this.ctx = configuration.dsl(); this.historyCtx = initCtx(configuration, configuration.settings().getMigrationHistorySchema()).dsl(); this.commits = configuration.commitProvider().provide(); @@ -184,12 +191,14 @@ class HistoryImpl extends AbstractScope implements History { } @Nullable - final HistoryRecord currentHistoryRecord() { + final HistoryRecord currentHistoryRecord(boolean successOnly) { return existsHistory() ? historyCtx.selectFrom(HISTORY) // TODO: How to recover from failure? - .where(HISTORY.STATUS.eq(inline(SUCCESS))) + .where(successOnly + ? HISTORY.STATUS.eq(inline(SUCCESS)) + : HISTORY.STATUS.eq(inline(SUCCESS)).or(HISTORY.RESOLUTION.eq(OPEN))) .orderBy(HISTORY.MIGRATED_AT.desc(), HISTORY.ID.desc()) .limit(1) .fetchOne() @@ -254,6 +263,18 @@ class HistoryImpl extends AbstractScope implements History { } } + @Override + public final void resolve(String message) { + HistoryRecord h = currentHistoryRecord(false); + + if (h != null) + h.setResolution(RESOLVED) + .setResolutionMessage(message) + .update(); + else + throw new DataMigrationVerificationException("No current history record found to resolve"); + } + // ------------------------------------------------------------------------- // The Object API // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/java/org/jooq/impl/HistoryRecord.java b/jOOQ/src/main/java/org/jooq/impl/HistoryRecord.java index 038c673e3a..8b727bc1c6 100644 --- a/jOOQ/src/main/java/org/jooq/impl/HistoryRecord.java +++ b/jOOQ/src/main/java/org/jooq/impl/HistoryRecord.java @@ -7,6 +7,7 @@ package org.jooq.impl; import java.sql.Timestamp; import org.jooq.Record1; +import org.jooq.impl.MigrationImpl.Resolution; import org.jooq.impl.MigrationImpl.Status; @@ -169,6 +170,57 @@ class HistoryRecord extends UpdatableRecordImpl { return (Status) get(8); } + /** + * Setter for JOOQ_MIGRATION_HISTORY.STATUS_MESSAGE. Any info + * or error message explaining the status. + */ + HistoryRecord setStatusMessage(String value) { + set(9, value); + return this; + } + + /** + * Getter for JOOQ_MIGRATION_HISTORY.STATUS_MESSAGE. Any info + * or error message explaining the status. + */ + String getStatusMessage() { + return (String) get(9); + } + + /** + * Setter for JOOQ_MIGRATION_HISTORY.RESOLUTION. The error + * resolution, if any. + */ + HistoryRecord setResolution(Resolution value) { + set(10, value); + return this; + } + + /** + * Getter for JOOQ_MIGRATION_HISTORY.RESOLUTION. The error + * resolution, if any. + */ + Resolution getResolution() { + return (Resolution) get(10); + } + + /** + * Setter for JOOQ_MIGRATION_HISTORY.RESOLUTION_MESSAGE. Any + * info or error message explaining the resolution. + */ + HistoryRecord setResolutionMessage(String value) { + set(11, value); + return this; + } + + /** + * Getter for JOOQ_MIGRATION_HISTORY.RESOLUTION_MESSAGE. Any + * info or error message explaining the resolution. + */ + String getResolutionMessage() { + return (String) get(11); + } + // ------------------------------------------------------------------------- // Primary key information // ------------------------------------------------------------------------- @@ -192,7 +244,7 @@ class HistoryRecord extends UpdatableRecordImpl { /** * Create a detached, initialised HistoryRecord */ - HistoryRecord(Integer id, String migratedFrom, String migratedTo, Timestamp migratedAt, Long migrationTime, String jooqVersion, String sql, Integer sqlCount, Status status) { + HistoryRecord(Integer id, String migratedFrom, String migratedTo, Timestamp migratedAt, Long migrationTime, String jooqVersion, String sql, Integer sqlCount, Status status, String statusMessage, Resolution resolution, String resolutionMessage) { super(History.HISTORY); setId(id); @@ -204,6 +256,9 @@ class HistoryRecord extends UpdatableRecordImpl { setSql(sql); setSqlCount(sqlCount); setStatus(status); + setStatusMessage(statusMessage); + setResolution(resolution); + setResolutionMessage(resolutionMessage); resetChangedOnNotNull(); } } diff --git a/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java b/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java index 4c55889dd2..fb0fcaed71 100644 --- a/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/MigrationImpl.java @@ -44,12 +44,15 @@ import static org.jooq.impl.DSL.dropSchemaIfExists; import static org.jooq.impl.DSL.dropTableIfExists; import static org.jooq.impl.History.HISTORY; import static org.jooq.impl.HistoryImpl.initCtx; +import static org.jooq.impl.MigrationImpl.Resolution.OPEN; 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.io.PrintWriter; +import java.io.StringWriter; import java.sql.Timestamp; import java.util.HashSet; import java.util.Set; @@ -72,13 +75,14 @@ import org.jooq.exception.DataMigrationException; import org.jooq.exception.DataMigrationVerificationException; import org.jooq.tools.JooqLogger; import org.jooq.tools.StopWatch; +import org.jooq.tools.StringUtils; /** * @author Lukas Eder */ final class MigrationImpl extends AbstractScope implements Migration { - static final JooqLogger log = JooqLogger.getLogger(Migration.class); + static final JooqLogger log = JooqLogger.getLogger(Migration.class); final HistoryImpl history; final Commit to; Commit from; @@ -133,9 +137,19 @@ final class MigrationImpl extends AbstractScope implements Migration { } private final void verify0(DefaultMigrationContext ctx) { - HistoryRecord currentRecord = history.currentHistoryRecord(); + HistoryRecord currentRecord = history.currentHistoryRecord(false); if (currentRecord != null) { + switch (currentRecord.getStatus()) { + case FAILURE: + throw new DataMigrationVerificationException("Previous migration attempt from " + currentRecord.getMigratedFrom() + " to " + currentRecord.getMigratedTo() + " has failed. Please resolve before migrating."); + + case STARTING: + case REVERTING: + case MIGRATING: + throw new DataMigrationVerificationException("Ongoing migration from " + currentRecord.getMigratedFrom() + " to " + currentRecord.getMigratedTo() + ". Please wait until it has finished."); + } + Commit currentCommit = commits().get(currentRecord.getMigratedTo()); if (currentCommit == null) @@ -212,90 +226,109 @@ final class MigrationImpl extends AbstractScope implements Migration { // TODO: Transactions don't really make sense in most dialects. In some, they do // e.g. PostgreSQL supports transactional DDL. Check if we're getting this right. - run(new ContextTransactionalRunnable() { - @Override - public void run() { - DefaultMigrationContext ctx = migrationContext(); - MigrationListener listener = new MigrationListeners(configuration); + run(() -> { + DefaultMigrationContext ctx = migrationContext(); + MigrationListener listener = new MigrationListeners(configuration); - if (!FALSE.equals(dsl().settings().isMigrationAutoVerification())) - verify0(ctx); + if (!FALSE.equals(dsl().settings().isMigrationAutoVerification())) + verify0(ctx); + + try { + listener.migrationStart(ctx); + + if (from().equals(to())) { + log.info("jOOQ Migrations", "Version " + to().id() + " is already installed as the current version."); + return; + } + + // TODO: Implement preconditions + // TODO: Implement a listener with a variety of pro / oss features + // TODO: Implement additional out-of-the-box sanity checks + // TODO: Allow undo migrations only if enabled explicitly + // TODO: Add some migration settings, e.g. whether HISTORY.SQL should be filled + // TODO: Migrate the HISTORY table with the Migration API + // TODO: Create an Enum for HISTORY.STATUS + // TODO: Add HISTORY.USERNAME and HOSTNAME columns + // TODO: Add HISTORY.COMMENTS column + // TODO: Replace (MIGRATED_AT, MIGRATION_TIME) by (MIGRATION_START, MIGRATION_END) + + log.info("jOOQ Migrations", "Version " + from().id() + " is being migrated to " + to().id()); + + StopWatch watch = new StopWatch(); + + // TODO: Make logging configurable + if (log.isDebugEnabled()) + for (Query query : queries()) + log.debug("jOOQ Migrations", dsl().renderInlined(query)); + + HistoryRecord record = createRecord(STARTING); try { - listener.migrationStart(ctx); - - if (from().equals(to())) { - log.info("jOOQ Migrations", "Version " + to().id() + " is already installed as the current version."); - return; - } - - // TODO: Implement preconditions - // TODO: Implement a listener with a variety of pro / oss features - // TODO: Implement additional out-of-the-box sanity checks - // TODO: Allow undo migrations only if enabled explicitly - // TODO: Add some migration settings, e.g. whether HISTORY.SQL should be filled - // TODO: Migrate the HISTORY table with the Migration API - // TODO: Create an Enum for HISTORY.STATUS - // TODO: Add HISTORY.USERNAME and HOSTNAME columns - // TODO: Add HISTORY.COMMENTS column - // TODO: Replace (MIGRATED_AT, MIGRATION_TIME) by (MIGRATION_START, MIGRATION_END) - - log.info("jOOQ Migrations", "Version " + from().id() + " is migrated to " + to().id()); - - StopWatch watch = new StopWatch(); - - // TODO: Make logging configurable - if (log.isDebugEnabled()) - for (Query query : queries()) - log.debug("jOOQ Migrations", dsl().renderInlined(query)); - - HistoryRecord record = createRecord(STARTING); - - try { - log(watch, record, REVERTING); - revertUntracked(ctx, listener, record); - 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? - log(watch, record, FAILURE); - throw e; - } + log(watch, record, REVERTING); + revertUntracked(ctx, listener, record); + log(watch, record, MIGRATING); + execute(ctx, listener, queries()); + log(watch, record, SUCCESS); } - finally { - listener.migrationEnd(ctx); + catch (Exception e) { + StringWriter s = new StringWriter(); + e.printStackTrace(new PrintWriter(s)); + + log.error("jOOQ Migrations", "Version " + from().id() + " migration to " + to().id() + " failed: " + e.getMessage()); + log(watch, record, FAILURE, OPEN, s.toString()); + throw new DataMigrationRedoLogException(record, e); } } - - private final HistoryRecord createRecord(Status status) { - HistoryRecord record = history.historyCtx.newRecord(HISTORY); - - record - .setJooqVersion(Constants.VERSION) - .setMigratedAt(new Timestamp(dsl().configuration().clock().instant().toEpochMilli())) - .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, HistoryRecord record, Status status) { - record.setMigrationTime(watch.split() / 1000000L) - .setStatus(status) - .update(); + finally { + listener.migrationEnd(ctx); } }); } + /** + * An internal wrapper class for exceptions that allows for re-creating the + * {@link HistoryRecord} in case it was rolled back. + */ + static final class DataMigrationRedoLogException extends DataMigrationException { + + final HistoryRecord record; + + public DataMigrationRedoLogException(HistoryRecord record, Exception cause) { + super("Redo log", cause); + + this.record = record; + } + } + + private final HistoryRecord createRecord(Status status) { + HistoryRecord record = history.historyCtx.newRecord(HISTORY); + + record + .setJooqVersion(Constants.VERSION) + .setMigratedAt(new Timestamp(dsl().configuration().clock().instant().toEpochMilli())) + .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, HistoryRecord record, Status status) { + log(watch, record, status, null, null); + } + + private final void log(StopWatch watch, HistoryRecord record, Status status, Resolution resolution, String message) { + record.setMigrationTime(watch.split() / 1000000L) + .setStatus(status) + .setStatusMessage(message) + .setResolution(resolution) + .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 @@ -327,7 +360,7 @@ final class MigrationImpl extends AbstractScope implements Migration { } final Commit currentCommit() { - HistoryRecord currentRecord = history.currentHistoryRecord(); + HistoryRecord currentRecord = history.currentHistoryRecord(true); if (currentRecord == null) { Commit result = TRUE.equals(settings().isMigrationAutoBaseline()) ? to() : to().root(); @@ -352,6 +385,21 @@ final class MigrationImpl extends AbstractScope implements Migration { init(); dsl().transaction(runnable); } + catch (DataMigrationRedoLogException e) { + + // [#9506] Make sure history record is re-created in case it was rolled back. + HistoryRecord record = history.currentHistoryRecord(false); + + if (record == null || !StringUtils.equals(e.record.getId(), record.getId())) { + e.record.changed(true); + e.record.insert(); + } + + if (e.getCause() instanceof DataMigrationException r) + throw r; + else + throw new DataMigrationException("Exception during migration", e); + } catch (DataMigrationException e) { throw e; } @@ -368,6 +416,12 @@ final class MigrationImpl extends AbstractScope implements Migration { FAILURE } + enum Resolution { + OPEN, + RESOLVED, + IGNORED + } + // ------------------------------------------------------------------------- // The Object API // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/resources/org/jooq/migrations/v1-init.sql b/jOOQ/src/main/resources/org/jooq/migrations/v1-init.sql index 85a6ed9546..87c2fab88a 100644 --- a/jOOQ/src/main/resources/org/jooq/migrations/v1-init.sql +++ b/jOOQ/src/main/resources/org/jooq/migrations/v1-init.sql @@ -1,26 +1,33 @@ CREATE TABLE jooq_migration_history ( - id BIGINT NOT NULL IDENTITY, - migrated_from VARCHAR(255) NOT NULL, - migrated_to VARCHAR(255) NOT NULL, - migrated_at TIMESTAMP NOT NULL, - migration_time BIGINT NULL, - jooq_version VARCHAR(50) NOT NULL, - sql CLOB NULL, - sql_count INT NOT NULL, - status VARCHAR(10) NOT NULL, + id BIGINT NOT NULL IDENTITY, + migrated_from VARCHAR(255) NOT NULL, + migrated_to VARCHAR(255) NOT NULL, + migrated_at TIMESTAMP NOT NULL, + migration_time BIGINT NULL, + jooq_version VARCHAR(50) NOT NULL, + sql CLOB NULL, + sql_count INT NOT NULL, + status VARCHAR(10) NOT NULL, + status_message CLOB NULL, + resolution VARCHAR(10) NULL, + resolution_message CLOB NULL, CONSTRAINT jooq_migr_hist_pk PRIMARY KEY (id), - CONSTRAINT jooq_migr_hist_chk1 CHECK (status IN ('STARTING', 'REVERTING', 'MIGRATING', 'SUCCESS', 'FAILURE')) + CONSTRAINT jooq_migr_hist_chk1 CHECK (status IN ('STARTING', 'REVERTING', 'MIGRATING', 'SUCCESS', 'FAILURE')), + CONSTRAINT jooq_migr_hist_chk2 CHECK (resolution IN ('OPEN', 'RESOLVED', 'IGNORED')) ); CREATE INDEX jooq_migr_hist_i1 ON jooq_migration_history (migrated_at); -COMMENT ON TABLE jooq_migration_history IS 'The migration history of jOOQ Migrations.'; -COMMENT ON COLUMN jooq_migration_history.id IS 'The database version ID.'; -COMMENT ON COLUMN jooq_migration_history.migrated_from IS 'The previous database version ID.'; -COMMENT ON COLUMN jooq_migration_history.migrated_at IS 'The date/time when the database version was migrated to.'; -COMMENT ON COLUMN jooq_migration_history.migration_time IS 'The time in milliseconds it took to migrate to this database version.'; -COMMENT ON COLUMN jooq_migration_history.jooq_version IS 'The jOOQ version used to migrate to this database version.'; -COMMENT ON COLUMN jooq_migration_history.sql_count IS 'The number of SQL statements that were run to install this database version.'; -COMMENT ON COLUMN jooq_migration_history.sql IS 'The SQL statements that were run to install this database version.'; -COMMENT ON COLUMN jooq_migration_history.status IS 'The database version installation status.'; +COMMENT ON TABLE jooq_migration_history IS 'The migration history of jOOQ Migrations.'; +COMMENT ON COLUMN jooq_migration_history.id IS 'The database version ID.'; +COMMENT ON COLUMN jooq_migration_history.migrated_from IS 'The previous database version ID.'; +COMMENT ON COLUMN jooq_migration_history.migrated_at IS 'The date/time when the database version was migrated to.'; +COMMENT ON COLUMN jooq_migration_history.migration_time IS 'The time in milliseconds it took to migrate to this database version.'; +COMMENT ON COLUMN jooq_migration_history.jooq_version IS 'The jOOQ version used to migrate to this database version.'; +COMMENT ON COLUMN jooq_migration_history.sql_count IS 'The number of SQL statements that were run to install this database version.'; +COMMENT ON COLUMN jooq_migration_history.sql IS 'The SQL statements that were run to install this database version.'; +COMMENT ON COLUMN jooq_migration_history.status IS 'The database version installation status.'; +COMMENT ON COLUMN jooq_migration_history.status_message IS 'Any info or error message explaining the status.'; +COMMENT ON COLUMN jooq_migration_history.resolution IS 'The error resolution, if any.'; +COMMENT ON COLUMN jooq_migration_history.resolution_message IS 'Any info or error message explaining the resolution.'; \ No newline at end of file