diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml
index bbd9c52185..33ade035a2 100644
--- a/client-java/controller/pom.xml
+++ b/client-java/controller/pom.xml
@@ -193,6 +193,11 @@
libthrifttest
+
+ org.apache.cassandra
+ java-driver-core
+ test
+ org.mongodbbson
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculator.java
new file mode 100644
index 0000000000..c74259062c
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculator.java
@@ -0,0 +1,101 @@
+package org.evomaster.client.java.controller.cassandra.calculator;
+
+import org.evomaster.client.java.controller.cassandra.model.CassandraRow;
+import org.evomaster.client.java.controller.cassandra.operations.CqlQueryOperation;
+import org.evomaster.client.java.controller.cassandra.parser.CqlParserUtils;
+import org.evomaster.client.java.distance.heuristics.DistanceHelper;
+import org.evomaster.client.java.distance.heuristics.Truthness;
+import org.evomaster.client.java.distance.heuristics.TruthnessUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Calculator designed to compute a branch-distance-based heuristic for a CQL (Cassandra Query Language) query,
+ * given the rows returned by executing that query against the database.
+ *
+ * The distance reflects how far the query's WHERE condition is from being satisfied by
+ * the available data and is used to guide the search towards inserting data in the database
+ * that makes the executed queries return non-empty results during the execution of generated tests.
+ */
+public class CassandraHeuristicsCalculator {
+
+ public static final double C = DistanceHelper.H_NOT_NULL;
+ public static final double C_BETTER = 0.15;
+
+ public static final Truthness TRUE_TRUTHNESS = new Truthness(1.0, C);
+ public static final Truthness FALSE_TRUTHNESS = new Truthness(C, 1.0);
+ public static final Truthness FALSE_TRUTHNESS_BETTER = new Truthness(C_BETTER, 1.0);
+
+ private final CassandraOperationEvaluator evaluator = new CassandraOperationEvaluator();
+
+ /**
+ * Computes the heuristic distance of the given CQL query with respect to the provided rows.
+ * A distance of {@code 0.0} means the query is satisfied, while values closer
+ * to {@code 1.0} indicate the query is further from being satisfied.
+ *
+ * @param cqlQuery the CQL query to evaluate
+ * @param returnedRows all rows in the table the query targets, used to evaluate how close
+ * the query's condition is to matching at least one row
+ * @return a distance value in {@code [0.0, 1.0]}, where {@code 0.0} represents the best case
+ */
+ public double computeDistance(String cqlQuery, Iterable returnedRows) {
+ return 1.0 - computeHQuery(cqlQuery, returnedRows).getOfTrue();
+ }
+
+ private Truthness computeHQuery(String cqlQuery, Iterable returnedRows) {
+ if (!CqlParserUtils.canParseCqlCommand(cqlQuery)) {
+ return FALSE_TRUTHNESS;
+ }
+
+ List rows = toList(returnedRows);
+
+ org.evomaster.client.java.controller.cassandra.parser.CqlParser.RootContext root =
+ CqlParserUtils.parseCqlCommand(cqlQuery);
+ CqlQueryOperation whereOp = CqlParserUtils.getWhereOperation(root);
+
+ Truthness hRowSet = computeHRowSet(rows);
+
+ if (whereOp == null) {
+ return hRowSet;
+ }
+
+ Truthness hCondition = computeHCondition(whereOp, rows);
+ return TruthnessUtils.buildAndAggregationTruthness(hRowSet, hCondition);
+ }
+
+ private Truthness computeHRowSet(List rows) {
+ return TruthnessUtils.getTruthnessToEmpty(rows.size()).invert();
+ }
+
+ private Truthness computeHCondition(CqlQueryOperation condition,
+ List rows) {
+ if (rows.isEmpty()) {
+ return FALSE_TRUTHNESS;
+ }
+
+ double maxOfTrue = 0.0;
+ for (CassandraRow row : rows) {
+ double ofTrue = evaluator.evaluate(condition, row).getOfTrue();
+ if (ofTrue >= 1.0) {
+ return TRUE_TRUTHNESS;
+ } else if (ofTrue > maxOfTrue) {
+ maxOfTrue = ofTrue;
+ }
+ }
+
+ return TruthnessUtils.buildScaledTruthness(C, maxOfTrue);
+ }
+
+ private static List toList(Iterable iterable) {
+ if (iterable instanceof List) {
+ return (List) iterable;
+ }
+
+ List list = new ArrayList<>();
+ for (CassandraRow row : iterable) {
+ list.add(row);
+ }
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraOperationEvaluator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraOperationEvaluator.java
new file mode 100644
index 0000000000..6dfcbad855
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraOperationEvaluator.java
@@ -0,0 +1,411 @@
+package org.evomaster.client.java.controller.cassandra.calculator;
+
+import org.evomaster.client.java.controller.cassandra.model.CassandraRow;
+import org.evomaster.client.java.controller.cassandra.model.CqlDurationLiteral;
+import org.evomaster.client.java.controller.cassandra.operations.*;
+import org.evomaster.client.java.controller.cassandra.parser.CqlDurationLiteralParser;
+import org.evomaster.client.java.distance.heuristics.Truthness;
+import org.evomaster.client.java.distance.heuristics.TruthnessUtils;
+
+import java.net.InetAddress;
+import java.time.*;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalAmount;
+import java.time.temporal.TemporalUnit;
+import java.util.*;
+import java.util.function.Function;
+
+import static org.evomaster.client.java.controller.cassandra.calculator.CassandraHeuristicsCalculator.*;
+
+/**
+ * Computes CQL-heuristics {@link Truthness} for a {@link CqlQueryOperation} against a
+ * candidate {@link CassandraRow}, i.e. how close the row comes to satisfying the query's
+ * WHERE-clause condition(s). Used to guide the search when EvoMaster generates data intended
+ * to make a given Cassandra query return non-empty results.
+ */
+public class CassandraOperationEvaluator {
+
+ private enum ComparisonType { EQUALS, GT, GTE, LT, LTE }
+
+ /**
+ * Each entry attempts to parse a timestamp string under a different format, returning null
+ * if that format doesn't match, so {@link #parseTimestampString} can fall through to the next one.
+ */
+ private static final List> TIMESTAMP_PARSERS = Arrays.asList(
+ CassandraOperationEvaluator::tryParseIsoInstant,
+ CassandraOperationEvaluator::tryParseWithTimestampFormatters,
+ CassandraOperationEvaluator::tryParseDateWithOffset,
+ CassandraOperationEvaluator::tryParseDateOnly
+ );
+
+ private static final DateTimeFormatter[] TIMESTAMP_FORMATTERS = {
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSXX"),
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXX"),
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mmXX"),
+ DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXX"),
+ DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXX"),
+ DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mmXX"),
+ };
+
+ private static final DateTimeFormatter DATE_WITH_OFFSET = DateTimeFormatter.ofPattern("yyyy-MM-ddXX");
+
+ /**
+ * Recursively evaluates a {@link CqlQueryOperation} against a row, dispatching to the
+ * appropriate comparison logic based on the operation's concrete type (AND, equals,
+ * greater/less-than variants, IN, CONTAINS, CONTAINS KEY). Returns {@code FALSE_TRUTHNESS}
+ * for operation types that are not recognised/supported.
+ */
+ public Truthness evaluate(CqlQueryOperation op, CassandraRow candidateRow) {
+ if (op instanceof AndOperation) {
+ return evaluateAnd((AndOperation) op, candidateRow);
+ } else if (op instanceof EqualsOperation>) {
+ return evaluateEquals((EqualsOperation>) op, candidateRow);
+ } else if (op instanceof GreaterThanOperation>) {
+ return evaluateGreaterThan((GreaterThanOperation>) op, candidateRow);
+ } else if (op instanceof GreaterThanEqualsOperation>) {
+ return evaluateGreaterThanEquals((GreaterThanEqualsOperation>) op, candidateRow);
+ } else if (op instanceof LessThanOperation>) {
+ return evaluateLessThan((LessThanOperation>) op, candidateRow);
+ } else if (op instanceof LessThanEqualsOperation>) {
+ return evaluateLessThanEquals((LessThanEqualsOperation>) op, candidateRow);
+ } else if (op instanceof InOperation) {
+ return evaluateIn((InOperation) op, candidateRow);
+ } else if (op instanceof ContainsOperation>) {
+ return evaluateContains((ContainsOperation>) op, candidateRow);
+ } else if (op instanceof ContainsKeyOperation>) {
+ return evaluateContainsKey((ContainsKeyOperation>) op, candidateRow);
+ } else {
+ return FALSE_TRUTHNESS;
+ }
+ }
+
+ private Truthness evaluateAnd(AndOperation op, CassandraRow candidateRow) {
+ List results = new ArrayList<>();
+ for (CqlQueryOperation condition : op.getConditions()) {
+ results.add(evaluate(condition, candidateRow));
+ }
+
+ return TruthnessUtils.buildAndAggregationTruthness(results.toArray(new Truthness[0]));
+ }
+
+ private Truthness evaluateEquals(EqualsOperation> op, CassandraRow candidateRow) {
+ return evaluateComparison(op, candidateRow, ComparisonType.EQUALS);
+ }
+
+ private Truthness evaluateGreaterThan(GreaterThanOperation> op, CassandraRow candidateRow) {
+ return evaluateComparison(op, candidateRow, ComparisonType.GT);
+ }
+
+ private Truthness evaluateGreaterThanEquals(GreaterThanEqualsOperation> op, CassandraRow candidateRow) {
+ return evaluateComparison(op, candidateRow, ComparisonType.GTE);
+ }
+
+ private Truthness evaluateLessThan(LessThanOperation> op, CassandraRow candidateRow) {
+ return evaluateComparison(op, candidateRow, ComparisonType.LT);
+ }
+
+ private Truthness evaluateLessThanEquals(LessThanEqualsOperation> op, CassandraRow candidateRow) {
+ return evaluateComparison(op, candidateRow, ComparisonType.LTE);
+ }
+
+ private Truthness evaluateIn(InOperation op, CassandraRow candidateRow) {
+ Object rowValue = candidateRow.getValue(op.getColumnName());
+
+ return any(rowValue, op.getValues());
+ }
+
+ private Truthness evaluateContains(ContainsOperation> op, CassandraRow candidateRow) {
+ Object rawCol = candidateRow.getValue(op.getColumnName());
+ if (rawCol == null) {
+ return FALSE_TRUTHNESS;
+ } else {
+ return any(op.getValue(), toElementList(rawCol));
+ }
+ }
+
+ private Truthness evaluateContainsKey(ContainsKeyOperation> op, CassandraRow candidateRow) {
+ Object rawCol = candidateRow.getValue(op.getColumnName());
+ if (rawCol == null) {
+ return FALSE_TRUTHNESS;
+ } else if (!(rawCol instanceof Map, ?>)) {
+ return FALSE_TRUTHNESS;
+ } else {
+ List> keys = new ArrayList<>(((Map, ?>) rawCol).keySet());
+ return any(op.getValue(), keys);
+ }
+ }
+
+ private Truthness evaluateComparison(ComparisonOperation> op, CassandraRow candidateRow, ComparisonType type) {
+ Object rowValue = candidateRow.getValue(op.getColumnName());
+ Object queryValue = op.getValue();
+
+ if (rowValue == null && queryValue == null) {
+ return FALSE_TRUTHNESS;
+ } else if (rowValue == null || queryValue == null) {
+ return FALSE_TRUTHNESS_BETTER;
+ } else {
+ Truthness typeResult = compareByType(rowValue, queryValue, type);
+ if (typeResult.isTrue()) {
+ return typeResult;
+ } else {
+ return TruthnessUtils.buildScaledTruthness(C_BETTER, typeResult.getOfTrue());
+ }
+ }
+ }
+
+ private Truthness compareByType(Object rowValue, Object literalValue, ComparisonType comparisonType) {
+ if (rowValue instanceof Number && literalValue instanceof Number) {
+ return compareNumeric(rowValue, literalValue, comparisonType);
+ } else if (rowValue instanceof String || rowValue instanceof InetAddress) {
+ return compareString(rowValue, literalValue, comparisonType);
+ } else if (rowValue instanceof Boolean) {
+ return compareBoolean(rowValue, literalValue, comparisonType);
+ } else if (rowValue instanceof UUID) {
+ return compareUuid(rowValue, literalValue, comparisonType);
+ } else if (isTemporalType(rowValue)) {
+ return compareTemporal(rowValue, literalValue, comparisonType);
+ } else if (isCqlDuration(rowValue)) {
+ return compareDuration(rowValue, literalValue, comparisonType);
+ } else {
+ throw new IllegalArgumentException("Unsupported row value type: " + rowValue.getClass().getName());
+ }
+ }
+
+ private Truthness compareNumeric(Object rowValue, Object literalValue, ComparisonType comparisonType) {
+ double x = ((Number) rowValue).doubleValue();
+ double y = ((Number) literalValue).doubleValue();
+
+ return getTruthnessForNumeric(comparisonType, x, y);
+ }
+
+ private Truthness compareString(Object rowValue, Object literalValue, ComparisonType comparisonType) {
+ if (comparisonType != ComparisonType.EQUALS) {
+ throw new IllegalArgumentException("Unsupported operator for string literals: " + comparisonType);
+ } else {
+ String a = (rowValue instanceof InetAddress)
+ ? ((InetAddress) rowValue).getHostAddress()
+ : (String) rowValue;
+ String b = (String) literalValue;
+
+ return TruthnessUtils.getStringEqualityTruthness(a, b);
+ }
+ }
+
+ private Truthness compareBoolean(Object rowValue, Object literalValue, ComparisonType comparisonType) {
+ if (comparisonType != ComparisonType.EQUALS) {
+ throw new IllegalArgumentException("Unsupported operator for boolean literals: " + comparisonType);
+ } else {
+ double x = ((Boolean) rowValue) ? 1.0 : 0.0;
+ double y = ((Boolean) literalValue) ? 1.0 : 0.0;
+
+ return TruthnessUtils.getEqualityTruthness(x, y);
+ }
+ }
+
+ private Truthness compareUuid(Object rowValue, Object literalValue, ComparisonType comparisonType) {
+ if (comparisonType != ComparisonType.EQUALS) {
+ throw new IllegalArgumentException("Unsupported operator for UUID literals: " + comparisonType);
+ } else {
+ return TruthnessUtils.getEqualityTruthness((UUID) rowValue, (UUID) literalValue);
+ }
+ }
+
+ private static boolean isTemporalType(Object rowValue) {
+ return rowValue instanceof LocalDate
+ || rowValue instanceof LocalTime
+ || rowValue instanceof Instant;
+ }
+
+ private Truthness compareTemporal(Object rowValue, Object literalValue, ComparisonType comparisonType) {
+ try {
+ double x = (double) toLong(rowValue, rowValue);
+ double y = (double) toLong(literalValue, rowValue);
+
+ return getTruthnessForNumeric(comparisonType, x, y);
+ } catch (Exception e) {
+ return FALSE_TRUTHNESS;
+ }
+ }
+
+ private Truthness getTruthnessForNumeric(ComparisonType comparisonType, double x, double y) {
+ switch (comparisonType) {
+ case EQUALS: return TruthnessUtils.getEqualityTruthness(x, y);
+ case GT: return TruthnessUtils.getLessThanTruthness(y, x);
+ case GTE: return TruthnessUtils.getLessThanTruthness(x, y).invert();
+ case LT: return TruthnessUtils.getLessThanTruthness(x, y);
+ case LTE: return TruthnessUtils.getLessThanTruthness(y, x).invert();
+ default: throw new IllegalArgumentException("Unsupported operator for numeric literals: " + comparisonType);
+ }
+ }
+
+ /**
+ * Converts a temporal value to a comparable epoch-millisecond {@code long}.
+ *
+ * {@code rowValue} is the value to convert, and it may come either from the row itself
+ * or from the query's literal — in the latter case it is often a plain {@link String}
+ * (e.g. {@code "2011-02-03"}) that carries no type information of its own. {@code
+ * rowValueHint} disambiguates which temporal type ({@link LocalDate}, {@link LocalTime}
+ * or {@link Instant}) {@code rowValue} should be interpreted as, since that type is only
+ * known from the actual row value's column type. This lets a query literal be parsed and
+ * compared using the same temporal interpretation as the row value it's being compared
+ * against, even though the literal itself is untyped.
+ */
+ private static long toLong(Object rowValue, Object rowValueHint) {
+ if (rowValueHint instanceof LocalDate) {
+ LocalDate dateVal = (rowValue instanceof Long) ? LocalDate.ofEpochDay((Long) rowValue)
+ : (rowValue instanceof String) ? LocalDate.parse((String) rowValue)
+ : (LocalDate) rowValue;
+
+ return dateVal.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli();
+ } else if (rowValueHint instanceof LocalTime) {
+ LocalTime timeVal = (rowValue instanceof Long) ? LocalTime.ofNanoOfDay((Long) rowValue)
+ : (rowValue instanceof String) ? LocalTime.parse((String) rowValue)
+ : (LocalTime) rowValue;
+
+ return LocalDateTime.of(LocalDate.of(1970, 1, 1), timeVal).toInstant(ZoneOffset.UTC).toEpochMilli();
+ } else if (rowValueHint instanceof Instant) {
+ if (rowValue instanceof Long) {
+ return (Long) rowValue;
+ } else if (rowValue instanceof Instant) {
+ return ((Instant) rowValue).toEpochMilli();
+ } else if (rowValue instanceof String) {
+ return parseTimestampString((String) rowValue).toEpochMilli();
+ } else {
+ throw new IllegalArgumentException("Unexpected timestamp value type: " + rowValue.getClass());
+ }
+ } else {
+ throw new IllegalArgumentException("Unrecognized temporal type: " + rowValueHint.getClass());
+ }
+ }
+
+ private static Instant parseTimestampString(String rowValue) {
+ for (Function parser : TIMESTAMP_PARSERS) {
+ Instant parsed = parser.apply(rowValue);
+ if (parsed != null) {
+ return parsed;
+ }
+ }
+
+ throw new IllegalArgumentException("Cannot parse timestamp string: " + rowValue);
+ }
+
+ private static Instant tryParseIsoInstant(String rowValue) {
+ try {
+ return Instant.parse(rowValue);
+ } catch (DateTimeParseException e) {
+ return null;
+ }
+ }
+
+ private static Instant tryParseWithTimestampFormatters(String rowValue) {
+ for (DateTimeFormatter formatter : TIMESTAMP_FORMATTERS) {
+ try {
+ return OffsetDateTime.parse(rowValue, formatter).toInstant();
+ } catch (DateTimeParseException ignored) {}
+ }
+ return null;
+ }
+
+ /**
+ * Parses date-only strings with an offset (e.g. {@code "2011-02-03+0000"}), which {@link
+ * OffsetDateTime#parse} rejects since they carry no time component. The {@link LocalDate}
+ * and {@link ZoneOffset} are extracted from the parsed {@link TemporalAccessor} directly.
+ */
+ private static Instant tryParseDateWithOffset(String rowValue) {
+ try {
+ TemporalAccessor accessor = DATE_WITH_OFFSET.parse(rowValue);
+ return LocalDate.from(accessor).atStartOfDay(ZoneOffset.from(accessor)).toInstant();
+ } catch (DateTimeParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Parses date-only strings without an offset (e.g. {@code "2011-02-03"}), treating them as
+ * midnight UTC.
+ */
+ private static Instant tryParseDateOnly(String rowValue) {
+ try {
+ return LocalDate.parse(rowValue).atStartOfDay(ZoneOffset.UTC).toInstant();
+ } catch (DateTimeParseException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Identifies the driver's {@code CqlDuration} by checking for its distinctive combination of supported temporal
+ * units rather than {@code instanceof}, since other JDK types (e.g. {@link Period}) also implement {@link TemporalAmount}.
+ */
+ private static boolean isCqlDuration(Object rowValue) {
+ if (!(rowValue instanceof TemporalAmount)) {
+ return false;
+ } else {
+ List units = ((TemporalAmount) rowValue).getUnits();
+ return units.contains(ChronoUnit.MONTHS)
+ && units.contains(ChronoUnit.DAYS)
+ && units.contains(ChronoUnit.NANOS);
+ }
+ }
+
+ private Truthness compareDuration(Object rowValue, Object literalValue, ComparisonType comparisonType) {
+ if (comparisonType != ComparisonType.EQUALS) {
+ throw new IllegalArgumentException("Unsupported operator for duration literals: " + comparisonType);
+ } else {
+ TemporalAmount duration = (TemporalAmount) rowValue;
+ long months = duration.get(ChronoUnit.MONTHS);
+ long days = duration.get(ChronoUnit.DAYS);
+ long nanos = duration.get(ChronoUnit.NANOS);
+
+ CqlDurationLiteral literalDurationValue = CqlDurationLiteralParser.parse((String) literalValue);
+
+ return TruthnessUtils.buildAndAggregationTruthness(
+ TruthnessUtils.getEqualityTruthness(months, literalDurationValue.months),
+ TruthnessUtils.getEqualityTruthness(days, literalDurationValue.days),
+ TruthnessUtils.getEqualityTruthness(nanos, literalDurationValue.nanos)
+ );
+ }
+ }
+
+ private Truthness any(Object value, List> candidates) {
+ if (candidates.isEmpty()) {
+ return FALSE_TRUTHNESS;
+ } else {
+ Truthness[] truthnesses = candidates.stream()
+ .map(candidate -> evaluateEquals(value, candidate))
+ .toArray(Truthness[]::new);
+
+ return TruthnessUtils.buildOrAggregationTruthness(truthnesses);
+ }
+ }
+
+ private Truthness evaluateEquals(Object a, Object b) {
+ if (a == null && b == null) {
+ return FALSE_TRUTHNESS;
+ } else if (a == null || b == null) {
+ return FALSE_TRUTHNESS_BETTER;
+ } else {
+ Truthness raw = compareByType(a, b, ComparisonType.EQUALS);
+ if (raw.isTrue()) {
+ return raw;
+ } else {
+ return TruthnessUtils.buildScaledTruthness(C_BETTER, raw.getOfTrue());
+ }
+ }
+ }
+
+ private static List> toElementList(Object collection) {
+ if (collection instanceof List>) {
+ return (List>) collection;
+ } else if (collection instanceof Set>) {
+ return new ArrayList<>((Set>) collection);
+ } else if (collection instanceof Map, ?>) {
+ return new ArrayList<>(((Map, ?>) collection).values());
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/model/CassandraRow.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/model/CassandraRow.java
new file mode 100644
index 0000000000..9746dbcf65
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/model/CassandraRow.java
@@ -0,0 +1,48 @@
+package org.evomaster.client.java.controller.cassandra.model;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A single row of a Cassandra table, keyed by column name.
+ * Column-name lookups are case-insensitive and tolerate CQL-quoted identifiers
+ * (e.g. {@code "myCol"}), matching CQL's own identifier normalisation rules.
+ */
+public class CassandraRow {
+
+ private static final String DOUBLE_QUOTE = "\"";
+
+ private final Map columns;
+
+ public CassandraRow(Map columns) {
+ this.columns = new LinkedHashMap<>();
+ for (Map.Entry e : columns.entrySet()) {
+ this.columns.put(normaliseColumnName(e.getKey()), e.getValue());
+ }
+ }
+
+ /**
+ * @param rawColumnName the column name, possibly CQL-quoted
+ * @return the column's value, or {@code null} if the column is absent from this
+ * row or its value is {@code null}
+ */
+ public Object getValue(String rawColumnName) {
+ return columns.get(normaliseColumnName(rawColumnName));
+ }
+
+ /**
+ * Normalises a column name for lookup: strips surrounding double quotes (CQL's
+ * case-sensitive quoted-identifier syntax) and lower-cases the result, since
+ * unquoted CQL identifiers are case-insensitive.
+ *
+ * @param rawColumnName the column name as it appears in the CQL query, possibly quoted
+ * @return the normalized column name, or {@code null} if {@code rawColumnName} is {@code null}
+ */
+ private static String normaliseColumnName(String rawColumnName) {
+ if (rawColumnName == null) return null;
+ if (rawColumnName.startsWith(DOUBLE_QUOTE) && rawColumnName.endsWith(DOUBLE_QUOTE)) {
+ return rawColumnName.substring(1, rawColumnName.length() - 1).toLowerCase();
+ }
+ return rawColumnName.toLowerCase();
+ }
+}
\ No newline at end of file
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/model/CqlDurationLiteral.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/model/CqlDurationLiteral.java
new file mode 100644
index 0000000000..4ae872a631
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/model/CqlDurationLiteral.java
@@ -0,0 +1,21 @@
+package org.evomaster.client.java.controller.cassandra.model;
+
+/**
+ * Represents a parsed Cassandra {@code duration} literal, decomposed into its three
+ * components: months, days, and nanoseconds.
+ *
+ * Instances are produced by
+ * {@link org.evomaster.client.java.controller.cassandra.parser.CqlDurationLiteralParser}.
+ */
+public class CqlDurationLiteral {
+
+ public final int months;
+ public final int days;
+ public final long nanos;
+
+ public CqlDurationLiteral(int months, int days, long nanos) {
+ this.months = months;
+ this.days = days;
+ this.nanos = nanos;
+ }
+}
\ No newline at end of file
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/EqualsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/EqualsOperation.java
index e4259af623..e5eb6af21f 100644
--- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/EqualsOperation.java
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/EqualsOperation.java
@@ -1,5 +1,8 @@
package org.evomaster.client.java.controller.cassandra.operations;
+/**
+ * Represents a CQL equality comparison operation ({@code column = value}).
+ */
public class EqualsOperation extends ComparisonOperation {
public EqualsOperation(String columnName, V value) {
super(columnName, value);
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/GreaterThanEqualsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/GreaterThanEqualsOperation.java
index 93e3e0f29b..70f2fe4a0d 100644
--- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/GreaterThanEqualsOperation.java
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/GreaterThanEqualsOperation.java
@@ -1,5 +1,8 @@
package org.evomaster.client.java.controller.cassandra.operations;
+/**
+ * Represents a CQL greater-than-or-equals comparison operation ({@code column >= value}).
+ */
public class GreaterThanEqualsOperation extends ComparisonOperation {
public GreaterThanEqualsOperation(String columnName, V value) {
super(columnName, value);
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/GreaterThanOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/GreaterThanOperation.java
index b98de80818..03bb7dddd4 100644
--- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/GreaterThanOperation.java
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/GreaterThanOperation.java
@@ -1,5 +1,8 @@
package org.evomaster.client.java.controller.cassandra.operations;
+/**
+ * Represents a CQL greater-than comparison operation ({@code column > value}).
+ */
public class GreaterThanOperation extends ComparisonOperation {
public GreaterThanOperation(String columnName, V value) {
super(columnName, value);
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/LessThanEqualsOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/LessThanEqualsOperation.java
index e4f0a2860a..60d621106e 100644
--- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/LessThanEqualsOperation.java
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/LessThanEqualsOperation.java
@@ -1,5 +1,8 @@
package org.evomaster.client.java.controller.cassandra.operations;
+/**
+ * Represents a CQL less-than-or-equals comparison operation ({@code column <= value}).
+ */
public class LessThanEqualsOperation extends ComparisonOperation {
public LessThanEqualsOperation(String columnName, V value) {
super(columnName, value);
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/LessThanOperation.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/LessThanOperation.java
index 00b1c7a32e..bfd88be50c 100644
--- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/LessThanOperation.java
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/operations/LessThanOperation.java
@@ -1,5 +1,8 @@
package org.evomaster.client.java.controller.cassandra.operations;
+/**
+ * Represents a CQL less-than comparison operation ({@code column < value}).
+ */
public class LessThanOperation extends ComparisonOperation {
public LessThanOperation(String columnName, V value) {
super(columnName, value);
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java
new file mode 100644
index 0000000000..11ddce2c76
--- /dev/null
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java
@@ -0,0 +1,242 @@
+package org.evomaster.client.java.controller.cassandra.parser;
+
+import org.evomaster.client.java.controller.cassandra.model.CqlDurationLiteral;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses a Cassandra duration string literal into a {@link CqlDurationLiteral}.
+ *
+ *
Supports four formats, all optionally prefixed with {@code -} for a negative duration,
+ * mirroring {@code com.datastax.oss.driver.api.core.data.CqlDuration#from(String)}:
+ *
+ *
+ *
Standard Cassandra: {@code 1y2mo3w4d5h6m7s} (units, including weeks, may appear in any
+ * order)
+ *
ISO 8601: {@code P1Y2M3DT4H5M6S}
+ *
ISO 8601, weeks only: {@code P3W}
+ *
ISO 8601, alternative format: {@code P0001-02-03T04:05:06}
+ *
+ */
+public class CqlDurationLiteralParser {
+
+ private CqlDurationLiteralParser() {}
+
+ private static final String UNIT_MONTH = "mo";
+ private static final String UNIT_MILLISECOND = "ms";
+ private static final String UNIT_MICROSECOND_MU = "µs";
+ private static final String UNIT_MICROSECOND = "us";
+ private static final String UNIT_NANOSECOND = "ns";
+ private static final String UNIT_YEAR = "y";
+ private static final String UNIT_WEEK = "w";
+ private static final String UNIT_DAY = "d";
+ private static final String UNIT_HOUR = "h";
+ private static final String UNIT_MINUTE = "m";
+ private static final String UNIT_SECOND = "s";
+
+ private static final int MONTHS_PER_YEAR = 12;
+ private static final int DAYS_PER_WEEK = 7;
+ private static final long NANOS_PER_HOUR = 3_600_000_000_000L;
+ private static final long NANOS_PER_MINUTE = 60_000_000_000L;
+ private static final long NANOS_PER_SECOND = 1_000_000_000L;
+ private static final long NANOS_PER_MILLISECOND = 1_000_000L;
+ private static final long NANOS_PER_MICROSECOND = 1_000L;
+
+ private static final char ISO_TIME_SEPARATOR = 'T';
+ private static final char ISO_YEAR_DESIGNATOR = 'Y';
+ private static final char ISO_MONTH_OR_MINUTE_DESIGNATOR = 'M';
+ private static final char ISO_DAY_DESIGNATOR = 'D';
+ private static final char ISO_HOUR_DESIGNATOR = 'H';
+ private static final char ISO_SECOND_DESIGNATOR = 'S';
+
+ private static final String ISO_DURATION_PREFIX_UPPER = "P";
+ private static final String ISO_DURATION_PREFIX_LOWER = "p";
+ private static final String ISO_WEEK_SUFFIX = "W";
+
+ /**
+ * Matches the standard Cassandra format: an optional digit sequence followed by a unit
+ * token. Units are ordered longest-first ({@code mo, ms, µs, us, ns, y, w, d, h, m, s}) to
+ * avoid {@code m} consuming {@code mo}, or {@code ms} consuming {@code m}.
+ */
+ private static final Pattern STANDARD_PATTERN = Pattern.compile(
+ "(\\d+)(" + String.join("|", UNIT_MONTH, UNIT_MILLISECOND, UNIT_MICROSECOND_MU,
+ UNIT_MICROSECOND, UNIT_NANOSECOND, UNIT_YEAR, UNIT_WEEK, UNIT_DAY, UNIT_HOUR, UNIT_MINUTE, UNIT_SECOND) + ")",
+ Pattern.CASE_INSENSITIVE);
+
+ /**
+ * Matches the general ISO 8601 format, e.g. {@code P1Y2M3DT4H5M6S}. Every component is
+ * optional, mirroring the driver's own grammar, so bare {@code P} or {@code PT} are valid
+ * (zero-length) durations.
+ */
+ private static final Pattern ISO8601_PATTERN = Pattern.compile(
+ ISO_DURATION_PREFIX_UPPER
+ + "(?:(\\d+)" + ISO_YEAR_DESIGNATOR + ")?"
+ + "(?:(\\d+)" + ISO_MONTH_OR_MINUTE_DESIGNATOR + ")?"
+ + "(?:(\\d+)" + ISO_DAY_DESIGNATOR + ")?"
+ + "(?:" + ISO_TIME_SEPARATOR
+ + "(?:(\\d+)" + ISO_HOUR_DESIGNATOR + ")?"
+ + "(?:(\\d+)" + ISO_MONTH_OR_MINUTE_DESIGNATOR + ")?"
+ + "(?:(\\d+)" + ISO_SECOND_DESIGNATOR + ")?)?");
+
+ /** Matches the ISO 8601 weeks-only format, e.g. {@code P3W}. */
+ private static final Pattern ISO8601_WEEK_PATTERN =
+ Pattern.compile(ISO_DURATION_PREFIX_UPPER + "(\\d+)" + ISO_WEEK_SUFFIX);
+
+ /** Matches the ISO 8601 alternative format, e.g. {@code P0001-02-03T04:05:06}. */
+ private static final Pattern ISO8601_ALTERNATIVE_PATTERN = Pattern.compile(
+ ISO_DURATION_PREFIX_UPPER + "(\\d{4})-(\\d{2})-(\\d{2})" + ISO_TIME_SEPARATOR + "(\\d{2}):(\\d{2}):(\\d{2})");
+
+ /**
+ * Parses a CQL duration literal into a {@link CqlDurationLiteral}. A leading {@code -} is
+ * stripped and applied as a sign to all three components. The remainder is dispatched to the
+ * matching pattern-specific parser by {@link #parseUnsigned}.
+ *
+ * @param stringDuration the duration literal to parse; must not be {@code null}, empty, or
+ * blank
+ * @return the parsed duration, decomposed into months, days and nanoseconds
+ * @throws IllegalArgumentException if {@code stringDuration} is {@code null}, empty, or blank
+ */
+ public static CqlDurationLiteral parse(String stringDuration) {
+ if (stringDuration == null) {
+ throw new IllegalArgumentException("Empty duration literal");
+ } else {
+ String t = stringDuration.trim();
+ if (t.isEmpty()) {
+ throw new IllegalArgumentException("Empty duration literal");
+ } else {
+ boolean isNegative = t.startsWith("-");
+ String unsigned = isNegative ? t.substring(1) : t;
+ if (unsigned.isEmpty()) {
+ throw new IllegalArgumentException("Empty duration literal");
+ } else {
+ CqlDurationLiteral magnitude = parseUnsigned(unsigned);
+
+ return isNegative
+ ? new CqlDurationLiteral(-magnitude.months, -magnitude.days, -magnitude.nanos)
+ : magnitude;
+ }
+ }
+ }
+ }
+
+ /**
+ * Routes to one of the four pattern-specific parsers, mirroring the format detection in the
+ * driver's {@code CqlDuration.from(String)}: non-{@code P}-prefixed input is the standard
+ * Cassandra format; {@code P}-prefixed input ending in {@code W} is the ISO week-only format;
+ * {@code P}-prefixed input containing {@code -} is the ISO alternative format; anything else
+ * {@code P}-prefixed is the general ISO 8601 format.
+ */
+ private static CqlDurationLiteral parseUnsigned(String unsigned) {
+ if (!unsigned.startsWith(ISO_DURATION_PREFIX_UPPER) && !unsigned.startsWith(ISO_DURATION_PREFIX_LOWER)) {
+ return parseStandardPattern(unsigned);
+ } else {
+ String upper = unsigned.toUpperCase();
+ if (upper.endsWith(ISO_WEEK_SUFFIX)) {
+ return parseIso8601WeekPattern(upper);
+ } else if (upper.contains("-")) {
+ return parseIso8601AlternativePattern(upper);
+ } else {
+ return parseIso8601Pattern(upper);
+ }
+ }
+ }
+
+ private static CqlDurationLiteral parseStandardPattern(String text) {
+ int months = 0;
+ int days = 0;
+ long nanos = 0L;
+
+ Matcher m = STANDARD_PATTERN.matcher(text);
+ while (m.find()) {
+ long value = Long.parseLong(m.group(1));
+ String unit = m.group(2).toLowerCase();
+ switch (unit) {
+ case UNIT_YEAR:
+ months += (int) (value * MONTHS_PER_YEAR);
+ break;
+ case UNIT_MONTH:
+ months += (int) value;
+ break;
+ case UNIT_WEEK:
+ days += (int) (value * DAYS_PER_WEEK);
+ break;
+ case UNIT_DAY:
+ days += (int) value;
+ break;
+ case UNIT_HOUR:
+ nanos += value * NANOS_PER_HOUR;
+ break;
+ case UNIT_MINUTE:
+ nanos += value * NANOS_PER_MINUTE;
+ break;
+ case UNIT_SECOND:
+ nanos += value * NANOS_PER_SECOND;
+ break;
+ case UNIT_MILLISECOND:
+ nanos += value * NANOS_PER_MILLISECOND;
+ break;
+ case UNIT_MICROSECOND:
+ case UNIT_MICROSECOND_MU:
+ nanos += value * NANOS_PER_MICROSECOND;
+ break;
+ case UNIT_NANOSECOND:
+ nanos += value;
+ break;
+ default:
+ break;
+ }
+ }
+ return new CqlDurationLiteral(months, days, nanos);
+ }
+
+ private static CqlDurationLiteral parseIso8601Pattern(String isoDuration) {
+ Matcher matcher = ISO8601_PATTERN.matcher(isoDuration);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'");
+ } else {
+ int years = groupAsInt(matcher, 1);
+ int months = groupAsInt(matcher, 2);
+ int days = groupAsInt(matcher, 3);
+ int hours = groupAsInt(matcher, 4);
+ int minutes = groupAsInt(matcher, 5);
+ int seconds = groupAsInt(matcher, 6);
+
+ long nanos = hours * NANOS_PER_HOUR + minutes * NANOS_PER_MINUTE + seconds * NANOS_PER_SECOND;
+ return new CqlDurationLiteral(years * MONTHS_PER_YEAR + months, days, nanos);
+ }
+ }
+
+ /** Returns the matched group as an {@code int}, or {@code 0} if the (optional) group didn't participate. */
+ private static int groupAsInt(Matcher matcher, int group) {
+ String value = matcher.group(group);
+ return value == null ? 0 : Integer.parseInt(value);
+ }
+
+ private static CqlDurationLiteral parseIso8601WeekPattern(String isoDuration) {
+ Matcher matcher = ISO8601_WEEK_PATTERN.matcher(isoDuration);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'");
+ } else {
+ int weeks = Integer.parseInt(matcher.group(1));
+ return new CqlDurationLiteral(0, weeks * DAYS_PER_WEEK, 0L);
+ }
+ }
+
+ private static CqlDurationLiteral parseIso8601AlternativePattern(String isoDuration) {
+ Matcher matcher = ISO8601_ALTERNATIVE_PATTERN.matcher(isoDuration);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'");
+ } else {
+ int years = Integer.parseInt(matcher.group(1));
+ int months = Integer.parseInt(matcher.group(2));
+ int days = Integer.parseInt(matcher.group(3));
+ int hours = Integer.parseInt(matcher.group(4));
+ int minutes = Integer.parseInt(matcher.group(5));
+ int seconds = Integer.parseInt(matcher.group(6));
+
+ long nanos = hours * NANOS_PER_HOUR + minutes * NANOS_PER_MINUTE + seconds * NANOS_PER_SECOND;
+ return new CqlDurationLiteral(years * MONTHS_PER_YEAR + months, days, nanos);
+ }
+ }
+}
\ No newline at end of file
diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlParserUtils.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlParserUtils.java
index 741a4e110d..2636e454e3 100644
--- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlParserUtils.java
+++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlParserUtils.java
@@ -8,16 +8,39 @@
import java.util.List;
import java.util.UUID;
+/**
+ * Utility methods to parse CQL (Cassandra Query Language) commands with the ANTLR-generated
+ * {@link CqlParser}, and to extract the information needed for heuristics computation (e.g.
+ * the WHERE-clause condition tree) from the resulting parse tree.
+ */
public class CqlParserUtils {
private CqlParserUtils() {}
+ private static final String KEYWORD_SELECT = "SELECT";
+ private static final String KEYWORD_UPDATE = "UPDATE";
+ private static final String KEYWORD_DELETE = "DELETE";
+
+ /**
+ * Parses a CQL command string into its ANTLR parse tree, with no validity checks
+ * performed on the result (assumes {@link #canParseCqlCommand} was used first).
+ *
+ * @param cqlCommand the CQL command to parse
+ * @return the root of the resulting ANTLR parse tree
+ */
public static CqlParser.RootContext parseCqlCommand(String cqlCommand) {
CqlLexer lexer = new CqlLexer(CharStreams.fromString(cqlCommand));
CqlParser parser = new CqlParser(new CommonTokenStream(lexer));
return parser.root();
}
+ /**
+ * Checks whether {@code cqlCommand} can be parsed into a single, well-formed CQL statement.
+ *
+ * @param cqlCommand the CQL command to validate
+ * @return {@code true} if the command parses cleanly (no parser exception, and exactly one
+ * statement is present); {@code false} otherwise
+ */
public static boolean canParseCqlCommand(String cqlCommand) {
try {
CqlParser.RootContext root = parseCqlCommand(cqlCommand);
@@ -27,25 +50,51 @@ public static boolean canParseCqlCommand(String cqlCommand) {
}
}
+ /**
+ * @param cqlCommand the CQL command to inspect
+ * @return {@code true} if {@code cqlCommand} is a SELECT statement
+ */
public static boolean isSelect(String cqlCommand) {
- return cqlCommand.trim().toUpperCase().startsWith("SELECT");
+ return cqlCommand.trim().toUpperCase().startsWith(KEYWORD_SELECT);
}
+ /**
+ * @param cqlCommand the CQL command to inspect
+ * @return {@code true} if {@code cqlCommand} is an UPDATE statement
+ */
public static boolean isUpdate(String cqlCommand) {
- return cqlCommand.trim().toUpperCase().startsWith("UPDATE");
+ return cqlCommand.trim().toUpperCase().startsWith(KEYWORD_UPDATE);
}
+ /**
+ * @param cqlCommand the CQL command to inspect
+ * @return {@code true} if {@code cqlCommand} is a DELETE statement
+ */
public static boolean isDelete(String cqlCommand) {
- return cqlCommand.trim().toUpperCase().startsWith("DELETE");
+ return cqlCommand.trim().toUpperCase().startsWith(KEYWORD_DELETE);
}
+ /**
+ * Extracts the WHERE-clause parse-tree node from a parsed CQL SELECT, UPDATE, or DELETE
+ * statement.
+ *
+ * @param root the root of a parsed CQL command, as returned by {@link #parseCqlCommand}
+ * @return the WHERE clause's parse-tree node, or {@code null} if the statement has none
+ * (or isn't a SELECT/UPDATE/DELETE)
+ */
public static CqlParser.WhereSpecContext getWhereSpec(CqlParser.RootContext root) {
CqlParser.CqlContext cql = root.cqls() != null ? root.cqls().cql(0) : null;
- if (cql == null) return null;
- if (cql.select_() != null) return cql.select_().whereSpec();
- if (cql.update() != null) return cql.update().whereSpec();
- if (cql.delete_() != null) return cql.delete_().whereSpec();
- return null;
+ if (cql == null) {
+ return null;
+ } else if (cql.select_() != null) {
+ return cql.select_().whereSpec();
+ } else if (cql.update() != null) {
+ return cql.update().whereSpec();
+ } else if (cql.delete_() != null) {
+ return cql.delete_().whereSpec();
+ } else {
+ return null;
+ }
}
/**
@@ -56,70 +105,85 @@ public static CqlParser.WhereSpecContext getWhereSpec(CqlParser.RootContext root
*/
public static CqlQueryOperation getWhereOperation(CqlParser.RootContext root) {
CqlParser.WhereSpecContext whereSpec = getWhereSpec(root);
- if (whereSpec == null) return null;
-
- List elements = whereSpec.relationElements().relationElement();
- if (elements.isEmpty()) return null;
- if (elements.size() == 1) return parseRelationElement(elements.get(0));
-
- List ops = new ArrayList<>();
- for (CqlParser.RelationElementContext el : elements) {
- CqlQueryOperation op = parseRelationElement(el);
- if (op != null) ops.add(op);
+ if (whereSpec != null) {
+ List elements = whereSpec.relationElements().relationElement();
+ if (elements.isEmpty()) {
+ return null;
+ } else if (elements.size() == 1) {
+ return parseRelationElement(elements.get(0));
+ } else {
+ List ops = new ArrayList<>();
+ for (CqlParser.RelationElementContext el : elements) {
+ CqlQueryOperation op = parseRelationElement(el);
+ if (op != null) {
+ ops.add(op);
+ }
+ }
+ return new AndOperation(ops);
+ }
+ } else {
+ return null;
}
- return new AndOperation(ops);
}
private static CqlQueryOperation parseRelationElement(CqlParser.RelationElementContext rel) {
- // CONTAINS KEY: col CONTAINS KEY value
- if (rel.relalationContainsKey() != null) {
+ if (rel.relalationContainsKey() != null) { // CONTAINS KEY
CqlParser.RelalationContainsKeyContext ck = rel.relalationContainsKey();
return new ContainsKeyOperation<>(ck.OBJECT_NAME().getText(), parseConstant(ck.constant()));
- }
-
- // CONTAINS: col CONTAINS value
- if (rel.relalationContains() != null) {
+ } else if (rel.relalationContains() != null) { // CONTAINS
CqlParser.RelalationContainsContext c = rel.relalationContains();
return new ContainsOperation<>(c.OBJECT_NAME().getText(), parseConstant(c.constant()));
- }
-
- // IN: col IN (v1, v2, ...)
- if (rel.kwIn() != null) {
+ } else if (rel.kwIn() != null) { // IN
String col = rel.OBJECT_NAME(0).getText();
List