fix(vectordata): render a CQL conjunction without grouping parentheses

A CQL WHERE clause is a bare list of relations and Cassandra rejects a
conjunction wrapped in parentheses, which is what the reference's CQL
vernacular emits. Render the bare form for CQL, keep the grouping for
SQL and for the != and OR rewrites, and record the deviation in the
contract for upstream reconciliation.
This commit is contained in:
Jonathan Shook committed 2026-09-05 00:02:51 +00:00
1 parent 0a6c01031c
commit d461f5831b
3 files changed
+32 -3

No files matched your search

+7
View File
@@ -274,6 +274,13 @@ Remaining representation differences:
them as its JSON library's values; here `Form.extra()` holds the
`MValue`s the JSON vernacular parses them to, typed as that parse
types them.
- **A CQL conjunction renders without parentheses.** The reference's
CQL vernacular wraps `AND`-joined relations in parentheses, as its SQL
vernacular does; a CQL `WHERE` clause has no grouping parentheses and
Cassandra rejects the wrapped form. Here the CQL rendering of a
conjunction is the bare `a AND b`, and the SQL rendering, the `!=`
rewrite, and an `OR` keep the reference's parentheses. To be
reconciled upstream.
- **The raw predicate scanner is not ported.** The reference's
`mnode::scan` also compiles predicates to positions and evaluates them
against raw bytes for its predicate-index pipeline; that is workload
@@ -34,7 +34,9 @@ public final class PNodeVernacular {
public static String toSql(PNode node) { return expr(node, Dialect.SQL); }
/// A CQL `WHERE` expression: `!=` becomes a `<`/`>` disjunction and
/// `MATCHES` becomes `LIKE`, which is what CQL offers.
/// `MATCHES` becomes `LIKE`, which is what CQL offers. A conjunction
/// renders as the bare `a AND b` a CQL `WHERE` clause accepts —
/// the reference wraps it in parentheses, which Cassandra rejects.
public static String toCql(PNode node) { return expr(node, Dialect.CQL); }
/// A CDDL group: `{ field: "age", op: "gt", value: 18 }`, conjugates
@@ -73,7 +75,12 @@ public final class PNodeVernacular {
PNode.Conjugate c = (PNode.Conjugate) node;
List<String> parts = new ArrayList<>();
for (PNode child : c.children()) parts.add(expr(child, dialect));
return parts.size() == 1 ? parts.get(0) : "(" + String.join(" " + c.type().name() + " ", parts) + ")";
if (parts.size() == 1) return parts.get(0);
String joined = String.join(" " + c.type().name() + " ", parts);
// A CQL WHERE clause is a bare list of relations: it has no
// grouping parentheses, and Cassandra rejects a conjunction
// wrapped in them. SQL keeps the grouping.
return dialect == Dialect.CQL && c.type() == PNode.ConjugateType.AND ? joined : "(" + joined + ")";
}
private static String single(String field, OpType op, List<Comparand> cs, Dialect dialect) {
@@ -97,7 +97,7 @@ class VernacularsTest {
String sql = render(p, Vernacular.SQL);
assertTrue(sql.contains("age > 18")); assertTrue(sql.contains("status IN (1, 2, 3)")); assertTrue(sql.contains("AND"));
assertEquals("(age > 18 AND status IN (1, 2, 3))", sql);
assertEquals(sql, render(p, Vernacular.CQL));
assertEquals("age > 18 AND status IN (1, 2, 3)", render(p, Vernacular.CQL), "CQL takes no grouping parentheses");
String json = render(p, Vernacular.JSON);
assertTrue(json.contains("\"and\"")); assertTrue(json.contains("\"age\""));
assertEquals("{\"type\":\"and\",\"children\":[{\"type\":\"predicate\",\"field\":\"age\",\"op\":\">\",\"value\":18},{\"type\":\"predicate\",\"field\":\"status\",\"op\":\"IN\",\"values\":[1,2,3]}]}",
@@ -197,4 +197,19 @@ class VernacularsTest {
assertEquals("and", p.get("type"));
assertEquals(2, ((List<?>) p.get("children")).size());
}
/// A CQL WHERE clause has no grouping parentheses, so a conjunction
/// renders bare there — unlike SQL, which keeps the grouping, and
/// unlike the reference, which parenthesizes both (to be reconciled
/// upstream). The `!=` rewrite keeps its parentheses in both.
@Test void aCqlConjunctionRendersWithoutGroupingParentheses() {
PNode and = new PNode.Conjugate(PNode.ConjugateType.AND, java.util.List.of(
new PNode.Predicate(new PNode.FieldRef.Named("topic"), PNode.OpType.EQ, java.util.List.of(new PNode.Comparand.Text("tax"))),
new PNode.Predicate(new PNode.FieldRef.Named("pct"), PNode.OpType.GE, java.util.List.of(new PNode.Comparand.Int(99)))));
assertEquals("topic = 'tax' AND pct >= 99", Vernaculars.render(ANode.of(and), Vernacular.CQL));
assertEquals("(topic = 'tax' AND pct >= 99)", Vernaculars.render(ANode.of(and), Vernacular.SQL));
PNode nested = new PNode.Conjugate(PNode.ConjugateType.AND, java.util.List.of(and,
new PNode.Predicate(new PNode.FieldRef.Named("year"), PNode.OpType.NE, java.util.List.of(new PNode.Comparand.Int(2020)))));
assertEquals("topic = 'tax' AND pct >= 99 AND (year < 2020 OR year > 2020)", Vernaculars.render(ANode.of(nested), Vernacular.CQL));
}
}