Repository: floci-io/floci (2026-07-16T17:53:52Z)

Issue:
[BUG] Athena: GetQueryExecution reports OutputLocation as a directory prefix instead of the result CSV object key
**Service**
Athena (result delivery via S3)

**AWS API Action**
GetQueryExecution (after StartQueryExecution)

**Expected behavior**
Real AWS Athena writes the query result to a CSV object named after the query execution id and reports that object's key in `ResultConfiguration.OutputLocation`:

```
s3://<bucket>/<prefix>/<QueryExecutionId>.csv
```

`s3api get-object` on the reported OutputLocation succeeds. SDK-shaped consumers routinely fetch the reported location verbatim to stream results.

**Actual behavior**
floci reports a directory prefix (trailing slash) and writes the object to `results.csv` inside it:

```
"OutputLocation": "s3://probe-results/out/b2e6ffce-5905-468a-baf9-5c5dacdf8b91/"
```

`aws s3 ls s3://probe-results/out/ --recursive` shows the object at `out/<qid>/results.csv`. `get-object` on the reported key verbatim fails:

```
An error occurred (NoSuchKey) when calling the GetObject operation: The specified key does not exist.
```

Constructing the AWS-shaped key `out/<qid>.csv` also returns NoSuchKey — the object only exists at `out/<qid>/results.csv`, so a client cannot compensate by deriving the conventional key.

So even on a `SUCCEEDED` query, any client that follows the AWS contract (fetch the reported OutputLocation) breaks with NoSuchKey. The query engine itself works — the query returned correct data; only the reported location diverges from AWS.

**Reproduction**
```bash
export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1
EP=--endpoint-url=http://localhost:4566

aws $EP s3 mb s3://probe-data
aws $EP s3 mb s3://probe-results
printf '{"eventname":"login","username":"alice"}\n{"eventname":"logout","username":"bob"}\n' > /tmp/events.json
aws $EP s3 cp /tmp/events.json s3://probe-data/events/events.json

aws $EP glue create-database --database-input Name=probedb
aws $EP glue create-table --database-name probedb --table-input '{
  "Name": "events",
  "StorageDescriptor": {
    "Columns": [{"Name":"eventname","Type":"string"},{"Name":"username","Type":"string"}],
    "Location": "s3://probe-data/events/",
    "SerdeInfo": {"SerializationLibrary": "org.openx.data.jsonserde.JsonSerDe"}
  }
}'

QID=$(aws $EP athena start-query-execution \
  --query-string "SELECT eventname, username FROM events WHERE username='alice'" \
  --query-execution-context Database=probedb \
  --result-configuration OutputLocation=s3://probe-results/out/ \
  --query QueryExecutionId --output text)
# first query on a cold container can take minutes while the engine sidecar starts
until [ "$(aws $EP athena get-query-execution --query-execution-id "$QID" \
  --query 'QueryExecution.Status.State' --output text)" = "SUCCEEDED" ]; do sleep 5; done
aws $EP athena get-query-execution --query-execution-id "$QID" \
  --query 'QueryExecution.{State:Status.State,OutputLocation:ResultConfiguration.OutputLocation}'
aws $EP s3 ls s3://probe-results/out/ --recursive
# AWS contract: fetching the key reported in OutputLocation verbatim must succeed — on floci it returns NoSuchKey
OUT=$(aws $EP athena get-query-execution --query-execution-id "$QID" \
  --query 'QueryExecution.ResultConfiguration.OutputLocation' --output text)
aws $EP s3api get-object --bucket probe-results --key "${OUT#s3://probe-results/}" /tmp/result.csv
```

**Environment**
- Floci version: `floci/floci:1.5.30-compat` (also reproduces on `1.5.28-compat`)
- Running via: Docker, `docker run -p 4566:4566 -v /var/run/docker.sock:/var/run/docker.sock floci/floci:1.5.30-compat` (socket mounted so the Athena engine sidecar can start)
- Client: aws-cli 2.x



Discussion:
floci's Athena `GetQueryExecution` reports a directory prefix as the OutputLocation, so a client that GETs it verbatim hits `NoSuchKey`. `resolveOutputLocation` (AthenaService.java:278) returns `<base>/<queryId>/` — always a trailing slash, i.e. a prefix — and `startQueryExecution` writes the result to `outputLocation + "results.csv"` (line 96). So the object lands at `<base>/<queryId>/results.csv` while the reported `ResultConfiguration.OutputLocation` (line 78) is the bare prefix `<base>/<queryId>/`.

Real AWS reports (and writes) the result object itself — `<base>/<QueryExecutionId>.csv` — so a `GetQueryExecution` → `GetObject` round-trip on the reported location succeeds.

The write and read are coupled: `readResultsFromS3` (line 419) reads from the stored OutputLocation by listing the prefix and taking the first `.csv` (line 429), so a fix has to move both to the object key. Proposal: write and report `<base>/<QueryExecutionId>.csv`, and read that key directly in `readResultsFromS3`. Happy to send a PR with a `GetQueryExecution`-then-`GetObject` round-trip test.




Patch:
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 9eb04f11c9..3f075f3006 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
@@ -73,7 +73,8 @@ public String startQueryExecution(String query,
             resolvedContext.setCatalog(DEFAULT_CATALOG);
         }
 
-        // Ensure output location has a trailing slash so floci-duck writes into the prefix
+        // AWS reports the result CSV object itself as the OutputLocation, not a
+        // directory prefix — the same key is written and returned to the client.
         String outputLocation = resolveOutputLocation(resultConfiguration, id);
         ResultConfiguration resolvedResult = new ResultConfiguration(outputLocation);
 
@@ -93,7 +94,7 @@ public String startQueryExecution(String query,
         vertx.executeBlocking(() -> {
             String setupDdl = buildGlueDdl(database);
             ensureOutputBucket(outputLocation);
-            duckClient.execute(query, setupDdl, outputLocation + "results.csv");
+            duckClient.execute(query, setupDdl, outputLocation);
             return null;
         }).onSuccess(v -> {
             execution.getStatus().setState(QueryExecutionState.SUCCEEDED);
@@ -279,7 +280,7 @@ private String resolveOutputLocation(ResultConfiguration rc, String queryId) {
         String base = (rc != null && rc.getOutputLocation() != null && !rc.getOutputLocation().isBlank())
                 ? rc.getOutputLocation()
                 : "s3://" + DEFAULT_OUTPUT_BUCKET + "/results/";
-        return base.endsWith("/") ? base + queryId + "/" : base + "/" + queryId + "/";
+        return base.endsWith("/") ? base + queryId + ".csv" : base + "/" + queryId + ".csv";
     }
 
     private WorkGroupConfiguration normalizeWorkGroupConfiguration(CreateWorkGroupConfigurationRequest configuration) {


Test patch:
diff --git a/src/test/java/io/github/hectorvent/floci/services/athena/AthenaIntegrationTest.java b/src/test/java/io/github/hectorvent/floci/services/athena/AthenaIntegrationTest.java
index 926143c0ea..a83ea6bbf1 100644
--- a/src/test/java/io/github/hectorvent/floci/services/athena/AthenaIntegrationTest.java
+++ b/src/test/java/io/github/hectorvent/floci/services/athena/AthenaIntegrationTest.java
@@ -377,4 +377,35 @@ void getTableMetadataTimestampsAreSerializedAsEpochSecondsNumbers() {
                 .body("TableMetadata.CreateTime", instanceOf(Number.class))
                 .body("TableMetadata.LastAccessTime", instanceOf(Number.class));
     }
+
+    @Test
+    @Order(13)
+    void outputLocationIsTheResultCsvObjectKey() {
+        String id = given()
+            .header("X-Amz-Target", "AmazonAthena.StartQueryExecution")
+            .contentType(CONTENT_TYPE)
+            .body("""
+                {
+                  "QueryString": "SELECT 1",
+                  "ResultConfiguration": { "OutputLocation": "s3://athena-location-test/out/" }
+                }
+                """)
+        .when()
+            .post("/")
+        .then()
+            .statusCode(200)
+            .extract().path("QueryExecutionId");
+
+        // AWS reports the result CSV object itself, not a directory prefix
+        given()
+            .header("X-Amz-Target", "AmazonAthena.GetQueryExecution")
+            .contentType(CONTENT_TYPE)
+            .body("{ \"QueryExecutionId\": \"" + id + "\" }")
+        .when()
+            .post("/")
+        .then()
+            .statusCode(200)
+            .body("QueryExecution.ResultConfiguration.OutputLocation",
+                    equalTo("s3://athena-location-test/out/" + id + ".csv"));
+    }
 }