From 486f6dce1f55345064c149b36b64a9b6ecec77c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20=C3=87engel?= Date: Fri, 25 Sep 2026 22:56:06 +0300 Subject: [PATCH 1/2] fix: report output write failures as one sqlcj diagnostic --- .../sqlcj/compiler/CompilationException.java | 3 +- .../dev/sqlcj/compiler/SqlcjCompiler.java | 3 +- .../dev/sqlcj/io/GeneratedFileWriter.java | 22 +++++--- .../cli/command/GenerateCommandTest.java | 39 ++++++++++++++ .../SqlcjCompilerIntegrationTest.java | 51 +++++++++++++++++++ .../dev/sqlcj/io/GeneratedFileWriterTest.java | 46 +++++++++++++++++ 6 files changed, 154 insertions(+), 10 deletions(-) diff --git a/sqlcj-cli/src/main/java/dev/sqlcj/compiler/CompilationException.java b/sqlcj-cli/src/main/java/dev/sqlcj/compiler/CompilationException.java index 9b318ab..6443a0e 100644 --- a/sqlcj-cli/src/main/java/dev/sqlcj/compiler/CompilationException.java +++ b/sqlcj-cli/src/main/java/dev/sqlcj/compiler/CompilationException.java @@ -1,7 +1,8 @@ package dev.sqlcj.compiler; /** - * Signals that configured SQL sources cannot be loaded or compiled. + * Signals that configured SQL sources cannot be loaded or compiled, or that + * their generated output cannot be written. */ public class CompilationException extends RuntimeException { diff --git a/sqlcj-cli/src/main/java/dev/sqlcj/compiler/SqlcjCompiler.java b/sqlcj-cli/src/main/java/dev/sqlcj/compiler/SqlcjCompiler.java index 5de91bb..dd82b09 100644 --- a/sqlcj-cli/src/main/java/dev/sqlcj/compiler/SqlcjCompiler.java +++ b/sqlcj-cli/src/main/java/dev/sqlcj/compiler/SqlcjCompiler.java @@ -16,7 +16,6 @@ import dev.sqlcj.sql.SqlParser; import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; @@ -198,7 +197,7 @@ private void write(GeneratedFile file, Path outputDirectory) { try { generatedFileWriter.write(file, outputDirectory); } catch (IOException e) { - throw new UncheckedIOException(e); + throw new CompilationException(e.getMessage(), e); } } diff --git a/sqlcj-cli/src/main/java/dev/sqlcj/io/GeneratedFileWriter.java b/sqlcj-cli/src/main/java/dev/sqlcj/io/GeneratedFileWriter.java index 10e0afb..cd656a3 100644 --- a/sqlcj-cli/src/main/java/dev/sqlcj/io/GeneratedFileWriter.java +++ b/sqlcj-cli/src/main/java/dev/sqlcj/io/GeneratedFileWriter.java @@ -15,14 +15,22 @@ public void write(GeneratedFile file, Path outputDirectory) throws IOException { Path parent = outputFile.getParent(); if (parent != null) { - Files.createDirectories(parent); + try { + Files.createDirectories(parent); + } catch (IOException e) { + throw new IOException("Cannot create output directory: " + parent, e); + } } - Files.writeString( - outputFile, - file.content(), - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING - ); + try { + Files.writeString( + outputFile, + file.content(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ); + } catch (IOException e) { + throw new IOException("Cannot write generated file: " + outputFile, e); + } } } 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 9013a25..cb6f8b6 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 @@ -244,6 +244,45 @@ void shouldFailWithConciseDiagnosticForInvalidQuery() throws Exception { assertFalse(Files.exists(workingDirectory.resolve("generated"))); } + @Test + void shouldFailWithConciseDiagnosticForUnwritableOutputDirectory() 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 + """ + ); + + Path output = workingDirectory.resolve("generated"); + + Files.writeString(output, "not a directory"); + + Result result = runGenerate(); + + assertEquals(1, result.exitCode(), result.error()); + + assertEquals( + List.of( + "sqlcj: Cannot create output directory: " + + workingDirectory.resolve("generated/dev/example/generated") + ), + result.error().lines().toList() + ); + + assertFalse(result.error().contains("\tat ")); + assertTrue(Files.isRegularFile(output)); + assertEquals("not a directory", Files.readString(output)); + } + private Result runGenerate(String... arguments) throws IOException, InterruptedException { return runGenerateIn(workingDirectory, arguments); } diff --git a/sqlcj-cli/src/test/java/dev/sqlcj/compiler/SqlcjCompilerIntegrationTest.java b/sqlcj-cli/src/test/java/dev/sqlcj/compiler/SqlcjCompilerIntegrationTest.java index 41248ff..e1b25e0 100644 --- a/sqlcj-cli/src/test/java/dev/sqlcj/compiler/SqlcjCompilerIntegrationTest.java +++ b/sqlcj-cli/src/test/java/dev/sqlcj/compiler/SqlcjCompilerIntegrationTest.java @@ -1478,6 +1478,57 @@ name VARCHAR(255) ); } + @Test + void shouldReportTheGeneratedFileItCannotWrite() throws IOException { + Path usersSchema = tempDir.resolve("users-schema.sql"); + Path usersQueries = tempDir.resolve("users-queries.sql"); + Path generatedDirectory = tempDir.resolve("generated"); + + Files.writeString( + usersSchema, + """ + CREATE TABLE users + ( + id BIGINT NOT NULL, + name VARCHAR(255) + ); + """ + ); + + Files.writeString( + usersQueries, + """ + -- name: GetUser :one + SELECT id + FROM users + WHERE id = $1; + """ + ); + + Path generatedFile = generatedDirectory + .resolve("dev/example/generated") + .resolve("UsersRepository.java"); + + Files.createDirectories(generatedFile); + + Config config = new Config( + List.of(new SqlConfig("Users", usersSchema.toString(), usersQueries.toString())), + new JavaConfig(generatedDirectory.toString(), "dev.example.generated") + ); + + SqlcjCompiler compiler = new SqlcjCompiler(); + + CompilationException exception = assertThrows( + CompilationException.class, + () -> compiler.compile(config) + ); + + assertEquals( + "Cannot write generated file: " + generatedFile, + exception.getMessage() + ); + } + @Test void shouldRejectAnonymousPlaceholderBeforeWriting() { Path generatedDirectory = tempDir.resolve("generated"); diff --git a/sqlcj-cli/src/test/java/dev/sqlcj/io/GeneratedFileWriterTest.java b/sqlcj-cli/src/test/java/dev/sqlcj/io/GeneratedFileWriterTest.java index c67a804..02b535a 100644 --- a/sqlcj-cli/src/test/java/dev/sqlcj/io/GeneratedFileWriterTest.java +++ b/sqlcj-cli/src/test/java/dev/sqlcj/io/GeneratedFileWriterTest.java @@ -9,6 +9,7 @@ import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; class GeneratedFileWriterTest { @@ -57,4 +58,49 @@ void shouldCreateParentDirectories() throws IOException { Files.readString(outputFile) ); } + + @Test + void shouldReportTheDirectoryItCannotCreate() throws IOException { + Path outputDirectory = tempDir.resolve("generated"); + + Files.writeString(outputDirectory, "not a directory"); + + GeneratedFile file = new GeneratedFile( + Path.of("nested", "GetUser.java"), + "public final class GetUser {}" + ); + + IOException failure = assertThrows( + IOException.class, + () -> writer.write(file, outputDirectory) + ); + + assertEquals( + "Cannot create output directory: " + + outputDirectory.resolve(file.path()).getParent(), + failure.getMessage() + ); + } + + @Test + void shouldReportTheFileItCannotWrite() throws IOException { + Path outputDirectory = tempDir.resolve("generated"); + + GeneratedFile file = new GeneratedFile( + Path.of("GetUser.java"), + "public final class GetUser {}" + ); + + Files.createDirectories(outputDirectory.resolve(file.path())); + + IOException failure = assertThrows( + IOException.class, + () -> writer.write(file, outputDirectory) + ); + + assertEquals( + "Cannot write generated file: " + outputDirectory.resolve(file.path()), + failure.getMessage() + ); + } } From f69dd65e1e2e39d23bd3d385a0aeec0c8a7136f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20=C3=87engel?= Date: Fri, 25 Sep 2026 22:56:13 +0300 Subject: [PATCH 2/2] docs: document output write failure diagnostics --- README.md | 4 ++-- docs/configuration.md | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9ba6d94..96152d5 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,8 @@ 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. +after printing one diagnostic for invalid configuration, an unreadable source, a +query it cannot compile, or an output directory or file it cannot write. ## Getting started diff --git a/docs/configuration.md b/docs/configuration.md index f076785..19b3306 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -287,14 +287,16 @@ sqlcj: Generated file paths for repositories 'UserData' and 'Userdata' differ on ## Diagnostics -Invalid configuration and unreadable SQL sources make `sqlcj generate` print a -single message on standard error and exit with a non-zero status. The message -names the configuration file and, when available, the offending field, value, or -source path: +Invalid configuration, unreadable SQL sources, and output that cannot be written +make `sqlcj generate` print a single message on standard error and exit with a +non-zero status. The message names the configuration file and, when available, +the offending field, value, source path, or output path: ```text sqlcj: Invalid configuration in /home/dev/project/sqlcj.yaml: 'java.package' value 'dev.class.generated' is not a valid Java package name sqlcj: Cannot read queries source: /home/dev/project/sql/missing.sql +sqlcj: Cannot create output directory: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated +sqlcj: Cannot write generated file: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated/UsersRepository.java ``` ## Generated Output and Failures @@ -310,6 +312,10 @@ failure part-way through can leave a mixture of newly written files and files from a previous run. sqlcj does not remove or roll back files it has already written. +A filesystem failure while writing ends the run with one diagnostic naming the +output directory or generated file it could not write and exit status `1`. The +files the run had already written stay in place. + sqlcj also never deletes a generated file that the current run did not produce, so a renamed or removed configuration entry leaves its previous repository behind. Generate into a