Correcting serialization of "Der:sa /+w1 Betrieb" ("Der:sa" already
correct).
Bug 1 corrected: same ClassId for different spans.
Bug 2 corrected: object stack worked wrongly for prox and postion
conditions, see query above.
JUnits adapted, as classIds have modified values now.
1 JUnit added to check the correctness of the query above.
Changes updated.
Change-Id: Ife93b8992adc6d5c167a2644f12864b706d43ff1
Reviewed-on: https://korap.ids-mannheim.de/gerrit/c/KorAP/Koral/+/11138
Reviewed-by: <bodmer@ids-mannheim.de>
diff --git a/.gitignore b/.gitignore
index dc2f525..894c6d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,8 @@
.*
!.gitignore
!.github
+*.txt
+*.odt
\#*\#
# /src/main/antlr/cosmas/
diff --git a/Changes b/Changes
index a083c3b..eff1967 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,10 @@
+0.52 2026-07-29
+ - [bugfix] C2QL: Correcting serialization of "Der:sa" combined with e.g. "/+w1 Betrieb".
+ - [bugfix] in case above: corrected: same ClassId for different spans.
+ - [bugfix] in case above: corrected: object stack worked wrongly for prox and position together
+ - [bugfix] JUnits adapted, as classIds have modified values now.
+ - [bugfix] JUnit added to check the correctness of the query above .
+
0.51 2026-07-29
- [feature] C2QL MORPH(v1 v2..): more than 1 value with/without foundry/layer specification works now.
- [feature] translating MORPH expressions from C2-style STTS/CONNEXOR to STTS/marmot.
diff --git a/src/main/java/de/ids_mannheim/korap/query/serialize/Cosmas2QueryProcessor.java b/src/main/java/de/ids_mannheim/korap/query/serialize/Cosmas2QueryProcessor.java
index 5d2c744..a8aca8c 100644
--- a/src/main/java/de/ids_mannheim/korap/query/serialize/Cosmas2QueryProcessor.java
+++ b/src/main/java/de/ids_mannheim/korap/query/serialize/Cosmas2QueryProcessor.java
@@ -47,7 +47,10 @@
* @author Nils Diewald (diewald@ids-mannheim.de)
* @author Eliza Margaretha (margaretha@ids-mannheim.de)
* @author Franck Bodmer (bodmer@ids-mannheim.de)
- * @version 0.4 -- 09.06.26/FB
+ * @version 0.4 -- 07.07.26/FB
+ * - Korrektur: Doppelte Vergabe einer KlassenID.
+ * - Korrektur: fehlerhafte Serialisierung von "Der:sa /+w1 Betrieb" (derweil "Der:sa" korrekt behandelt wurde)
+ * infolge falscher Verwaltung des objectStacks.
*/
public class Cosmas2QueryProcessor extends Antlr3AbstractQueryProcessor {
@@ -306,7 +309,10 @@
private void processNode (Tree node) {
- // Top-down processing
+
+ final String func = "Debug: processNode";
+
+ // Top-down processing
if (visited.contains(node))
return;
else
@@ -522,8 +528,10 @@
// See C-II QL documentation for more detail:
// http://www.ids-mannheim.de/cosmas2/win-app/hilfe/suchanfrage/eingabe-grafisch/syntax/textpositionen.html
+ final String func = "Debug: processOPBED";
+
if( DEBUG )
- System.out.printf("Debug: processOPBED: '%s'.\n", node.toStringTree());
+ System.out.printf("%s: '%s'.\n", func, node.toStringTree());
// Step I: create group
int optsChild = node.getChildCount() - 1;
@@ -534,6 +542,7 @@
Map<String, Object> submatchgroup =
KoralObjectGenerator.makeReference(classCounter + 128);
+
ArrayList<Object> submatchOperands = new ArrayList<Object>();
submatchgroup.put("operands", submatchOperands);
putIntoSuperObject(submatchgroup);
@@ -947,12 +956,12 @@
if (!(openNodeCats.get(1).equals("OPBEG")
|| openNodeCats.get(1).equals("OPEND") || inOPALL
|| openNodeCats.get(1).equals("OPNHIT"))) {
- wrapOperandInClass(node, 1, 128 + classCounter);
- wrapOperandInClass(node, 2, 128 + classCounter);
+ wrapOperandInClass(node, 1, 128 + classCounter++); // 07.07.26/FB
+ wrapOperandInClass(node, 2, 128 + classCounter++); // 07.07.26/FB
// Deactivated, uncomment to wrap sequence in reference.
// group = KoralObjectGenerator.wrapInReference(group,
// classCounter++);
- classCounter++;
+ // classCounter++; 07.07.26/FB
}
else if (openNodeCats.get(1).equals("OPNHIT")) {
Map<String, Object> repetition =
@@ -1850,6 +1859,8 @@
private void processOPWF_OPLEM (Tree node)
{
+ final String func = "Debug: processOPWF_OPLEM";
+
String nodeCat = getNodeCat(node);
// Step I: get info
Map<String, Object> token = KoralObjectGenerator.makeToken();
@@ -1914,6 +1925,7 @@
visited.add(node.getChild(0));
}
else {
+
// TODO
if( DEBUG )
System.out.printf("Error: processOPWF_OPLEM: TPOS not implemented: '%s'!\n", node.toStringTree());
@@ -2063,6 +2075,7 @@
positionGroup.put("operands", posOperands);
Map<String, Object> span = KoralObjectGenerator.makeSpan(elem);
objectStack.push(classGroup);
+ stackedObjects++; // 07.07.26/FB
if (hitSpanRef != null) {
Map<String, Object> spanRefAroundHit = KoralObjectGenerator
.makeSpanReference(hitSpanRef, KoralOperation.FOCUS);
diff --git a/src/test/java/de/ids_mannheim/korap/test/cosmas2/Cosmas2QueryProcessorTest.java b/src/test/java/de/ids_mannheim/korap/test/cosmas2/Cosmas2QueryProcessorTest.java
index 1ee0aa2..0ac1341 100644
--- a/src/test/java/de/ids_mannheim/korap/test/cosmas2/Cosmas2QueryProcessorTest.java
+++ b/src/test/java/de/ids_mannheim/korap/test/cosmas2/Cosmas2QueryProcessorTest.java
@@ -1048,7 +1048,7 @@
assertEquals("operation:class", res.at("/query/operands/0/operation")
.asText());
assertEquals(129, res.at("/query/operands/0/classOut").asInt());
- assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+ assertEquals(130, res.at("/query/operands/1/classOut").asInt()); // due to correction 07.07.26/FB
assertEquals("koral:token", res
.at("/query/operands/0/operands/0/@type").asText());
assertEquals("Sonne", res.at("/query/operands/0/operands/0/wrap/key")
@@ -1076,7 +1076,7 @@
assertEquals("operation:class", res.at("/query/operands/0/operation")
.asText());
assertEquals(129, res.at("/query/operands/0/classOut").asInt());
- assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+ assertEquals(130, res.at("/query/operands/1/classOut").asInt());
assertEquals("koral:token", res
.at("/query/operands/0/operands/0/@type").asText());
assertEquals("Sonne", res.at("/query/operands/0/operands/0/wrap/key")
@@ -1105,7 +1105,7 @@
assertEquals("operation:class", res.at("/query/operands/0/operation")
.asText());
assertEquals(129, res.at("/query/operands/0/classOut").asInt());
- assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+ assertEquals(130, res.at("/query/operands/1/classOut").asInt()); // due to correction 07.07.26/FB
assertEquals("koral:token", res
.at("/query/operands/0/operands/0/@type").asText());
assertEquals("Sonne", res.at("/query/operands/0/operands/0/wrap/key")
@@ -1253,7 +1253,7 @@
assertEquals(129, res.at("/query/operands/0/classOut").asInt());
assertEquals("Sonne", res.at("/query/operands/0/operands/0/wrap/key")
.asText());
- assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+ assertEquals(130, res.at("/query/operands/1/classOut").asInt());
assertEquals("operation:sequence",
res.at("/query/operands/1/operands/0/operation").asText());
assertEquals("w", res
@@ -1264,14 +1264,14 @@
assertEquals(7,
res.at("/query/operands/1/operands/0/distances/0/boundary/max")
.asInt());
- assertEquals(130,
+ assertEquals(131, // due to correction 07.07.26/FB
res.at("/query/operands/1/operands/0/operands/0/classOut")
.asInt());
assertEquals(
"Mond",
res.at("/query/operands/1/operands/0/operands/0/operands/0/wrap/key")
.asText());
- assertEquals(130,
+ assertEquals(132, // due to correction 07.07.26/FB
res.at("/query/operands/1/operands/0/operands/1/classOut")
.asInt());
assertEquals(
@@ -1310,7 +1310,7 @@
assertEquals(129, res.at("/query/operands/1/classOut").asInt());
assertEquals("Sonne", res.at("/query/operands/1/operands/0/wrap/key")
.asText());
- assertEquals(129, res.at("/query/operands/0/classOut").asInt());
+ assertEquals(130, res.at("/query/operands/0/classOut").asInt()); // due to correction 07.07.26/FB
assertEquals("operation:sequence",
res.at("/query/operands/0/operands/0/operation").asText());
assertEquals("w", res
@@ -1321,14 +1321,14 @@
assertEquals(2,
res.at("/query/operands/0/operands/0/distances/0/boundary/max")
.asInt());
- assertEquals(130,
+ assertEquals(131, // due to correction 07.07.26/FB
res.at("/query/operands/0/operands/0/operands/0/classOut")
.asInt());
assertEquals(
"Mond",
res.at("/query/operands/0/operands/0/operands/0/operands/0/wrap/key")
.asText());
- assertEquals(130,
+ assertEquals(132, // due to correction 07.07.26/FB
res.at("/query/operands/0/operands/0/operands/1/classOut")
.asInt());
assertEquals(
@@ -1427,7 +1427,7 @@
.asText());
assertEquals("operation:class", res.at("/query/operands/1/operation")
.asText());
- assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+ assertEquals(130, res.at("/query/operands/1/classOut").asInt()); // due to correction 07.07.26/FB
assertEquals("koral:token", res
.at("/query/operands/1/operands/0/@type").asText());
assertEquals("kommt", res.at("/query/operands/1/operands/0/wrap/key")
@@ -1447,7 +1447,7 @@
assertEquals("koral:group", res.at("/query/operands/1/@type").asText());
assertEquals("operation:class", res.at("/query/operands/1/operation")
.asText());
- assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+ assertEquals(130, res.at("/query/operands/1/classOut").asInt()); // due to correction 07.07.26/FB
assertEquals("koral:reference",
res.at("/query/operands/1/operands/0/@type").asText());
assertEquals("operation:focus",
@@ -2160,7 +2160,7 @@
res.at("/query/operands/0/operands/0/operands/1/operands/0/operands/0/operands/0/wrap/key")
.asText());
assertEquals(
- 132,
+ 133, // due to correction 07.07.26/FB
res.at("/query/operands/0/operands/0/operands/1/operands/0/operands/1/classOut")
.asInt());
assertEquals(
@@ -2455,7 +2455,13 @@
"Mann",
res.at("/query/operands/0/operands/1/operands/1/operands/0/operands/1/wrap/key")
.asText());
-
+
+ // check the combination of Positional condition and proximity operator,
+ // this was corrected by 07.07.26/FB
+
+ query = "Der:sa /+w1 Betrieb";
+ qs.setQuery(query, "cosmas2");
+ res = mapper.readTree(qs.toJSON());
}
@@ -2718,6 +2724,103 @@
*/
}
+
+ /**
+ * Regression test for a bug where a sequence combining a
+ * position operator (OPBED, e.g. "sa" = starts sentence) with
+ * a distance operator (OPPROX, e.g. "/+w1") produced an invalid
+ * KoralQuery: the second sequence operand ended up nested
+ * *inside* the koral:group/operation:class wrapping the first
+ * operand, instead of being its sibling in the sequence's
+ * operands array. This violated the requirement that
+ * operation:class expects exactly one operand, and caused
+ * KorAP/Krill to reject the query.
+ *
+ * Root cause: processPositionCondition() pushed a class group
+ * onto the internal objectStack (to collect the wrapped token)
+ * but never recorded this push via stackedObjects++, so the
+ * generic per-node stack bookkeeping in processNode() failed to
+ * pop it again. This left one stale object on the stack, which
+ * shifted where the second sequence operand got inserted.
+ */
+ @Test
+ public void testOPBEDinOPPROXSequence () throws JsonProcessingException,
+ IOException {
+ query = "Der:sa /+w1 Betrieb";
+ qs.setQuery(query, "cosmas2");
+ res = mapper.readTree(qs.toJSON());
+
+ // Top level: a 2-operand sequence with the distance constraint
+ assertEquals("koral:group", res.at("/query/@type").asText());
+ assertEquals("operation:sequence", res.at("/query/operation")
+ .asText());
+ assertTrue(res.at("/query/inOrder").asBoolean());
+ assertEquals("cosmas:distance", res.at("/query/distances/0/@type")
+ .asText());
+ assertEquals("w", res.at("/query/distances/0/key").asText());
+ assertEquals(0, res.at("/query/distances/0/boundary/min").asInt());
+ assertEquals(1, res.at("/query/distances/0/boundary/max").asInt());
+ assertEquals(2, res.at("/query/operands").size());
+
+ // First operand: "Der" at sentence start, wrapped in its own class
+ assertEquals("koral:group", res.at("/query/operands/0/@type")
+ .asText());
+ assertEquals("operation:class",
+ res.at("/query/operands/0/operation").asText());
+ int firstClass = res.at("/query/operands/0/classOut").asInt();
+
+ assertEquals("koral:reference",
+ res.at("/query/operands/0/operands/0/@type").asText());
+ assertEquals("operation:focus",
+ res.at("/query/operands/0/operands/0/operation").asText());
+ int innerClass = res
+ .at("/query/operands/0/operands/0/classRef/0").asInt();
+
+ assertEquals(
+ "koral:group",
+ res.at("/query/operands/0/operands/0/operands/0/@type")
+ .asText());
+ assertEquals(
+ "operation:position",
+ res.at("/query/operands/0/operands/0/operands/0/operation")
+ .asText());
+ assertEquals(
+ "frames:startsWith",
+ res.at("/query/operands/0/operands/0/operands/0/frames/0")
+ .asText());
+ assertEquals(
+ "s",
+ res.at("/query/operands/0/operands/0/operands/0/operands/0/wrap/key")
+ .asText());
+ assertEquals(
+ innerClass,
+ res.at("/query/operands/0/operands/0/operands/0/operands/1/classOut")
+ .asInt());
+ assertEquals(
+ "Der",
+ res.at("/query/operands/0/operands/0/operands/0/operands/1/operands/0/wrap/key")
+ .asText());
+
+ // Second operand: "Betrieb" - a direct sibling of the first
+ // operand in the sequence, NOT nested inside it.
+ assertEquals("koral:group", res.at("/query/operands/1/@type")
+ .asText());
+ assertEquals("operation:class",
+ res.at("/query/operands/1/operation").asText());
+ assertEquals("koral:token",
+ res.at("/query/operands/1/operands/0/@type").asText());
+ assertEquals("Betrieb",
+ res.at("/query/operands/1/operands/0/wrap/key").asText());
+
+ // The sequence itself must not have picked up a spurious
+ // third, deeply-nested operand.
+ assertTrue(res.at("/query/operands/0/operands/1").isMissingNode());
+
+ // Class numbers used within a query must be distinct.
+ assertNotEquals(firstClass, innerClass);
+ }
+
+
@Test
public void testWildcard () throws JsonProcessingException, IOException {
query = "meine* /+w1:2,s0 &Erfahrung";
diff --git a/src/test/java/de/ids_mannheim/korap/test/cosmas2/OPOVTest.java b/src/test/java/de/ids_mannheim/korap/test/cosmas2/OPOVTest.java
index b7a1c5d..636b91d 100644
--- a/src/test/java/de/ids_mannheim/korap/test/cosmas2/OPOVTest.java
+++ b/src/test/java/de/ids_mannheim/korap/test/cosmas2/OPOVTest.java
@@ -17,6 +17,8 @@
import de.ids_mannheim.korap.query.object.KoralOperation;
import de.ids_mannheim.korap.query.serialize.QuerySerializer;
+// Anpassung der inkorrekten JUnits nach der Korrektur vom 07.07.26/FB an Cosmas2QueryProcessor.java.
+
public class OPOVTest {
String query;
@@ -237,6 +239,6 @@
res.at("/operation").asText());
// class
assertEquals(130, res.at("/operands/0/classOut").asInt());
- assertEquals(130, res.at("/operands/1/classOut").asInt());
+ assertEquals(131, res.at("/operands/1/classOut").asInt()); // due to correction 07.07.26/FB
}
}