Skip to content

Commit 721f900

Browse files
committed
fix: Return 400 for CSV export with array columns
When exporting a ViewDefinition result to CSV via $viewdefinition-export, if the view contains array columns, Spark throws an AnalysisException with UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE. This previously surfaced as a 500 Internal Server Error. Now it returns a 400 Bad Request with a clear error message indicating the unsupported column.
1 parent d8eeb1c commit 721f900

File tree

2 files changed

+59
-2
lines changed

2 files changed

+59
-2
lines changed

server/src/main/java/au/csiro/pathling/operations/view/ViewDefinitionExportExecutor.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.hadoop.conf.Configuration;
4141
import org.apache.hadoop.fs.FileSystem;
4242
import org.apache.hadoop.fs.Path;
43+
import org.apache.spark.sql.AnalysisException;
4344
import org.apache.spark.sql.Column;
4445
import org.apache.spark.sql.Dataset;
4546
import org.apache.spark.sql.Row;
@@ -281,7 +282,12 @@ private List<String> writeNdjson(
281282
FileSystemPersistence.renamePartitionedFiles(sparkSession, outputPath, outputPath, "json"));
282283
}
283284

284-
/** Writes the result as CSV files. */
285+
/**
286+
* Writes the result as CSV files.
287+
*
288+
* @throws InvalidRequestException if the dataset contains data types that are not supported by
289+
* the CSV format
290+
*/
285291
@Nonnull
286292
private List<String> writeCsv(
287293
@Nonnull final Dataset<Row> result,
@@ -290,7 +296,22 @@ private List<String> writeCsv(
290296
final boolean includeHeader,
291297
@Nonnull final Path jobDirPath) {
292298

293-
result.write().mode(SaveMode.Overwrite).option("header", includeHeader).csv(outputPath);
299+
try {
300+
result.write().mode(SaveMode.Overwrite).option("header", includeHeader).csv(outputPath);
301+
} catch (final Exception e) {
302+
// Spark throws AnalysisException when it encounters unsupported data types for a datasource.
303+
// We convert this to an InvalidRequestException to return a 400 status code.
304+
if (e instanceof final AnalysisException ae
305+
&& "UNSUPPORTED_DATA_TYPE_FOR_DATASOURCE".equals(ae.getErrorClass())) {
306+
throw new InvalidRequestException(
307+
"CSV export failed for view '%s': %s".formatted(viewName, e.getMessage()));
308+
}
309+
// Re-throw unexpected exceptions as runtime exceptions.
310+
if (e instanceof RuntimeException re) {
311+
throw re;
312+
}
313+
throw new RuntimeException(e);
314+
}
294315

295316
// Rename partitioned files to follow naming convention.
296317
return new ArrayList<>(

server/src/test/java/au/csiro/pathling/operations/view/ViewDefinitionExportExecutorTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import au.csiro.pathling.test.SpringBootUnitTest;
3030
import au.csiro.pathling.util.CustomObjectDataSource;
3131
import au.csiro.pathling.util.FhirServerTestConfiguration;
32+
import au.csiro.pathling.views.Column;
3233
import au.csiro.pathling.views.FhirView;
3334
import ca.uhn.fhir.context.FhirContext;
3435
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@@ -148,6 +149,41 @@ void csvOutputCreatesFiles() {
148149
assertThat(outputs.get(0).fileUrls().get(0)).contains(".csv");
149150
}
150151

152+
@Test
153+
void csvExportWithArrayColumnThrowsInvalidRequestException() {
154+
// CSV format does not support array columns. When a view contains a collection column, CSV
155+
// export should fail with a 400 error rather than a 500 internal server error.
156+
final Patient patient = createPatient("test-1", "Smith");
157+
patient.addName().addGiven("John").addGiven("James");
158+
executor = createExecutor(patient);
159+
160+
// Create a view that includes a collection column (given names as an array).
161+
final FhirView view =
162+
FhirView.ofResource("Patient")
163+
.select(
164+
FhirView.columns(
165+
FhirView.column("id", "id"),
166+
Column.collection("given_names", "name.first().given")))
167+
.build();
168+
169+
final ViewInput viewInput = new ViewInput("patients", view);
170+
final ViewDefinitionExportRequest request =
171+
new ViewDefinitionExportRequest(
172+
"http://example.org/$viewdefinition-export",
173+
"http://example.org/fhir",
174+
List.of(viewInput),
175+
null,
176+
ViewExportFormat.CSV,
177+
true,
178+
Collections.emptySet(),
179+
null);
180+
181+
assertThatThrownBy(() -> executor.execute(request, UUID.randomUUID().toString()))
182+
.isInstanceOf(InvalidRequestException.class)
183+
.hasMessageContaining("CSV export failed")
184+
.hasMessageContaining("given_names");
185+
}
186+
151187
// -------------------------------------------------------------------------
152188
// Parquet output tests
153189
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)