diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/silo/ArrowRowConverter.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/silo/ArrowRowConverter.kt index 94c068fbd..d68241b56 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/silo/ArrowRowConverter.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/silo/ArrowRowConverter.kt @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.node.NullNode import com.fasterxml.jackson.databind.node.TextNode import org.apache.arrow.vector.BigIntVector import org.apache.arrow.vector.BitVector +import org.apache.arrow.vector.DateDayVector import org.apache.arrow.vector.Float4Vector import org.apache.arrow.vector.Float8Vector import org.apache.arrow.vector.IntVector @@ -188,6 +189,7 @@ private fun VectorSchemaRoot.fieldValueAsJsonNode( is Float8Vector -> DoubleNode(vector.get(rowIndex)) is Float4Vector -> FloatNode(vector.get(rowIndex)) is BitVector -> BooleanNode.valueOf(vector.get(rowIndex) != 0) + is DateDayVector -> TextNode(java.time.LocalDate.ofEpochDay(vector.get(rowIndex).toLong()).toString()) else -> TextNode(vector.getObject(rowIndex)?.toString() ?: "") } } diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/silo/ArrowTestHelper.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/silo/ArrowTestHelper.kt index 09cea8d8e..8434993dc 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/silo/ArrowTestHelper.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/silo/ArrowTestHelper.kt @@ -3,17 +3,20 @@ package org.genspectrum.lapis.silo import org.apache.arrow.memory.RootAllocator import org.apache.arrow.vector.BigIntVector import org.apache.arrow.vector.BitVector +import org.apache.arrow.vector.DateDayVector import org.apache.arrow.vector.Float8Vector import org.apache.arrow.vector.IntVector import org.apache.arrow.vector.VarCharVector import org.apache.arrow.vector.VectorSchemaRoot import org.apache.arrow.vector.ipc.ArrowStreamWriter +import org.apache.arrow.vector.types.DateUnit import org.apache.arrow.vector.types.pojo.ArrowType import org.apache.arrow.vector.types.pojo.Field import org.apache.arrow.vector.types.pojo.FieldType import org.apache.arrow.vector.types.pojo.Schema import java.io.ByteArrayOutputStream import java.nio.channels.Channels +import java.time.LocalDate /** * Builds an Arrow IPC stream byte array from [rows] using the given builder. @@ -51,6 +54,8 @@ fun buildArrowIpcStream(rows: List>): ByteArray { is Boolean -> Field(name, FieldType.nullable(ArrowType.Bool()), null) + is LocalDate -> Field(name, FieldType.nullable(ArrowType.Date(DateUnit.DAY)), null) + // Note: null values (and any unrecognized types) are typed as Utf8. // If the first row has null for a column that later rows fill with a typed value (e.g. Int, Long), // the column will be inferred as Utf8 and the converter will throw a cast error at runtime. @@ -101,6 +106,14 @@ fun buildArrowIpcStream(rows: List>): ByteArray { } } + is DateDayVector -> { + if (value == null) { + vector.setNull(rowIdx) + } else { + vector.set(rowIdx, (value as LocalDate).toEpochDay().toInt()) + } + } + is VarCharVector -> { if (value == null) { vector.setNull(rowIdx) diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt index 0d50475ca..f36c8e9a1 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/silo/SiloClientTest.kt @@ -41,6 +41,7 @@ import org.mockserver.model.MediaType import org.mockserver.model.MediaType.parse import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import java.time.LocalDate private const val MOCK_SERVER_PORT = 1080 @@ -295,6 +296,36 @@ class SiloClientTest( ) } + @Test + fun `GIVEN server returns date32 column THEN dates are converted to ISO-8601 strings and nulls remain null`() { + expectQueryRequestAndRespondWith( + response() + .withContentType(parse(ARROW_STREAM_MEDIA_TYPE)) + .withBody( + buildArrowIpcStream( + listOf( + mapOf("date" to LocalDate.of(2021, 2, 23), "label" to "A"), + mapOf("date" to LocalDate.of(2021, 3, 19), "label" to "B"), + mapOf("date" to null, "label" to "C"), + ), + ), + ), + ) + + val query = SiloQuery(SiloAction.details(), StringEquals("theColumn", "theValue")) + val result = sendQuery(query) + + assertThat(result, hasSize(3)) + assertThat( + result, + containsInAnyOrder( + DetailsData(mapOf("date" to TextNode("2021-02-23"), "label" to TextNode("A"))), + DetailsData(mapOf("date" to TextNode("2021-03-19"), "label" to TextNode("B"))), + DetailsData(mapOf("date" to NullNode.instance, "label" to TextNode("C"))), + ), + ) + } + @Test fun `GIVEN server returns most recent common ancestor response THEN response can be deserialized`() { expectQueryRequestAndRespondWith(