Skip to content

Commit e75aab2

Browse files
authored
Add the jOOQ flyway plugin (#11)
2 parents a3b5c35 + 1472a94 commit e75aab2

File tree

12 files changed

+562
-5
lines changed

12 files changed

+562
-5
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
strategy:
1111
fail-fast: false
1212
matrix:
13-
jre: [17]
13+
jre: [21]
1414
os: [ubuntu-latest, windows-latest]
1515
runs-on: ${{ matrix.os }}
1616
steps:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
### Added
55
- `webtools.Env.isHerokuBuild()` and `isGitHubAction()`
66
- `com.diffplug.webtools.jte` plugin ([#10](https://github.com/diffplug/webtools/pull/10))
7+
- `com.diffplug.webtools.flywayjooq` plugin ([#11](https://github.com/diffplug/webtools/pull/11))
78

89
## [1.2.6] - 2025-08-22
910
### Fixed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- [node](#node) - hassle-free `npm install` and `npm run blah`
44
- [static server](#static-server) - a simple static file server
55
- [jte](#jte) - creates idiomatic Kotlin model classes for `jte` templates (strict nullability & idiomatic collections and generics)
6+
- [flywayjooq](#flywayjooq) - coordinates docker, flyway, and jOOQ for fast testing
67

78
## Node
89

@@ -66,3 +67,24 @@ class header(
6667
```
6768

6869
We also translate Java collections and generics to their Kotlin equivalents. See `JteRenderer.convertJavaToKotlin` for details.
70+
71+
### flywayjooq
72+
73+
Compile tasks just need to depend on the `jooq` task. It will keep a live database running to test against.
74+
75+
```gradle
76+
flywayJooq {
77+
// starts this docker container which needs to have postgres
78+
setup.dockerComposeFile = file('src/test/resources/docker-compose.yml')
79+
// writes out connection data to this file
80+
setup.dockerConnectionParams = file('build/pgConnection.properties')
81+
// migrates a template database to this
82+
setup.flywayMigrations = file('src/main/resources/db/migration')
83+
// dumps the final schema out to this
84+
setup.flywaySchemaDump = file('src/test/resources/schema.sql')
85+
// sets up jOOQ
86+
configuration {
87+
// jOOQ setup same as the official jOOQ plugin
88+
}
89+
}
90+
```

build.gradle

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,22 @@ dependencies {
4747
jteCompileOnly gradleApi()
4848
jteCompileOnly "gg.jte:jte-runtime:${VER_JTE}"
4949
jteCompileOnly "gg.jte:jte:${VER_JTE}"
50+
// flyway and jooq
51+
String VER_FLYWAY='11.11.1'
52+
String VER_JOOQ='3.20.6'
53+
String VER_PALANTIR_DOCKER_COMPOSE='2.3.0'
54+
String VER_POSTGRESQL_DRIVER='42.7.7'
55+
// https://github.com/palantir/docker-compose-rule
56+
api "com.palantir.docker.compose:docker-compose-rule-core:${VER_PALANTIR_DOCKER_COMPOSE}"
57+
api "com.palantir.docker.compose:docker-compose-rule-junit4:${VER_PALANTIR_DOCKER_COMPOSE}"
58+
// https://jdbc.postgresql.org/documentation/changelog.html
59+
implementation "org.postgresql:postgresql:${VER_POSTGRESQL_DRIVER}"
60+
// jooq codegen
61+
api "org.jooq:jooq-codegen:${VER_JOOQ}"
62+
api "org.jooq.jooq-codegen-gradle:org.jooq.jooq-codegen-gradle.gradle.plugin:${VER_JOOQ}"
63+
// db migration
64+
api "org.flywaydb:flyway-core:${VER_FLYWAY}"
65+
api "org.flywaydb:flyway-database-postgresql:${VER_FLYWAY}"
66+
// java8 utilities
67+
implementation 'com.diffplug.durian:durian-core:1.2.0'
5068
}

gradle.properties

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ org=diffplug
55
license=apache
66
git_url=github.com/diffplug/webtools
77
plugin_tags=node
8-
plugin_list=node jte
8+
plugin_list=node jte flywayjooq
99

10-
ver_java=17
10+
ver_java=21
1111

1212
javadoc_links=
1313

@@ -20,3 +20,8 @@ plugin_jte_id=com.diffplug.webtools.jte
2020
plugin_jte_impl=com.diffplug.webtools.jte.JtePlugin
2121
plugin_jte_name=DiffPlug JTE
2222
plugin_jte_desc=Runs the JTE plugin and adds typesafe model classes
23+
24+
plugin_flywayjooq_id=com.diffplug.webtools.flywayjooq
25+
plugin_flywayjooq_impl=com.diffplug.webtools.flywayjooq.FlywayJooqPlugin
26+
plugin_flywayjooq_name=DiffPlug Flyway jOOQ
27+
plugin_flywayjooq_desc=Coordinates Docker Compose, Flyway, and jOOQ for fast testing and easy deployment.

src/main/java/com/diffplug/webtools/node/SetupCleanup.java renamed to src/main/java/com/diffplug/webtools/SetupCleanup.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.diffplug.webtools.node;
16+
package com.diffplug.webtools;
1717

1818
import java.io.ByteArrayInputStream;
1919
import java.io.ByteArrayOutputStream;
@@ -24,7 +24,7 @@
2424
import java.nio.file.Files;
2525
import java.util.Arrays;
2626

27-
abstract class SetupCleanup<K> {
27+
public abstract class SetupCleanup<K> {
2828
public void start(File keyFile, K key) throws Exception {
2929
synchronized (key.getClass()) {
3030
byte[] required = toBytes(key);
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright (C) 2025 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.webtools.flywayjooq;
17+
18+
import java.net.URL;
19+
import java.net.URLClassLoader;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
22+
import javax.inject.Inject;
23+
import org.gradle.api.DefaultTask;
24+
import org.gradle.api.Plugin;
25+
import org.gradle.api.Project;
26+
import org.gradle.api.Task;
27+
import org.gradle.api.file.DirectoryProperty;
28+
import org.gradle.api.file.ProjectLayout;
29+
import org.gradle.api.model.ObjectFactory;
30+
import org.gradle.api.plugins.JavaBasePlugin;
31+
import org.gradle.api.plugins.JavaPlugin;
32+
import org.gradle.api.provider.Property;
33+
import org.gradle.api.tasks.Internal;
34+
import org.gradle.api.tasks.PathSensitivity;
35+
import org.gradle.api.tasks.TaskAction;
36+
import org.gradle.api.tasks.TaskProvider;
37+
import org.jooq.codegen.gradle.CodegenPluginExtension;
38+
39+
/**
40+
* This plugin spools up a fresh postgres session,
41+
* runs flyway on it, then runs jooq on the result.
42+
* The postgres stays up so that the flyway result
43+
* can be used as a template database for testing.
44+
*/
45+
public class FlywayJooqPlugin implements Plugin<Project> {
46+
private static final String EXTENSION_NAME = "flywayJooq";
47+
48+
public static class Extension extends CodegenPluginExtension {
49+
@Inject
50+
public Extension(
51+
ObjectFactory objects,
52+
ProjectLayout layout) {
53+
super(objects, layout);
54+
}
55+
56+
public final SetupCleanupDockerFlyway setup = new SetupCleanupDockerFlyway();
57+
58+
/** Ensures a database with a template prepared by Flyway is available. */
59+
public void neededBy(TaskProvider<?> taskProvider) {
60+
taskProvider.configure(this::neededBy);
61+
}
62+
63+
/** Ensures a database with a template prepared by Flyway is available. */
64+
public void neededBy(Task task) {
65+
task.dependsOn(DockerUp.TASK_NAME);
66+
task.getInputs().file(setup.dockerComposeFile).withPathSensitivity(PathSensitivity.RELATIVE);
67+
task.getInputs().dir(setup.flywayMigrations).withPathSensitivity(PathSensitivity.RELATIVE);
68+
}
69+
}
70+
71+
@Override
72+
public void apply(Project project) {
73+
// FlywayPlugin needs to be applied first
74+
project.getPlugins().apply(JavaBasePlugin.class);
75+
76+
Extension extension = project.getExtensions().create(EXTENSION_NAME, Extension.class);
77+
extension.configuration(a -> {});
78+
extension.setup.dockerPullOnStartup = !project.getGradle().getStartParameter().isOffline();
79+
80+
// force all jooq versions to match
81+
String jooqVersion = detectJooqVersion();
82+
project.getConfigurations().all(config -> {
83+
config.resolutionStrategy(strategy -> {
84+
strategy.eachDependency(details -> {
85+
String group = details.getRequested().getGroup();
86+
String name = details.getRequested().getName();
87+
if (group.equals("org.jooq") && name.startsWith("jooq")) {
88+
details.useTarget(group + ":" + name + ":" + jooqVersion);
89+
}
90+
});
91+
});
92+
});
93+
94+
// create a jooq task, which will be needed by all compilation tasks
95+
TaskProvider<JooqTask> jooqTask = project.getTasks().register("jooq", JooqTask.class, task -> {
96+
task.setup = extension.setup;
97+
var generator = extension.getExecutions().maybeCreate("").getConfiguration().getGenerator();
98+
task.generatorConfig = generator;
99+
task.getGeneratedSource().set(project.file(generator.getTarget().getDirectory()));
100+
});
101+
extension.neededBy(jooqTask);
102+
project.getTasks().named(JavaPlugin.COMPILE_JAVA_TASK_NAME).configure(task -> {
103+
task.dependsOn(jooqTask);
104+
});
105+
106+
project.getTasks().register(DockerDown.TASK_NAME, DockerDown.class, task -> {
107+
task.getSetupCleanup().set(extension.setup);
108+
task.getProjectDir().set(project.getProjectDir());
109+
});
110+
project.getTasks().register(DockerUp.TASK_NAME, DockerUp.class, task -> {
111+
task.getSetupCleanup().set(extension.setup);
112+
task.getProjectDir().set(project.getProjectDir());
113+
task.mustRunAfter(DockerDown.TASK_NAME);
114+
});
115+
}
116+
117+
public abstract static class DockerUp extends DefaultTask {
118+
private static final String TASK_NAME = "dockerUp";
119+
120+
@Internal
121+
public abstract DirectoryProperty getProjectDir();
122+
123+
@Internal
124+
public abstract Property<SetupCleanupDockerFlyway> getSetupCleanup();
125+
126+
@TaskAction
127+
public void dockerUp() throws Exception {
128+
getSetupCleanup().get().start(getProjectDir().get().getAsFile());
129+
}
130+
}
131+
132+
public abstract static class DockerDown extends DefaultTask {
133+
private static final String TASK_NAME = "dockerDown";
134+
135+
@Internal
136+
public abstract DirectoryProperty getProjectDir();
137+
138+
@Internal
139+
public abstract Property<SetupCleanupDockerFlyway> getSetupCleanup();
140+
141+
@TaskAction
142+
public void dockerDown() throws Exception {
143+
getSetupCleanup().get().forceStop(getProjectDir().get().getAsFile());
144+
}
145+
}
146+
147+
/** Detects the jooq version. */
148+
private static String detectJooqVersion() {
149+
URLClassLoader loader = (URLClassLoader) FlywayJooqPlugin.class.getClassLoader();
150+
for (URL url : loader.getURLs()) {
151+
Pattern pattern = Pattern.compile("(.*)/jooq-([0-9,.]*?).jar$");
152+
Matcher matcher = pattern.matcher(url.getPath());
153+
if (matcher.matches()) {
154+
return matcher.group(2);
155+
}
156+
}
157+
throw new IllegalStateException("Unable to detect jooq version.");
158+
}
159+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (C) 2025 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.webtools.flywayjooq;
17+
18+
import com.diffplug.common.base.Preconditions;
19+
import com.diffplug.common.base.Throwables;
20+
import java.io.File;
21+
import java.net.ConnectException;
22+
import org.gradle.api.DefaultTask;
23+
import org.gradle.api.GradleException;
24+
import org.gradle.api.file.DirectoryProperty;
25+
import org.gradle.api.tasks.*;
26+
import org.jooq.codegen.GenerationTool;
27+
import org.jooq.meta.jaxb.Configuration;
28+
import org.jooq.meta.jaxb.Generator;
29+
import org.jooq.meta.jaxb.Logging;
30+
31+
@CacheableTask
32+
public abstract class JooqTask extends DefaultTask {
33+
SetupCleanupDockerFlyway setup;
34+
Generator generatorConfig;
35+
36+
@Internal
37+
public SetupCleanupDockerFlyway getSetup() {
38+
return setup;
39+
}
40+
41+
@OutputDirectory
42+
public abstract DirectoryProperty getGeneratedSource();
43+
44+
@Input
45+
public Generator getGeneratorConfig() {
46+
return generatorConfig;
47+
}
48+
49+
@TaskAction
50+
public void generate() throws Exception {
51+
String targetDir = generatorConfig.getTarget().getDirectory();
52+
Preconditions.checkArgument(!(new File(targetDir).isAbsolute()), "`generator.target.directory` must not be absolute, was `%s`", targetDir);
53+
// configure jooq to run against the db
54+
try {
55+
generatorConfig.getTarget().setDirectory(getGeneratedSource().get().getAsFile().getAbsolutePath());
56+
Configuration jooqConfig = new Configuration();
57+
jooqConfig.setGenerator(generatorConfig);
58+
jooqConfig.setLogging(Logging.TRACE);
59+
60+
// write the config out to file
61+
GenerationTool tool = new GenerationTool();
62+
tool.setDataSource(setup.getConnection());
63+
tool.run(jooqConfig);
64+
} catch (Exception e) {
65+
var rootCause = Throwables.getRootCause(e);
66+
if (rootCause instanceof ConnectException) {
67+
throw new GradleException("Unable to connect to the database. Is the docker container running?", e);
68+
} else {
69+
throw e;
70+
}
71+
} finally {
72+
generatorConfig.getTarget().setDirectory(targetDir);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)