Usage Guide: Error Handling
If there are inaccuracies discovered with this documentation, please submit a GitHub issue. |
Introduction
The PartiQL library provides a robust error reporting mechanism, and this usage guide aims to show how you can leverage the exposed APIs.
Who is this for?
This usage guide is aimed at developers who use any one of PartiQL’s components for their application.
If you are looking for how to change how errors are reported in the CLI, please see the CLI help flag. |
To elaborate on why this usage guide may be useful to you, the developer, let us assume that your company provides an interface to enable your customers to execute PartiQL queries. When a user is typing a query and references a table that does not exist, your interface might want to highlight that error and halt processing of the query to save on computational costs. Or, your CLI might want to highlight the error but continue processing the query to accumulate errors to better enable the developer to see all of their mistakes at once. In any case, the PartiQL library allows developers to register their own error listeners to take control over their customers’ experience.
Error Listeners
Each major component (parser, planner, compiler) of the PartiQL Library allows for the registration of an PErrorListener
that will receive every warning/error that the particular component emits.
The default error listener aborts the component’s execution, by throwing an
PRuntimeException
, upon encountering the first error.
This behavior aims to protect developers who might have decided to avoid reading this documentation.
However, as seen further below, this is easy to override.
Halting a Component’s Execution
In the scenario where you want to halt one of the components when a particular warning/error is emitted, error listeners have the ability to throw an PRuntimeException
.
This exception acts as a wrapper over any PError
you’d like to halt with.
In the following code snippet, the AbortWhenAlwaysMissing
will halt processing upon receiving a PError
with severity PError
or upon receiving a PError
with code
PError.ALWAYS_MISSING
.
import org.jetbrains.annotations.NotNull;
import org.partiql.spi.errors.PError;
import org.partiql.spi.errors.PErrorKind;
import org.partiql.spi.errors.PErrorListener;
import org.partiql.spi.errors.PRuntimeException;
import org.partiql.spi.errors.Severity;
import java.util.HashMap;
class AbortWhenAlwaysMissing implements PErrorListener {
@Override
public void report(@NotNull PError error) throws PRuntimeException {
if (error.severity.code() == Severity.ERROR) {
throw new PRuntimeException(error);
}
if (error.code() == PError.ALWAYS_MISSING) {
throw new PRuntimeException(error);
}
System.out.println("w: " + getErrorMessage(error));
}
@NotNull
private String getErrorMessage(@NotNull PError error) {
// Internal implementation details
return "TODO!";
}
private static PRuntimeException internalError(@NotNull Throwable t) {
return new PRuntimeException(
new PError(
PError.INTERNAL_ERROR,
Severity.ERROR(),
PErrorKind.EXECUTION(),
null,
new HashMap<>() {{
put("CAUSE", t);
}}
)
);
}
}
If you throw an exception that is not an
PRuntimeException , the component that contains your registered
PErrorListener will catch the exception and send an error to your
PErrorListener with a code of Error.INTERNAL_ERROR .
This will lead to a duplication of errors (which can be a bad experience for your customers).
|
In the above example, we threw a PRuntimeException
, however, it is possible to extend the class to throw exceptions with their own Java-safe properties.
For example:
import org.jetbrains.annotations.NotNull;
import org.partiql.spi.errors.PError;
import org.partiql.spi.errors.PErrorKind;
import org.partiql.spi.errors.PRuntimeException;
import org.partiql.spi.errors.Severity;
public class SomeCustomPRuntimeException extends PRuntimeException {
/**
* With this, our custom exception can provide a JVM type-safe way to send information
* from your {@link org.partiql.spi.errors.PErrorListener}.
*/
@NotNull
public final String someUsefulMetadata;
/**
* Creates an exception with code {@link PError#INTERNAL_ERROR}
*
* @param someUsefulMetadata some useful metadata as an example
*/
public SomeCustomPRuntimeException(@NotNull String someUsefulMetadata) {
super(error());
this.someUsefulMetadata = someUsefulMetadata;
}
@NotNull
private static PError error() {
return new PError(
PError.INTERNAL_ERROR,
Severity.ERROR(),
PErrorKind.EXECUTION(),
null,
null
);
}
}
Registering Error Listeners
Each component allows for the registration of a custom error listener upon instantiation.
For example, let’s say you intend on registering the
AbortWhenAlwaysMissing
error listener from above, while also handling the possibility of a SomeCustomPRuntimeException
from being thrown.
The below example does just that.
import org.partiql.parser.PartiQLParser;
import org.partiql.spi.Context;
import org.partiql.spi.errors.PErrorListener;
import org.partiql.spi.errors.PRuntimeException;
public class Foo {
public static void main(String[] args) {
// User Input
String query = args[0];
// Register Error Listener
PErrorListener listener = new AbortWhenAlwaysMissing();
Context context = Context.of(listener); // Initial registration here!!
// Create Parser
PartiQLParser parser = PartiQLParser.standard();
// Parse
PartiQLParser.Result parseResult;
try {
// Registering the Error Listener with the Parser!
parseResult = parser.parse(query, context);
} catch (SomeCustomPRuntimeException ex) {
// This is an example on how to extend PRuntimeException
System.out.println("Some useful metadata: " + ex.someUsefulMetadata);
throw ex;
} catch (PRuntimeException ex) {
// Alternatively, you can use the existing properties map to get information out.
// Please see the Javadocs for each PError code to see what properties may be available.
String value = ex.getError().getOrNull("SOME_OTHER_INFO", String.class);
System.out.println("Some useful info: " + value);
throw ex;
}
// Do more ...
}
}
Errors and Warnings
Errors and warnings are both represented by the same data structure, a
PError
, and is emitted via PErrorListener#report(PError)
.
The PError
Java class allows for developers to introspect its properties to determine how to create their own error messages.
See the
[Javadocs] for the available methods.
As an example, the
PError.DIVISION_BY_ZERO allows for the retrieval of a DIVIDEND
and DIVIDEND_TYPE
.
Writing Quality Error Messages
As mentioned above, the PError
Java class exposes information for database implementers to write high quality error messages.
Specifically, PError
exposes a method, int getCode()
, to return the enumerated error code received.
All possible error codes are represented as static final fields in the
PError Javadocs.
An error code MAY have additional properties accessible via the
.get…(…)
APIs – please consult the Javadocs for an error code’s property usage.
It is recommended to use PError#getOrNull and/or
PError#getListOrNull to safely retrieve the underlying information.
As an example, a PError
with code
PError.DIVISION_BY_ZERO allows for the retrieval of a DIVIDEND (String)
and DIVIDEND_TYPE
.
Here is an example on how to safely retrieve the dividend:
import org.partiql.spi.errors.PError
fun getDividend(pError: PError): String? {
if (pError.code() == PError.DIVISION_BY_ZERO) {
return pError.getOrNull("DIVIDEND", String::class.java)
}
return null
}
Now, below is an example on how to create a PErrorListener
that emits high-quality error messages.
Notice that we prepend an "e" when an error is received and a "w" when a warning is received.
In this example, we only handle the error codes of PError.ALWAYS_MISSING
and PError.FEATURE_NOT_SUPPORTED
.
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.partiql.spi.SourceLocation;
import org.partiql.spi.errors.PError;
import org.partiql.spi.errors.PErrorListener;
import org.partiql.spi.errors.PRuntimeException;
import org.partiql.spi.errors.Severity;
public class ConsolePErrorListener implements PErrorListener {
boolean hasErrors = false;
@Override
public void report(@NotNull PError error) throws PRuntimeException {
String message;
switch (error.severity.code()) {
case Severity.ERROR:
message = getMessage(error, "e: ");
System.out.println(message);
hasErrors = true;
break;
case Severity.WARNING:
message = getMessage(error, "w: ");
System.out.println(message);
break;
default:
throw new PRuntimeException(error);
}
}
static String getMessage(@NotNull PError error, @NotNull String prefix) {
switch (error.code()) {
case PError.ALWAYS_MISSING:
SourceLocation location = error.location;
String locationStr = getNullSafeLocation(location);
return prefix + locationStr + " Expression always evaluates to missing.";
case PError.FEATURE_NOT_SUPPORTED:
String name = error.getOrNull("FEATURE_NAME", String.class);
if (name == null) {
name = "UNKNOWN";
}
return prefix + "Feature (" + name + ") not yet supported.";
default:
return "Unhandled error code received.";
}
}
@NotNull
static String getNullSafeLocation(@Nullable SourceLocation location) {
// Internal implementation
return "Not_yet_implemented";
}
}
A Component’s Output Structures
Notice that, in the above example, we did not halt execution when we received an error.
Rather, we set hasErrors
to true
.
This was intentional.
Each of PartiQL’s components produce a structure for future use. The parser outputs an AST, the planner outputs a plan, and the compiler outputs an executable. What happens when any of the components experience an error/warning?
The answer, as is often in software, depends. Since this error reporting mechanism allows developers to register error listeners that accumulate all errors, the PartiQL components still continue processing until terminated by an error listener. That being said, when error listeners receive an error, one must assume that the output of the component is a dud and is incorrect. Therefore, if the parser has produced errors with a malformed AST, you shouldn’t pass the AST to the planner to continue evaluation. However, if warnings have been emitted, the output can still be safely relied upon.
Oftentimes, some of your users' queries will hold many errors, and it might be a good idea to show them all of them at once (or at least some of them at once). If we were to register this error listener, we could check this flag before proceeding with further computation.
Below, we have created a ConsoleApp
that registers the new listener and checks the flag before proceeding with the computation.
import org.partiql.parser.PartiQLParser;
import org.partiql.spi.Context;
import org.partiql.spi.errors.PRuntimeException;
public class ConsoleApp {
public static void main(String[] args) {
// User Input
String query = args[0];
// Register Error Listener
ConsolePErrorListener listener = new ConsolePErrorListener(); // The new error listener!
Context context = Context.of(listener); // Initial registration here!
// Create Parser
PartiQLParser parser = PartiQLParser.standard();
// Parse
PartiQLParser.Result parseResult;
try {
// Registering the Error Listener with the Parser!
parseResult = parser.parse(query, context);
} catch (PRuntimeException ex) {
// While these exceptions were likely unexpected, the PartiQL library may throw PRuntimeExceptions
// as well. You should be able to unwrap these exceptions and provide quality
// error messages to your users. It might be a good idea to consolidate your logic of
// converting PErrors to user-presented messages and use that here. In this case, we reused
// the logic from our error listener.
ConsolePErrorListener.getMessage(ex.getError(), "e: ");
throw ex;
}
// New check here!!!
if (listener.hasErrors) {
throw new IllegalStateException("Your query has errors. Please see the above errors.");
}
// Do more ...
}
}
What about Execution?
Error listeners are specifically meant to provide control over the reporting of errors for PartiQL’s major components (parser, planner, and compiler).
However, for the execution of compiled statements, PartiQL does not send the PError
to a PErrorListener
.
It instead immediately throws a PRuntimeException
directly.
Here is an example of how you can leverage this functionality below:
import org.partiql.eval.Statement;
import org.partiql.spi.errors.PRuntimeException;
import org.partiql.spi.value.Datum;
class MyApplication {
void executeAndPrint(Statement stmt) {
Datum lazyData;
try {
lazyData = stmt.execute();
// Iterate through the lazyData and print to the console.
} catch (PRuntimeException e) {
System.out.println(
ConsolePErrorListener.getMessage(e.getError(), "e: ")
);
}
}
}
Reference Implementations
The PartiQL CLI offers multiple ways to process warnings/errors.
See the flags -Werror
, -w
, --max-errors
, and more when you run
partiql --help
.
See the PartiQL Kotlin CLI Reference.
The implementation details can be found in the
CLI subproject.