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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ 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, a
query it cannot compile, an output directory or file it cannot write, a stale
generated file it cannot delete, or an output manifest it cannot read or write.
schema it cannot parse, a query it cannot compile, an output directory or file
it cannot write, a stale generated file it cannot delete, or an output manifest
it cannot read or write.

## Getting started

Expand Down
20 changes: 12 additions & 8 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,21 @@ sqlcj: Generated file paths for repositories 'UserData' and 'Userdata' differ on

Invalid configuration, unreadable SQL sources, and output that cannot be written
or cleaned up 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:
exit with status `1`. The message names the configuration file and, when
available, the offending field, value, source path, or output path, and ends
with the fact that explains the failure, such as the reason the filesystem
reports or the problem the YAML parser reports with its line and column:

```text
sqlcj: Cannot read configuration file: /home/dev/project/sqlcj.yaml: No such file or directory
sqlcj: Malformed configuration file: /home/dev/project/sqlcj.yaml: expected <block end>, but found '<block mapping start>' at line 2, column 3
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
sqlcj: Cannot read output manifest: /home/dev/project/target/generated-sources/sqlcj/sqlcj-manifest.txt
sqlcj: Cannot delete stale generated file: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated/OrdersRepository.java
sqlcj: Cannot write output manifest: /home/dev/project/target/generated-sources/sqlcj/sqlcj-manifest.txt
sqlcj: Cannot read queries source: /home/dev/project/sql/missing.sql: No such file or directory
sqlcj: Cannot create output directory: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated: Not a directory
sqlcj: Cannot write generated file: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated/UsersRepository.java: Permission denied
sqlcj: Cannot read output manifest: /home/dev/project/target/generated-sources/sqlcj/sqlcj-manifest.txt: Permission denied
sqlcj: Cannot delete stale generated file: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated/OrdersRepository.java: Permission denied
sqlcj: Cannot write output manifest: /home/dev/project/target/generated-sources/sqlcj/sqlcj-manifest.txt: Permission denied
```

## Generated Output and Failures
Expand Down
14 changes: 8 additions & 6 deletions docs/postgresql.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,20 +219,22 @@ accepted:

### Failure Behavior

An unsupported type or an unsupported statement stops compilation. `sqlcj
generate` prints a single message on standard error and exits with a non-zero
status. Because every configured source is analyzed and generated before the
run writes its first file, such a failure writes no generated file, and output
An unsupported type, an unsupported statement, or a schema sqlcj cannot parse
stops compilation. `sqlcj generate` prints a single message on standard error
and exits with status `1`. Because every configured source is analyzed and
generated before the run writes its first file, such a failure writes no
generated file, and output
written by an earlier successful run is left unchanged. The writing step itself
is sequential rather than atomic; see
[Generated Output and Failures](configuration.md#generated-output-and-failures).

The message names the schema source and the offending type or statement:
The message names the schema source and the offending type or statement, or the
syntax error with the line and column it was found at:

```text
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported SQL column type: TIMESTAMPTZ
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported schema statement: Alter
sqlcj: Invalid schema source /home/dev/project/schema.sql: Failed to parse schema
sqlcj: Invalid schema source /home/dev/project/schema.sql: Encountered unexpected token: ";" <ST_SEMICOLON> at line 4, column 1
```

## Verified by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import dev.sqlcj.config.SqlConfig;
import dev.sqlcj.io.DefaultFileLoader;
import dev.sqlcj.io.FileLoader;
import dev.sqlcj.io.FileSystemReason;
import dev.sqlcj.parser.DefaultQueryParser;
import dev.sqlcj.parser.Query;
import dev.sqlcj.parser.QueryParser;
Expand Down Expand Up @@ -53,7 +54,8 @@ private String read(Path path, String kind) {
return fileLoader.read(path);
} catch (IOException e) {
throw new CompilationException(
"Cannot read %s source: %s".formatted(kind, path),
"Cannot read %s source: %s: %s"
.formatted(kind, path, FileSystemReason.of(e)),
e
);
}
Expand Down
80 changes: 70 additions & 10 deletions sqlcj-cli/src/main/java/dev/sqlcj/config/YamlConfigLoader.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package dev.sqlcj.config;

import dev.sqlcj.io.FileSystemReason;
import org.snakeyaml.engine.v2.exceptions.Mark;
import org.snakeyaml.engine.v2.exceptions.MarkedYamlEngineException;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.DeserializationFeature;
Expand All @@ -12,11 +15,13 @@
import tools.jackson.databind.type.LogicalType;
import tools.jackson.dataformat.yaml.YAMLMapper;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class YamlConfigLoader implements ConfigLoader {

Expand Down Expand Up @@ -55,16 +60,12 @@ public Config load(Path path) {
}

private Config read(Path configFile) {
if (!Files.isReadable(configFile)) {
throw new ConfigurationException(
"Cannot read configuration file: " + configFile
);
}
byte[] content = readBytes(configFile);

Config config;

try {
config = OBJECT_MAPPER.readValue(configFile.toFile(), Config.class);
config = OBJECT_MAPPER.readValue(content, Config.class);
} catch (UnrecognizedPropertyException e) {
throw new ConfigurationException(
"Invalid configuration in %s: unknown field '%s'"
Expand All @@ -74,10 +75,7 @@ private Config read(Path configFile) {
} catch (MismatchedInputException e) {
throw invalidValue(configFile, e);
} catch (JacksonException e) {
throw new ConfigurationException(
"Malformed configuration file: " + configFile,
e
);
throw malformed(configFile, e);
}

if (config == null) {
Expand All @@ -89,6 +87,68 @@ private Config read(Path configFile) {
return config;
}

private byte[] readBytes(Path configFile) {
try {
return Files.readAllBytes(configFile);
} catch (IOException e) {
throw new ConfigurationException(
"Cannot read configuration file: %s: %s"
.formatted(configFile, FileSystemReason.of(e)),
e
);
}
}

/**
* Translates a YAML syntax failure into a configuration diagnostic naming
* the configuration file and the problem the YAML parser reports, with the
* one-based line and column it reports the problem at.
*/
private ConfigurationException malformed(Path configFile, JacksonException e) {
MarkedYamlEngineException yamlFailure = yamlFailure(e);

Optional<Mark> mark = yamlFailure == null
? Optional.empty()
: yamlFailure.getProblemMark();

String detail = mark
.map(
problemMark -> "%s at line %d, column %d".formatted(
yamlFailure.getProblem(),
problemMark.getLine() + 1,
problemMark.getColumn() + 1
)
)
.orElseGet(() -> firstLine(e.getOriginalMessage(), e));

return new ConfigurationException(
"Malformed configuration file: %s: %s".formatted(configFile, detail),
e
);
}

/** The YAML failure the exception chain carries, or {@code null}. */
private MarkedYamlEngineException yamlFailure(JacksonException e) {
for (Throwable cause = e; cause != null; cause = cause.getCause()) {
if (cause instanceof MarkedYamlEngineException yamlFailure) {
return yamlFailure;
}
}

return null;
}

private String firstLine(String message, JacksonException e) {
if (message == null || message.isBlank()) {
return e.getClass().getSimpleName();
}

return message.lines()
.findFirst()
.orElse(message)
.trim();
}

/**
* Translates a Jackson mapping failure into a configuration diagnostic
* naming the configuration file, the offending property path, and the
Expand Down
50 changes: 50 additions & 0 deletions sqlcj-cli/src/main/java/dev/sqlcj/io/FileSystemReason.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dev.sqlcj.io;

import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystemException;
import java.nio.file.NoSuchFileException;

/**
* The corrective fact of a filesystem failure, such as {@code No such file or
* directory}.
*
* <p>The fact never repeats the path, because every diagnostic that uses it
* already names the file or directory it failed on.
*/
public final class FileSystemReason {

private FileSystemReason() {
}

/** The corrective fact of one filesystem failure. */
public static String of(IOException failure) {
if (failure instanceof NoSuchFileException) {
return "No such file or directory";
}

if (failure instanceof AccessDeniedException) {
return "Permission denied";
}

if (failure instanceof FileAlreadyExistsException) {
return "File exists";
}

if (failure instanceof FileSystemException fileSystemFailure && fileSystemFailure.getReason() != null) {
return fileSystemFailure.getReason().trim();
}

String message = failure.getMessage();

if (message == null || message.isBlank()) {
return failure.getClass().getSimpleName();
}

return message.lines()
.findFirst()
.orElse(message)
.trim();
}
}
12 changes: 10 additions & 2 deletions sqlcj-cli/src/main/java/dev/sqlcj/io/GeneratedFileWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ public void write(GeneratedFile file, Path outputDirectory) throws IOException {
try {
Files.createDirectories(parent);
} catch (IOException e) {
throw new IOException("Cannot create output directory: " + parent, e);
throw new IOException(
"Cannot create output directory: %s: %s"
.formatted(parent, FileSystemReason.of(e)),
e
);
}
}

Expand All @@ -30,7 +34,11 @@ public void write(GeneratedFile file, Path outputDirectory) throws IOException {
StandardOpenOption.TRUNCATE_EXISTING
);
} catch (IOException e) {
throw new IOException("Cannot write generated file: " + outputFile, e);
throw new IOException(
"Cannot write generated file: %s: %s"
.formatted(outputFile, FileSystemReason.of(e)),
e
);
}
}
}
18 changes: 15 additions & 3 deletions sqlcj-cli/src/main/java/dev/sqlcj/io/GeneratedOutputManifest.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ public List<String> read(Path outputDirectory) throws IOException {
Files.readAllLines(manifestPath, StandardCharsets.UTF_8)
);
} catch (IOException e) {
throw new IOException("Cannot read output manifest: " + manifestPath, e);
throw new IOException(
"Cannot read output manifest: %s: %s"
.formatted(manifestPath, FileSystemReason.of(e)),
e
);
}
}

Expand Down Expand Up @@ -84,7 +88,11 @@ public void write(Path outputDirectory, List<Path> generatedPaths) throws IOExce
StandardOpenOption.TRUNCATE_EXISTING
);
} catch (IOException e) {
throw new IOException("Cannot write output manifest: " + manifestPath, e);
throw new IOException(
"Cannot write output manifest: %s: %s"
.formatted(manifestPath, FileSystemReason.of(e)),
e
);
}
}

Expand Down Expand Up @@ -118,7 +126,11 @@ private void deleteStaleEntry(

Files.delete(file);
} catch (IOException e) {
throw new IOException("Cannot delete stale generated file: " + file, e);
throw new IOException(
"Cannot delete stale generated file: %s: %s"
.formatted(file, FileSystemReason.of(e)),
e
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import dev.sqlcj.schema.ConstraintType;
import dev.sqlcj.schema.Schema;
import dev.sqlcj.schema.Table;
import dev.sqlcj.sql.SqlParseReason;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.MultiPartName;
Expand Down Expand Up @@ -47,7 +48,7 @@ public Schema parse(String sql) {

return new Schema(tables);
} catch (JSQLParserException e) {
throw new SchemaParseException("Failed to parse schema", e);
throw new SchemaParseException(SqlParseReason.of(e, true), e);
}
}

Expand Down
4 changes: 2 additions & 2 deletions sqlcj-cli/src/main/java/dev/sqlcj/sql/SqlParseException.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public SqlParseException(String message) {
super(message);
}

public SqlParseException(Throwable cause) {
super(cause);
public SqlParseException(String message, Throwable cause) {
super(message, cause);
}
}
Loading
Loading