From ac96c6451e8edd11fe57f402d227a69e3dc96273 Mon Sep 17 00:00:00 2001 From: Zakaria Date: Sun, 25 Oct 2026 18:04:11 -0400 Subject: [PATCH] fix(athena): accept a query that ends in a semicolon Athena runs a statement with or without its closing semicolon, but the query engine wraps it in COPY (...) TO ..., where the semicolon is a syntax error, so SELECT * FROM orders; always failed. Trailing semicolons are now dropped before the statement is handed to the engine. --- .../floci/services/athena/AthenaService.java | 12 +++++++++++- .../athena/AthenaTrailingSemicolonTest.java | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create 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 1b2a0ee..13251fb 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,7 @@ 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 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; private static final String WORKGROUP_RESOURCE = "workgroup/"; @@ -191,7 +192,7 @@ public class AthenaService { if (outputLocation != null) { ensureOutputBucket(outputLocation); } - duckClient.execute(query, setupDdl, outputLocation); + duckClient.execute(withoutTrailingSemicolons(query), setupDdl, outputLocation); return null; }).onSuccess(v -> { markSucceeded(id, execution); @@ -555,6 +556,15 @@ public class AthenaService { return properties; } + /** + * Athena accepts one statement with or without a closing semicolon. The engine wraps the + * statement in {@code COPY (...) TO ...}, where a semicolon is a syntax error, so it is + * dropped before the query is handed over. + */ + static String withoutTrailingSemicolons(String query) { + return query == null ? null : TRAILING_SEMICOLONS.matcher(query).replaceFirst(""); + } + private static boolean producesResultRows(String query) { return RESULT_STATEMENT_PATTERN.matcher(statementText(query)).find(); } 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 new file mode 100644 index 0000000..c19e3b8 --- /dev/null +++ b/src/test/java/io/github/hectorvent/floci/services/athena/AthenaTrailingSemicolonTest.java @@ -0,0 +1,15 @@ +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")); + } +}