From 57aa86614499bb43182f0c764fcf01a67890f287 Mon Sep 17 00:00:00 2001 From: Zakaria Date: Sun, 25 Oct 2026 18:07:31 -0400 Subject: [PATCH] fix(athena): report a failed query's error the way Athena does StateChangeReason carried the raw engine call failure, HTTP envelope and all: floci-duck execute returned HTTP 500: {"status":"error","message":...}, ending with the statement as the engine saw it, wrapped in COPY (...) TO. It is now the engine's own message alone, such as Catalog Error: Table with name nope does not exist! --- .../floci/services/athena/AthenaService.java | 29 ++++++++++++++++++- .../services/athena/AthenaQueryTextTest.java | 22 ++++++++++++++ .../athena/AthenaTrailingSemicolonTest.java | 15 ---------- 3 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/test/java/io/github/hectorvent/floci/services/athena/AthenaQueryTextTest.java delete mode 100644 src/test/java/io/github/hectorvent/floci/services/athena/AthenaTrailingSemicolonTest.java diff --git a/src/main/java/io/github/hectorvent/floci/services/athena/AthenaService.java b/src/main/java/io/github/hectorvent/floci/services/athena/AthenaService.java index 13251fb..872668f 100644 --- a/src/main/java/io/github/hectorvent/floci/services/athena/AthenaService.java +++ b/src/main/java/io/github/hectorvent/floci/services/athena/AthenaService.java @@ -59,6 +59,8 @@ public class AthenaService { public static final String DEFAULT_CATALOG = "AwsDataCatalog"; private static final String DEFAULT_OUTPUT_BUCKET = "floci-athena-results"; private static final String DEFAULT_WORKGROUP = "primary"; + private static final Pattern ENGINE_FAILURE = Pattern.compile("^floci-duck \\w+ returned HTTP \\d+: (\\{.*})$", Pattern.DOTALL); + private static final com.fasterxml.jackson.databind.ObjectMapper FAILURE_READER = new com.fasterxml.jackson.databind.ObjectMapper(); private static final Pattern TRAILING_SEMICOLONS = Pattern.compile("[\\s;]+$"); private static final String DEFAULT_ENGINE_VERSION = "Athena engine version 3"; private static final long MIN_BYTES_SCANNED_CUTOFF_PER_QUERY = 10_000_000L; @@ -610,10 +612,35 @@ public class AthenaService { private void markFailed(String id, QueryExecution execution, Throwable failure) { execution.getStatus().setState(QueryExecutionState.FAILED); - execution.getStatus().setStateChangeReason(failure.getMessage()); + execution.getStatus().setStateChangeReason(failureReason(failure.getMessage())); queryStore.put(id, execution); } + /** + * The engine's own explanation of a failed query, as Athena reports it: without the HTTP + * envelope of the engine call, and without the echoed statement, which shows the + * internal {@code COPY ... TO} wrapper rather than the query that was sent. + */ + static String failureReason(String message) { + if (message == null) { + return null; + } + Matcher envelope = ENGINE_FAILURE.matcher(message); + if (!envelope.matches()) { + return message; + } + try { + String engineMessage = FAILURE_READER.readTree(envelope.group(1)).path("message").asText(""); + if (engineMessage.isBlank()) { + return message; + } + int echo = engineMessage.indexOf("\n\nLINE "); + return (echo >= 0 ? engineMessage.substring(0, echo) : engineMessage).strip(); + } catch (Exception e) { + return message; + } + } + private record CreateDatabaseDdl(String name, boolean ifNotExists, String comment, String location, Map properties) { } diff --git a/src/test/java/io/github/hectorvent/floci/services/athena/AthenaQueryTextTest.java b/src/test/java/io/github/hectorvent/floci/services/athena/AthenaQueryTextTest.java new file mode 100644 index 0000000..e68d548 --- /dev/null +++ b/src/test/java/io/github/hectorvent/floci/services/athena/AthenaQueryTextTest.java @@ -0,0 +1,22 @@ +package io.github.hectorvent.floci.services.athena; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AthenaQueryTextTest { + + @Test + void dropsTheClosingSemicolonsAthenaAccepts() { + assertEquals("SELECT * FROM orders LIMIT 10", AthenaService.withoutTrailingSemicolons("SELECT * FROM orders LIMIT 10;")); + assertEquals("SELECT 1", AthenaService.withoutTrailingSemicolons("SELECT 1 ; ;\n")); + assertEquals("SELECT ';' AS s", AthenaService.withoutTrailingSemicolons("SELECT ';' AS s")); + } + + @Test + void reportsTheEngineErrorWithoutItsEnvelopeOrTheWrappedStatement() { + String engine = "floci-duck execute returned HTTP 500: {\"status\":\"error\",\"message\":\"Catalog Error: Table with name nope does not exist!\\nDid you mean \\\"orders\\\"?\\n\\nLINE 1: COPY (SELECT * FROM nope) TO 's3://results/q.csv'\"}"; + assertEquals("Catalog Error: Table with name nope does not exist!\nDid you mean \"orders\"?", AthenaService.failureReason(engine)); + assertEquals("Failed to call floci-duck execute: timeout", AthenaService.failureReason("Failed to call floci-duck execute: timeout")); + } +} diff --git a/src/test/java/io/github/hectorvent/floci/services/athena/AthenaTrailingSemicolonTest.java b/src/test/java/io/github/hectorvent/floci/services/athena/AthenaTrailingSemicolonTest.java deleted file mode 100644 index c19e3b8..0000000 --- a/src/test/java/io/github/hectorvent/floci/services/athena/AthenaTrailingSemicolonTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.hectorvent.floci.services.athena; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class AthenaTrailingSemicolonTest { - - @Test - void dropsTheClosingSemicolonsAthenaAccepts() { - assertEquals("SELECT * FROM orders LIMIT 10", AthenaService.withoutTrailingSemicolons("SELECT * FROM orders LIMIT 10;")); - assertEquals("SELECT 1", AthenaService.withoutTrailingSemicolons("SELECT 1 ; ;\n")); - assertEquals("SELECT ';' AS s", AthenaService.withoutTrailingSemicolons("SELECT ';' AS s")); - } -}