Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` 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

Expand Down
22 changes: 18 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` 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

Expand Down Expand Up @@ -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 `~`,
Expand Down
8 changes: 6 additions & 2 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path>` 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.

Expand Down Expand Up @@ -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 <path to sqlcj.yaml>` 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`.

Expand Down
22 changes: 15 additions & 7 deletions sqlcj-cli/src/main/java/dev/sqlcj/cli/command/GenerateCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,17 +21,28 @@
)
public class GenerateCommand implements Callable<Integer> {

private static final String CONFIG_FILE = "sqlcj.yaml";

private final ConfigLoader configLoader = new YamlConfigLoader();

@Spec
CommandSpec spec;

@Option(
names = "--config",
paramLabel = "<path>",
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;
Expand All @@ -42,8 +54,4 @@ public Integer call() {
return CommandLine.ExitCode.SOFTWARE;
}
}

private Path configPath() {
return Path.of(CONFIG_FILE);
}
}
106 changes: 95 additions & 11 deletions sqlcj-cli/src/test/java/dev/sqlcj/cli/command/GenerateCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<Path> 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(
Expand Down Expand Up @@ -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<String> 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());
Expand Down
Loading