diff --git a/lapis2/src/main/antlr/org/genspectrum/lapis/model/variantqueryparser/VariantQuery.g4 b/lapis2/src/main/antlr/org/genspectrum/lapis/model/variantqueryparser/VariantQuery.g4 index 16f67b10..54e94e69 100644 --- a/lapis2/src/main/antlr/org/genspectrum/lapis/model/variantqueryparser/VariantQuery.g4 +++ b/lapis2/src/main/antlr/org/genspectrum/lapis/model/variantqueryparser/VariantQuery.g4 @@ -15,6 +15,7 @@ expr: single: nucleotide_mutation | pangolineage_query + | n_of_query ; nucleotide_mutation : nucleotide_symbol? position ambigous_nucleotide_symbol?; @@ -28,6 +29,10 @@ pangolineage_character: A | B | C | D | E | F | G | H | I | J | K | L | M | N | pangolineage_number_component: '.' NUMBER NUMBER? NUMBER?; pangolineage_include_sublineages: DOT? ASTERISK; +n_of_query: '[' n_of_match_exactly? n_of_number_of_matchers '-of:' n_of_exprs ']'; +n_of_match_exactly: 'EXACTLY-'; +n_of_number_of_matchers: NUMBER+; +n_of_exprs: expr (',' expr)*; // lexer rules diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/model/VariantQueryCustomListener.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/model/VariantQueryCustomListener.kt index a2ef8353..6e62a935 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/model/VariantQueryCustomListener.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/model/VariantQueryCustomListener.kt @@ -3,6 +3,7 @@ package org.genspectrum.lapis.model import VariantQueryBaseListener import VariantQueryParser.AndContext import VariantQueryParser.MaybeContext +import VariantQueryParser.N_of_queryContext import VariantQueryParser.NotContext import VariantQueryParser.Nucleotide_mutationContext import VariantQueryParser.OrContext @@ -10,6 +11,7 @@ import VariantQueryParser.Pangolineage_queryContext import org.antlr.v4.runtime.tree.ParseTreeListener import org.genspectrum.lapis.silo.And import org.genspectrum.lapis.silo.Maybe +import org.genspectrum.lapis.silo.NOf import org.genspectrum.lapis.silo.Not import org.genspectrum.lapis.silo.NucleotideSymbolEquals import org.genspectrum.lapis.silo.Or @@ -64,4 +66,20 @@ class VariantQueryCustomListener : VariantQueryBaseListener(), ParseTreeListener val child = expressionStack.removeLast() expressionStack.addLast(Maybe(child)) } + + override fun exitN_of_query(ctx: N_of_queryContext?) { + if (ctx == null) { + return + } + + val n = ctx.n_of_number_of_matchers().text.toInt() + val matchExactly = ctx.n_of_match_exactly() != null + + val children = mutableListOf() + for (i in 1..n) { + children += expressionStack.removeLast() + } + + expressionStack.addLast(NOf(n, matchExactly, children.reversed())) + } } diff --git a/lapis2/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt b/lapis2/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt index 3400f5a7..5a5f9a2f 100644 --- a/lapis2/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt +++ b/lapis2/src/main/kotlin/org/genspectrum/lapis/silo/SiloQuery.kt @@ -46,3 +46,6 @@ data class Or(val children: List) : SiloFilterExpression(" data class Not(val child: SiloFilterExpression) : SiloFilterExpression("Not") data class Maybe(val child: SiloFilterExpression) : SiloFilterExpression("Maybe") + +data class NOf(val numberOfMatchers: Int, val matchExactly: Boolean, val children: List) : + SiloFilterExpression("NOf") diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/model/VariantQueryFacadeTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/model/VariantQueryFacadeTest.kt index a971c14a..877dc617 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/model/VariantQueryFacadeTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/model/VariantQueryFacadeTest.kt @@ -2,6 +2,7 @@ package org.genspectrum.lapis.model import org.genspectrum.lapis.silo.And import org.genspectrum.lapis.silo.Maybe +import org.genspectrum.lapis.silo.NOf import org.genspectrum.lapis.silo.Not import org.genspectrum.lapis.silo.NucleotideSymbolEquals import org.genspectrum.lapis.silo.Or @@ -10,6 +11,7 @@ import org.hamcrest.MatcherAssert import org.hamcrest.Matchers import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows class VariantQueryFacadeTest { private lateinit var underTest: VariantQueryFacade @@ -20,7 +22,64 @@ class VariantQueryFacadeTest { } @Test - fun `given a variant query with a single entry then map should return the corresponding SiloQuery`() { + fun `given a complex variant query then map should return the corresponding SiloQuery`() { + val variantQuery = "300G & (400- | 500B) & !600 & MAYBE(700B | 800-) & [3-of: 123A, 234T, 345G] & A.1.2.3*" + + val result = underTest.map(variantQuery) + + val expectedResult = + And( + listOf( + And( + listOf( + And( + listOf( + And( + listOf( + And( + listOf( + NucleotideSymbolEquals(300, "G"), + Or( + listOf( + NucleotideSymbolEquals(400, "-"), + NucleotideSymbolEquals(500, "B"), + ), + ), + ), + ), + Not(NucleotideSymbolEquals(600, "-")), + ), + ), + Maybe( + Or( + listOf( + NucleotideSymbolEquals(700, "B"), + NucleotideSymbolEquals(800, "-"), + ), + ), + ), + ), + ), + NOf( + 3, + matchExactly = false, + listOf( + NucleotideSymbolEquals(123, "A"), + NucleotideSymbolEquals(234, "T"), + NucleotideSymbolEquals(345, "G"), + ), + ), + ), + ), + PangoLineageEquals("pangoLineage", "A.1.2.3", true), + ), + ) + + MatcherAssert.assertThat(result, Matchers.equalTo(expectedResult)) + } + + @Test + fun `given a variantQuery with a single entry then map should return the corresponding SiloQuery`() { val variantQuery = "300G" val result = underTest.map(variantQuery) @@ -30,7 +89,7 @@ class VariantQueryFacadeTest { } @Test - fun `given a variant variantQuery with an 'And' expression the map should return the corresponding SiloQuery`() { + fun `given a variantQuery with an 'And' expression then map should return the corresponding SiloQuery`() { val variantQuery = "300G & 400" val result = underTest.map(variantQuery) @@ -45,7 +104,7 @@ class VariantQueryFacadeTest { } @Test - fun `given a variant variantQuery with two 'And' expression the map should return the corresponding SiloQuery`() { + fun `given a variantQuery with two 'And' expression then map should return the corresponding SiloQuery`() { val variantQuery = "300G & 400- & 500B" val result = underTest.map(variantQuery) @@ -65,7 +124,7 @@ class VariantQueryFacadeTest { } @Test - fun `given a variant variantQuery with a 'Not' expression the map should return the corresponding SiloQuery`() { + fun `given a variantQuery with a 'Not' expression then map should return the corresponding SiloQuery`() { val variantQuery = "!300G" val result = underTest.map(variantQuery) @@ -75,7 +134,7 @@ class VariantQueryFacadeTest { } @Test - fun `given a variant variantQuery with an 'Or' expression the map should return the corresponding SiloQuery`() { + fun `given a variant variantQuery with an 'Or' expression then map should return the corresponding SiloQuery`() { val variantQuery = "300G | 400" val result = underTest.map(variantQuery) @@ -90,7 +149,7 @@ class VariantQueryFacadeTest { } @Test - fun `given a variant variantQuery with an bracket expression the map should return the corresponding SiloQuery`() { + fun `given a variant variantQuery with an bracket expression then map should return the corresponding SiloQuery`() { val variantQuery = "300C & (400A | 500G)" val result = underTest.map(variantQuery) @@ -110,7 +169,7 @@ class VariantQueryFacadeTest { } @Test - fun `given a variant variantQuery with a 'Maybe' expression the map should return the corresponding SiloQuery`() { + fun `given a variantQuery with a 'Maybe' expression then map should return the corresponding SiloQuery`() { val variantQuery = "MAYBE(300G)" val result = underTest.map(variantQuery) @@ -120,22 +179,130 @@ class VariantQueryFacadeTest { } @Test - fun `given a variant variantQuery with a 'Pangolineage' expression the map should return the corresponding SiloQuery`() { + fun `given a variantQuery with a 'Pangolineage' expression then map should return the corresponding SiloQuery`() { val variantQuery = "A.1.2.3" val result = underTest.map(variantQuery) - val expectedResult = PangoLineageEquals("A.1.2.3", false) + val expectedResult = PangoLineageEquals("pangoLineage", "A.1.2.3", false) MatcherAssert.assertThat(result, Matchers.equalTo(expectedResult)) } @Test - fun `given a variant variantQuery with a 'Pangolineage' expression including sublineages the map should return the corresponding SiloQuery`() { + fun `given a variantQuery with a 'Pangolineage' expression (including sublineages) then map should return the corresponding SiloQuery`() { // ktlint-disable max-line-length val variantQuery = "A.1.2.3*" val result = underTest.map(variantQuery) - val expectedResult = PangoLineageEquals("A.1.2.3", true) + val expectedResult = PangoLineageEquals("pangoLineage", "A.1.2.3", true) MatcherAssert.assertThat(result, Matchers.equalTo(expectedResult)) } + + @Test + fun `given a variantQuery with a 'Nof' expression then map should return the corresponding SiloQuery`() { + val variantQuery = "[3-of: 123A, 234T, 345G]" + + val result = underTest.map(variantQuery) + + val expectedResult = NOf( + 3, + false, + listOf( + NucleotideSymbolEquals(123, "A"), + NucleotideSymbolEquals(234, "T"), + NucleotideSymbolEquals(345, "G"), + ), + ) + MatcherAssert.assertThat(result, Matchers.equalTo(expectedResult)) + } + + @Test + fun `given a variantQuery with a exact 'Nof' expression then map should return the corresponding SiloQuery`() { + val variantQuery = "[exactly-3-of: 123A, 234T, 345G]" + + val result = underTest.map(variantQuery) + + val expectedResult = NOf( + 3, + true, + listOf( + NucleotideSymbolEquals(123, "A"), + NucleotideSymbolEquals(234, "T"), + NucleotideSymbolEquals(345, "G"), + ), + ) + MatcherAssert.assertThat(result, Matchers.equalTo(expectedResult)) + } + + @Test + fun `given a variantQuery with a 'Insertion' expression then map should throw an error`() { + val variantQuery = "ins_1234:GAG" + + val exception = assertThrows { underTest.map(variantQuery) } + + MatcherAssert.assertThat( + exception.message, + Matchers.equalTo("Nucleotide insertions are not supported yet."), + ) + } + + @Test + fun `given a variant variantQuery with a 'AA mutation' expression then map should throw an error`() { + val variantQuery = "S:N501Y" + + val exception = assertThrows { underTest.map(variantQuery) } + + MatcherAssert.assertThat( + exception.message, + Matchers.equalTo("Amino acid mutations are not supported yet."), + ) + } + + @Test + fun `given a valid variantQuery with a 'AA insertion' expression then map should throw an error`() { + val variantQuery = "ins_S:501:EPE" + + val exception = assertThrows { underTest.map(variantQuery) } + + MatcherAssert.assertThat( + exception.message, + Matchers.equalTo("Amino acid insertions are not supported yet."), + ) + } + + @Test + fun `given a valid variantQuery with a 'nextclade pango lineage' expression then map should throw an error`() { + val variantQuery = "nextcladePangoLineage:BA.5*" + + val exception = assertThrows { underTest.map(variantQuery) } + + MatcherAssert.assertThat( + exception.message, + Matchers.equalTo("Nextclade pango lineages are not supported yet."), + ) + } + + @Test + fun `given a valid variantQuery with a 'Nextstrain clade lineage' expression then map should throw an error`() { + val variantQuery = "nextstrainClade:22B" + + val exception = assertThrows { underTest.map(variantQuery) } + + MatcherAssert.assertThat( + exception.message, + Matchers.equalTo("Nextstrain clade lineages are not supported yet."), + ) + } + + @Test + fun `given a valid variantQuery with a 'Gisaid clade lineage' expression then map should throw an error`() { + val variantQuery = "gisaid:AB" + + val exception = assertThrows { underTest.map(variantQuery) } + + MatcherAssert.assertThat( + exception.message, + Matchers.equalTo("Gisaid clade lineages are not supported yet."), + ) + } } diff --git a/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt b/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt index 9ff42bc7..103405d3 100644 --- a/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt +++ b/lapis2/src/test/kotlin/org/genspectrum/lapis/silo/SiloQueryTest.kt @@ -234,6 +234,35 @@ class SiloQueryTest { } """, ), + Arguments.of( + NOf( + 2, + true, + listOf( + StringEquals("theColumn", "theValue"), + StringEquals("theOtherColumn", "theOtherValue"), + ), + ), + """ + { + "type": "NOf", + "numberOfMatchers": 2, + "matchExactly": true, + "children": [ + { + "type": "StringEquals", + "column": "theColumn", + "value": "theValue" + }, + { + "type": "StringEquals", + "column": "theOtherColumn", + "value": "theOtherValue" + } + ] + } + """, + ), ) } }