From fc300b048f83b7aac24e56522b6d38e202bad892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20=C3=87engel?= Date: Fri, 25 Sep 2026 21:47:08 +0300 Subject: [PATCH 1/2] feat(cli): add --config option to generate --- .../sqlcj/cli/command/GenerateCommand.java | 22 ++-- .../cli/command/GenerateCommandTest.java | 106 ++++++++++++++++-- 2 files changed, 110 insertions(+), 18 deletions(-) diff --git a/sqlcj-cli/src/main/java/dev/sqlcj/cli/command/GenerateCommand.java b/sqlcj-cli/src/main/java/dev/sqlcj/cli/command/GenerateCommand.java index 7a2af0f..c5cabf0 100644 --- a/sqlcj-cli/src/main/java/dev/sqlcj/cli/command/GenerateCommand.java +++ b/sqlcj-cli/src/main/java/dev/sqlcj/cli/command/GenerateCommand.java @@ -9,6 +9,7 @@ import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.Option; import picocli.CommandLine.Spec; import java.nio.file.Path; @@ -20,17 +21,28 @@ ) public class GenerateCommand implements Callable { - private static final String CONFIG_FILE = "sqlcj.yaml"; - private final ConfigLoader configLoader = new YamlConfigLoader(); @Spec CommandSpec spec; + @Option( + names = "--config", + paramLabel = "", + defaultValue = "sqlcj.yaml", + description = """ + Configuration file to generate from. A relative path is \ + resolved against the current directory, while relative paths \ + inside the file are resolved against its own directory. \ + Default: ${DEFAULT-VALUE} + """ + ) + Path configFile; + @Override public Integer call() { try { - Config config = configLoader.load(configPath()); + Config config = configLoader.load(configFile); SqlcjCompiler compiler = new SqlcjCompiler(); compiler.compile(config); return CommandLine.ExitCode.OK; @@ -42,8 +54,4 @@ public Integer call() { return CommandLine.ExitCode.SOFTWARE; } } - - private Path configPath() { - return Path.of(CONFIG_FILE); - } } diff --git a/sqlcj-cli/src/test/java/dev/sqlcj/cli/command/GenerateCommandTest.java b/sqlcj-cli/src/test/java/dev/sqlcj/cli/command/GenerateCommandTest.java index 9477ccd..9013a25 100644 --- a/sqlcj-cli/src/test/java/dev/sqlcj/cli/command/GenerateCommandTest.java +++ b/sqlcj-cli/src/test/java/dev/sqlcj/cli/command/GenerateCommandTest.java @@ -6,15 +6,19 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Exercises the {@code generate} command from its fixed {@code sqlcj.yaml} - * entry point in an isolated working directory. + * Exercises the {@code generate} command from its default {@code sqlcj.yaml} + * entry point and from an explicit {@code --config} path, in an isolated + * working directory. */ class GenerateCommandTest { @@ -69,6 +73,75 @@ void shouldGenerateFromConfigurationInWorkingDirectory() throws Exception { assertTrue(source.contains("public GetUserResult getUser(Long id)")); } + @Test + void shouldGenerateFromExplicitConfigurationPathInAnotherDirectory() throws Exception { + Path project = Files.createDirectory(workingDirectory.resolve("project")); + Path elsewhere = Files.createDirectory(workingDirectory.resolve("elsewhere")); + + Files.writeString(project.resolve("schema.sql"), SCHEMA); + Files.writeString(project.resolve("queries.sql"), QUERIES); + Files.writeString( + project.resolve("sqlcj-users.yaml"), + """ + version: "1" + sql: + - name: Users + schema: schema.sql + queries: queries.sql + java: + package: dev.example.generated + out: generated + """ + ); + + Result result = runGenerateIn(elsewhere, "--config", "../project/sqlcj-users.yaml"); + + assertEquals(0, result.exitCode(), result.error()); + + Path generated = project.resolve("generated/dev/example/generated/UsersRepository.java"); + + assertTrue(Files.exists(generated), result.error()); + assertTrue( + Files.readString(generated).contains("public GetUserResult getUser(Long id)") + ); + + try (Stream entries = Files.list(elsewhere)) { + assertEquals(List.of(), entries.toList()); + } + } + + @Test + void shouldFailWithoutFallbackWhenExplicitConfigurationIsMissing() throws Exception { + Files.writeString(workingDirectory.resolve("schema.sql"), SCHEMA); + Files.writeString(workingDirectory.resolve("queries.sql"), QUERIES); + Files.writeString( + workingDirectory.resolve("sqlcj.yaml"), + """ + version: "1" + sql: + - name: Users + schema: schema.sql + queries: queries.sql + java: + package: dev.example.generated + out: generated + """ + ); + + Result result = runGenerate("--config", "missing.yaml"); + + assertEquals(1, result.exitCode(), result.error()); + assertTrue( + result.error().contains( + "sqlcj: Cannot read configuration file: " + + workingDirectory.resolve("missing.yaml") + ), + result.error() + ); + assertFalse(result.error().contains("\tat ")); + assertFalse(Files.exists(workingDirectory.resolve("generated"))); + } + @Test void shouldFailWithConciseDiagnosticForMalformedConfiguration() throws Exception { Files.writeString( @@ -171,17 +244,28 @@ void shouldFailWithConciseDiagnosticForInvalidQuery() throws Exception { assertFalse(Files.exists(workingDirectory.resolve("generated"))); } - private Result runGenerate() throws IOException, InterruptedException { + private Result runGenerate(String... arguments) throws IOException, InterruptedException { + return runGenerateIn(workingDirectory, arguments); + } + + private Result runGenerateIn(Path directory, String... arguments) + throws IOException, InterruptedException { Path java = Path.of(System.getProperty("java.home"), "bin", "java"); - Process process = new ProcessBuilder( - java.toString(), - "-classpath", - System.getProperty("java.class.path"), - "dev.sqlcj.Main", - "generate" - ) - .directory(workingDirectory.toFile()) + List command = new ArrayList<>( + List.of( + java.toString(), + "-classpath", + System.getProperty("java.class.path"), + "dev.sqlcj.Main", + "generate" + ) + ); + + command.addAll(List.of(arguments)); + + Process process = new ProcessBuilder(command) + .directory(directory.toFile()) .start(); String output = new String(process.getInputStream().readAllBytes()); From 3dfaf3806ee36c5d7aa4ee8ce57ce50dc1dd0399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20=C3=87engel?= Date: Fri, 25 Sep 2026 21:47:16 +0300 Subject: [PATCH 2/2] docs: document the generate --config option --- README.md | 16 +++++++++++++--- docs/configuration.md | 22 ++++++++++++++++++---- docs/quickstart.md | 8 ++++++-- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b909868..9ba6d94 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,19 @@ java -jar sqlcj-cli-0.1.0-SNAPSHOT.jar generate # compile sqlcj.yaml into Java java -jar sqlcj-cli-0.1.0-SNAPSHOT.jar --help # list the commands ``` -`generate` reads `sqlcj.yaml` from the directory it is run in. It exits with -status `0` on success, and with status `1` after printing one diagnostic for -invalid configuration, an unreadable source, or a query it cannot compile. +`generate` reads `sqlcj.yaml` from the directory it is run in. +`--config ` names another configuration file, so generation can run from +any working directory: + +```bash +java -jar sqlcj-cli-0.1.0-SNAPSHOT.jar generate --config ../project/sqlcj.yaml +``` + +A relative `--config` value is resolved against the directory the command is +run in, while the relative paths inside the file stay relative to the file's own +directory. `generate` exits with status `0` on success, and with status `1` +after printing one diagnostic for invalid configuration, an unreadable source, +or a query it cannot compile. ## Getting started diff --git a/docs/configuration.md b/docs/configuration.md index 5c2a038..f076785 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -12,8 +12,21 @@ The `generate` command reads `sqlcj.yaml` from the directory it is run in: sqlcj generate ``` -There is no configuration-path option and no configuration discovery in parent -directories. +`--config ` selects another file, so generation can run from any working +directory: + +```bash +sqlcj generate --config ../project/sqlcj.yaml +``` + +The option defaults to `sqlcj.yaml`, and a relative value is resolved against +the directory the command is run in. Relative paths inside the file are still +resolved against the directory that contains the file, not against the working +directory. A missing or invalid explicit file reports the usual configuration +diagnostic and exits with status `1`; there is no fallback to `sqlcj.yaml`. + +There is no short alias, no environment variable, and no configuration +discovery in parent directories. ## Supported Version @@ -109,8 +122,9 @@ generated/dev/example/generated/AuthorRepository.java ## Path Resolution A relative `schema`, `queries`, or `java.out` path is resolved against the -directory that contains `sqlcj.yaml`, not against the process working directory -at a later point in time. An absolute path is used as-is. +directory that contains the configuration file, whether that file is the +default `sqlcj.yaml` or one named by `--config`, and not against the process +working directory at a later point in time. An absolute path is used as-is. Resolved paths are normalized lexically, so `../sql/schema.sql` is supported. Resolution does not require `java.out` to exist, does not expand `~`, diff --git a/docs/quickstart.md b/docs/quickstart.md index a534f73..fa7ba34 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -153,6 +153,7 @@ query of `sql/queries.sql`. `sqlcj generate` reads `sqlcj.yaml` from the directory it is run in, and the relative paths above are resolved against the directory that contains the file. +`--config ` names another file when the command runs from elsewhere. Generated output therefore belongs in the build directory, where `mvn clean` removes it. See [Configuration](configuration.md) for the full file format. @@ -414,8 +415,11 @@ mvn exec:java generate, so a query that was renamed or removed would otherwise leave a stale class behind that still compiles. - `sqlcj generate` is a separate command. It is not bound to the Maven - lifecycle, so it must run after `clean` and before `compile`. It must run from - the project root, because it reads `sqlcj.yaml` from the current directory. + lifecycle, so it must run after `clean` and before `compile`. The commands + above run from the project root, because `generate` reads `sqlcj.yaml` from + the current directory; from another directory, pass + `--config ` instead, and the paths inside the file keep + resolving against the project root. - `mvn compile` then compiles `src/main/java` together with `target/generated-sources/sqlcj`.