From 8ee5d59fcb3511755cdfc2d3f21909f6c544607b Mon Sep 17 00:00:00 2001 From: Alexander Taepper Date: Wed, 21 Oct 2026 09:30:19 +0200 Subject: [PATCH] fix(lapis): render empty Or and And as valid SaneQL A filter by an empty list, e.g. `{"accession": []}`, maps to an Or without children, which rendered as `filter(())` and failed with a parse error. Render an empty Or as `false` and an empty And as `true`. --- .../org/genspectrum/lapis/rhydb/SaneQlAst.kt | 16 ++++++++++++---- .../lapis/rhydb/RhyDbQueryToSaneQlTest.kt | 10 ++++++++++ .../org/genspectrum/lapis/rhydb/SaneQlAstTest.kt | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/lapis/src/main/kotlin/org/genspectrum/lapis/rhydb/SaneQlAst.kt b/lapis/src/main/kotlin/org/genspectrum/lapis/rhydb/SaneQlAst.kt index 8a4f71820..01ef199c0 100644 --- a/lapis/src/main/kotlin/org/genspectrum/lapis/rhydb/SaneQlAst.kt +++ b/lapis/src/main/kotlin/org/genspectrum/lapis/rhydb/SaneQlAst.kt @@ -94,21 +94,29 @@ data class SaneQlNot( } /** - * Variadic `&&` conjunction. + * Variadic `&&` conjunction. Without children, it is `true`. */ data class SaneQlAnd( val children: List, ) : SaneQlExpression { - override fun render() = children.joinToString(" && ") { wrapIfNeeded(it) } + override fun render() = + when { + children.isEmpty() -> "true" + else -> children.joinToString(" && ") { wrapIfNeeded(it) } + } } /** - * Variadic `||` disjunction. + * Variadic `||` disjunction. Without children (e.g. for a filter by an empty list), it is `false`. */ data class SaneQlOr( val children: List, ) : SaneQlExpression { - override fun render() = children.joinToString(" || ") { wrapIfNeeded(it) } + override fun render() = + when { + children.isEmpty() -> "false" + else -> children.joinToString(" || ") { wrapIfNeeded(it) } + } } private fun wrapIfNeeded(expression: SaneQlExpression): String { diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/rhydb/RhyDbQueryToSaneQlTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/rhydb/RhyDbQueryToSaneQlTest.kt index c4296215b..40342b2a5 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/rhydb/RhyDbQueryToSaneQlTest.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/rhydb/RhyDbQueryToSaneQlTest.kt @@ -455,6 +455,16 @@ class RhyDbQueryToSaneQlTest { Or(StringEquals("theColumn", "theValue"), StringEquals("theOtherColumn", "theOtherValue")), """"theColumn" = 'theValue' || "theOtherColumn" = 'theOtherValue'""", ), + // Empty Or, e.g. from a filter by an empty list + Arguments.of( + Or(emptyList()), + "false", + ), + // Empty And + Arguments.of( + And(emptyList()), + "true", + ), // Not Arguments.of( Not(StringEquals("theColumn", "theValue")), diff --git a/lapis/src/test/kotlin/org/genspectrum/lapis/rhydb/SaneQlAstTest.kt b/lapis/src/test/kotlin/org/genspectrum/lapis/rhydb/SaneQlAstTest.kt index 8964e188e..9af756f00 100644 --- a/lapis/src/test/kotlin/org/genspectrum/lapis/rhydb/SaneQlAstTest.kt +++ b/lapis/src/test/kotlin/org/genspectrum/lapis/rhydb/SaneQlAstTest.kt @@ -109,6 +109,22 @@ class SaneQlAstTest { assertThat(or.render(), equalTo("(true && false) || true")) } + @Test + fun `GIVEN empty or THEN renders false`() { + assertThat(SaneQlOr(emptyList()).render(), equalTo("false")) + } + + @Test + fun `GIVEN empty and THEN renders true`() { + assertThat(SaneQlAnd(emptyList()).render(), equalTo("true")) + } + + @Test + fun `GIVEN and with empty or child THEN renders valid SaneQL`() { + val and = SaneQlAnd(listOf(SaneQlOr(emptyList()), SaneQlBoolean(true))) + assertThat(and.render(), equalTo("(false) && true")) + } + @Test fun `GIVEN and with flat non-and-or children THEN does not add parentheses`() { val and = SaneQlAnd(listOf(SaneQlBoolean(true), SaneQlBoolean(false)))