[jOOQ/jOOQ#9506] More work on Migrations API:

- Infer JDBC driver from JDBC URL if not given explicitly
- Better FileData debugging info
- Add HistoryVersion type to add a migratedAt() Instant to a Version
- Add a CleanMojo
- Improve mojo Javadocs
- Rename init node to root
- Make root a reserved ID
This commit is contained in:
Lukas Eder 2024-11-19 14:08:29 +01:00
parent 159b664e9a
commit 2343e3c53d
18 changed files with 385 additions and 40 deletions

View File

@ -79,11 +79,11 @@ public abstract class AbstractMigrateMojo extends AbstractMigrationsMojo {
.migrations()
.migrateTo(commits.latest());
if (getLog().isInfoEnabled()) {
getLog().info("Migration from version: " + migration.from());
getLog().info("Migration to version : " + migration.to());
getLog().info("Migration queries : " + migration.queries().queries().length);
}
if (getLog().isInfoEnabled())
getLog().info(
"Migration loaded from version " + migration.from() + " to version " + migration.to()
+ " (number of queries: " + migration.queries().queries().length + ")"
);
execute1(migration);
}

View File

@ -49,7 +49,8 @@ import org.jooq.CloseableDSLContext;
import org.jooq.Configuration;
import org.jooq.conf.MigrationSchema;
import org.jooq.impl.DSL;
import org.jooq.tools.StringUtils;
import org.jooq.tools.ClassUtils;
import org.jooq.tools.jdbc.JDBCUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
@ -160,11 +161,14 @@ abstract class AbstractMigrationsMojo extends AbstractMojo {
// [#2886] Add the surrounding project's dependencies to the current classloader
Thread.currentThread().setContextClassLoader(pluginClassLoader);
project.getRuntimeClasspathElements().forEach(System.out::println);
if (jdbc == null || jdbc.url == null)
throw new MojoExecutionException("JDBC URL is required");
if (jdbc.driver != null)
Class.forName(jdbc.driver);
String driver = driverClass(jdbc);
if (driver != null)
ClassUtils.loadClass(driver).getConstructor().newInstance();
try (CloseableDSLContext ctx = DSL.using(jdbc.url, defaultIfNull(jdbc.user, jdbc.username), jdbc.password)) {
@ -227,6 +231,17 @@ abstract class AbstractMigrationsMojo extends AbstractMojo {
}
}
private String driverClass(Jdbc j) {
String result = j.driver;
if (result == null) {
result = JDBCUtils.driver(j.url);
getLog().info("Inferring driver " + result + " from URL " + j.url);
}
return result;
}
abstract void execute0(Configuration configuration) throws Exception;
private URLClassLoader getClassLoader() throws MojoExecutionException {

View File

@ -0,0 +1,72 @@
/*
* 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
* Apache-2.0 license and offer limited warranties, support, maintenance, and
* commercial database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.migrations.maven;
import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES;
import static org.apache.maven.plugins.annotations.ResolutionScope.TEST;
import org.jooq.Commit;
import org.jooq.Files;
import org.jooq.Migration;
import org.jooq.Queries;
import org.jooq.Query;
import org.jooq.Version;
import org.jooq.tools.StringUtils;
import org.apache.maven.plugins.annotations.Mojo;
/**
* Clean the configured schemas, dropping all objects.
*
* @author Lukas Eder
*/
@Mojo(
name = "clean",
defaultPhase = GENERATE_SOURCES,
requiresDependencyResolution = TEST,
threadSafe = true
)
public class CleanMojo extends AbstractMigrateMojo {
@Override
final void execute1(Migration migration) throws Exception {
Commit root = migration.to().root();
root.settings().setMigrationAllowsUndo(true);
migration.dsl().migrations().migrateTo(root).execute();
}
}

View File

@ -0,0 +1,92 @@
/*
* 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
* Apache-2.0 license and offer limited warranties, support, maintenance, and
* commercial database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.migrations.maven;
import static org.apache.maven.plugins.annotations.LifecyclePhase.GENERATE_SOURCES;
import static org.apache.maven.plugins.annotations.ResolutionScope.TEST;
import static org.jooq.tools.StringUtils.isEmpty;
import java.time.Instant;
import org.jooq.HistoryVersion;
import org.jooq.Migration;
import org.jooq.Version;
import org.jooq.tools.StringUtils;
import org.apache.maven.plugins.annotations.Mojo;
/**
* Show the history of currently installed versions on the connected database.
*
* @author Lukas Eder
*/
@Mojo(
name = "history",
defaultPhase = GENERATE_SOURCES,
requiresDependencyResolution = TEST,
threadSafe = true
)
public class HistoryMojo extends AbstractMigrateMojo {
@Override
final void execute1(Migration migration) throws Exception {
if (getLog().isInfoEnabled()) {
for (HistoryVersion version : migration.dsl().migrations().history()) {
getLog().info(string(version.migratedAt()) + " - Version: " + string(version.version()));
if (version.version().parents().size() > 1) {
getLog().info(" Merged parents: ");
for (Version p : version.version().parents())
getLog().info(" - " + string(p));
}
}
}
}
private final String string(Instant instant) {
if (instant == null)
return "0000-00-00T00:00:00.000Z";
else
return StringUtils.rightPad(instant.toString(), 24);
}
private final String string(Version version) {
return version.id() + (!isEmpty(version.message()) ? " (" + version.message() + ")" : "");
}
}

View File

@ -44,10 +44,11 @@ import org.jooq.Migration;
import org.jooq.Query;
import org.jooq.tools.StringUtils;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.Mojo;
/**
* The jOOQ Migrations migrate mojo
* Log the queries of the outstanding migration.
*
* @author Lukas Eder
*/
@ -63,10 +64,14 @@ public class LogMojo extends AbstractMigrateMojo {
final void execute1(Migration migration) throws Exception {
if (getLog().isInfoEnabled()) {
Query[] queries = migration.queries().queries();
int pad = ("" + queries.length).length();
for (int i = 0; i < queries.length; i++)
getLog().info(" Query " + StringUtils.leftPad("" + (i + 1), pad) + ": " + queries[i]);
log(getLog(), queries);
}
}
static final void log(Log log, Query[] queries) {
int pad = ("" + queries.length).length();
for (int i = 0; i < queries.length; i++)
log.info(" Query " + StringUtils.leftPad("" + (i + 1), pad) + ": " + queries[i]);
}
}

View File

@ -45,7 +45,7 @@ import org.jooq.Migration;
import org.apache.maven.plugins.annotations.Mojo;
/**
* The jOOQ Migrations migrate mojo
* Run a migration.
*
* @author Lukas Eder
*/

View File

@ -45,7 +45,7 @@ import org.jooq.Configuration;
import org.apache.maven.plugins.annotations.Mojo;
/**
* The jOOQ Migrations resolve mojo
* Mark an outstanding migration as resolved.
*
* @author Lukas Eder
*/

View File

@ -45,7 +45,7 @@ import org.jooq.Migration;
import org.apache.maven.plugins.annotations.Mojo;
/**
* The jOOQ Migrations verify mojo
* Verify an outstanding migration.
*
* @author Lukas Eder
*/

View File

@ -39,8 +39,8 @@ package org.jooq;
import java.util.Collection;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.ApiStatus.Experimental;
import org.jetbrains.annotations.NotNull;
/**
* A change set describing the exact migration path between the

View File

@ -52,7 +52,7 @@ import org.jetbrains.annotations.NotNull;
* @author Lukas Eder
*/
@Experimental
public interface History extends Iterable<Version>, Scope {
public interface History extends Iterable<HistoryVersion>, Scope {
/**
* The root {@link Version}.
@ -69,7 +69,7 @@ public interface History extends Iterable<Version>, Scope {
*/
@NotNull
@Experimental
Version root() throws DataMigrationVerificationException;
HistoryVersion root() throws DataMigrationVerificationException;
/**
* The currently installed {@link Version}.
@ -82,7 +82,7 @@ public interface History extends Iterable<Version>, Scope {
*/
@NotNull
@Experimental
Version current();
HistoryVersion current();
/**
* Resolve any previous failures in the {@link History}.

View File

@ -0,0 +1,70 @@
/*
* 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
* Apache-2.0 license and offer limited warranties, support, maintenance, and
* commercial database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq;
import java.time.Instant;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A {@link Version} in the context of a {@link History}.
*/
public interface HistoryVersion {
/**
* The version.
*/
@NotNull
Version version();
/**
* The history creating this {@link HistoryVersion}.
*/
@NotNull
History history();
/**
* The time when this {@link HistoryVersion} was migrated to in the context
* of the {@link #history()}.
* <p>
* This is <code>null</code> for the {@link History#root()} version.
*/
@Nullable
Instant migratedAt();
}

View File

@ -39,21 +39,26 @@ package org.jooq;
import java.util.List;
import org.jetbrains.annotations.ApiStatus.Experimental;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.ApiStatus.Experimental;
/**
* An abstraction over directed, acyclic graph models.
* <p>
* Examples of such models are {@link Version} / {@link Versions} or
* {@link Commit} / {@link Commits}.
* Examples of such models are {@link Version} or {@link Commit} /
* {@link Commits}.
*
* @author Lukas Eder
*/
@Experimental
public interface Node<N extends Node<N>> extends Scope {
/**
* The name of the {@link #root()} node.
*/
String ROOT = "root";
/**
* The ID of the node, unique within the graph.
*/

View File

@ -46,6 +46,7 @@ import java.util.Map.Entry;
import org.jooq.Configuration;
import org.jooq.Node;
import org.jooq.exception.DataDefinitionException;
import org.jooq.exception.DataMigrationVerificationException;
/**
* @author Lukas Eder
@ -54,7 +55,7 @@ abstract class AbstractNode<N extends Node<N>> extends AbstractLazyScope impleme
final N root;
final String id;
final String message;
String message;
@SuppressWarnings("unchecked")
AbstractNode(Configuration configuration, String id, String message, N root) {

View File

@ -64,10 +64,12 @@ import org.jooq.DSLContext;
import org.jooq.File;
import org.jooq.Files;
import org.jooq.Meta;
import org.jooq.Node;
import org.jooq.Source;
import org.jooq.Tag;
import org.jooq.Version;
import org.jooq.exception.DataMigrationException;
import org.jooq.exception.DataMigrationVerificationException;
import org.jooq.tools.StringUtils;
/**
@ -84,6 +86,9 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
CommitImpl(Configuration configuration, String id, String message, Commit root, List<Commit> parents, Collection<? extends File> delta) {
super(configuration, id, message, root);
if (Node.ROOT.equals(id) && root != null)
throw new DataMigrationVerificationException("Cannot use reserved ID \"root\"");
this.ctx = configuration.dsl();
this.parents = parents;
this.tags = new ArrayList<>();
@ -363,7 +368,7 @@ final class CommitImpl extends AbstractNode<Commit> implements Commit {
}
Map<String, File> versionFiles = new HashMap<>();
Version from = version(ctx.migrations().version("init"), id(), versionFiles, history.values());
Version from = version(ctx.migrations().version(ROOT), id(), versionFiles, history.values());
Version to = version(from, resultCommit.id(), versionFiles, result.values());
return new FilesImpl(from, to, result.values());
}

View File

@ -211,6 +211,24 @@ final class CommitsImpl implements Commits {
this.tags.add(new TagType().withId(tagArray[0]).withMessage(tagArray.length > 1 ? tagArray[1] : null));
}
}
@Override
public String toString() {
List<String> strings = new ArrayList<>();
if (id != null)
strings.add("id: " + id);
if (version != null)
strings.add("version: " + version);
if (message != null)
strings.add("message: " + message);
if (!tags.isEmpty())
strings.add("tags: " + tags);
if (!parentIds.isEmpty())
strings.add("parents: " + parentIds);
return "File: " + file + " " + strings;
}
}
@Override
@ -246,7 +264,7 @@ final class CommitsImpl implements Commits {
List<FileData> list = Stream.of(sql).map(FileData::new).collect(toList());
if (log.isDebugEnabled())
list.forEach(f -> log.debug("Reading file", f.basename));
list.forEach(f -> log.debug("Reading file", f));
/*
* An example:
@ -320,6 +338,11 @@ final class CommitsImpl implements Commits {
public final Commits load(MigrationsType migrations) {
Map<String, CommitType> map = new HashMap<>();
for (CommitType commit : migrations.getCommits())
if (Commit.ROOT.equals(commit.getId()))
throw new DataMigrationVerificationException("Cannot define reserved commit name \"root\"");
map.put(Commit.ROOT, new CommitType().withId(root.id()).withMessage(root.message()));
for (CommitType commit : migrations.getCommits())
map.put(commit.getId(), commit);

View File

@ -50,12 +50,14 @@ import static org.jooq.impl.HistoryStatus.SUCCESS;
import static org.jooq.impl.Tools.isEmpty;
import static org.jooq.tools.StringUtils.isBlank;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.jooq.Commit;
@ -63,6 +65,7 @@ import org.jooq.Commits;
import org.jooq.Configuration;
import org.jooq.DSLContext;
import org.jooq.History;
import org.jooq.HistoryVersion;
import org.jooq.Schema;
import org.jooq.Version;
import org.jooq.conf.InterpreterSearchSchema;
@ -72,6 +75,7 @@ import org.jooq.conf.MigrationSchema;
import org.jooq.conf.RenderMapping;
import org.jooq.exception.DataAccessException;
import org.jooq.exception.DataMigrationVerificationException;
import org.jooq.tools.JooqLogger;
import org.jetbrains.annotations.Nullable;
@ -80,10 +84,12 @@ import org.jetbrains.annotations.Nullable;
*/
class HistoryImpl extends AbstractScope implements History {
final DSLContext ctx;
final DSLContext historyCtx;
final Commits commits;
final List<Version> versions;
private static final JooqLogger log = JooqLogger.getLogger(HistoryImpl.class);
final DSLContext ctx;
final DSLContext historyCtx;
final Commits commits;
final List<HistoryVersion> versions;
HistoryImpl(Configuration configuration) {
super(configuration);
@ -97,12 +103,12 @@ class HistoryImpl extends AbstractScope implements History {
}
@Override
public final Iterator<Version> iterator() {
public final Iterator<HistoryVersion> iterator() {
return unmodifiableList(versions).iterator();
}
@Override
public final Version root() {
public final HistoryVersion root() {
if (!isEmpty(versions))
return versions.get(0);
else
@ -110,7 +116,7 @@ class HistoryImpl extends AbstractScope implements History {
}
@Override
public final Version current() {
public final HistoryVersion current() {
if (!isEmpty(versions))
return versions.get(versions.size() - 1);
else
@ -218,11 +224,15 @@ class HistoryImpl extends AbstractScope implements History {
return false;
}
private final List<Version> initVersions() {
List<Version> result = new ArrayList<>();
private final List<HistoryVersion> initVersions() {
List<HistoryVersion> result = new ArrayList<>();
if (existsHistory()) {
result.add(commits.root().version());
result.add(new HistoryVersionImpl(
this,
commits.root().version(),
null
));
for (HistoryRecord r : historyCtx
.selectFrom(HISTORY)
@ -232,9 +242,20 @@ class HistoryImpl extends AbstractScope implements History {
Commit commit = commits.get(r.getMigratedTo());
if (commit != null)
result.add(commit.version());
result.add(new HistoryVersionImpl(
this,
commit.version(),
r.getMigratedAt().toInstant()
));
else
throw new DataMigrationVerificationException("CommitProvider didn't provide version for ID: " + r.getMigratedTo());
throw new DataMigrationVerificationException(
"""
CommitProvider didn't provide version for ID: {id}
This may happen if a successful migration has happened in a database, but the source
for this migration is not available.
""".replace("{id}", r.getMigratedTo())
);
}
}
@ -256,6 +277,7 @@ class HistoryImpl extends AbstractScope implements History {
&& historyCtx.settings().getMigrationDefaultSchema() != null)
historyCtx.createSchemaIfNotExists("").execute();
log.info("Initialising history table: " + historyCtx.map(HISTORY));
historyCtx.meta(HISTORY).ddl().executeBatch();
}
}
@ -272,6 +294,31 @@ class HistoryImpl extends AbstractScope implements History {
throw new DataMigrationVerificationException("No current history record found to resolve");
}
static final record HistoryVersionImpl(History history, Version version, Instant migratedAt) implements HistoryVersion {
@Override
public int hashCode() {
return Objects.hash(version);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
HistoryVersionImpl other = (HistoryVersionImpl) obj;
return Objects.equals(version, other.version);
}
@Override
public String toString() {
return "HistoryVersion [version=" + version + ", migratedAt=" + migratedAt + "]";
}
}
// -------------------------------------------------------------------------
// The Object API
// -------------------------------------------------------------------------

View File

@ -169,7 +169,16 @@ final class MigrationImpl extends AbstractScope implements Migration {
for (Schema schema : history.lookup(commit.meta().getSchemas()))
if (!ctx.migratedSchemas().contains(schema))
throw new DataMigrationVerificationException("Schema is referenced from commit, but not configured for migration: " + schema);
throw new DataMigrationVerificationException(
"""
Schema is referenced from commit, but not configured for migration: {schema}
All schemas that are referenced from commits in a migration must be configured for
inclusion in the migration
TODO doclink
""".replace("{schema}", schema.toString())
);
}
private final Queries revertUntrackedQueries(Set<Schema> includedSchemas) {

View File

@ -38,6 +38,7 @@
package org.jooq.impl;
import static java.util.Collections.emptyList;
import static org.jooq.Commit.ROOT;
import org.jooq.Commit;
import org.jooq.Commits;
@ -75,7 +76,7 @@ final class MigrationsImpl extends AbstractScope implements Migrations {
@Override
public final Commits commits() {
return new CommitsImpl(configuration(), new CommitImpl(configuration(), "init", "init", null, emptyList(), emptyList()));
return new CommitsImpl(configuration(), new CommitImpl(configuration(), ROOT, ROOT, null, emptyList(), emptyList()));
}
@Override