From 92aa301730ae5d4e0f903d96cae4dd10a99ceb3f Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 23 Jun 2026 21:59:18 -0300 Subject: [PATCH 1/8] First calculator draft --- .../CassandraHeuristicsCalculator.java | 352 ++++++++++ .../cassandra/CqlDurationLiteral.java | 101 +++ .../CassandraHeuristicsCalculatorTest.java | 605 ++++++++++++++++++ .../distance/heuristics/TruthnessUtils.java | 11 + 4 files changed, 1069 insertions(+) create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CqlDurationLiteral.java create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java new file mode 100644 index 0000000000..96522ae85a --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java @@ -0,0 +1,352 @@ +package org.evomaster.client.java.controller.cassandra; + +import org.evomaster.client.java.controller.cassandra.operations.*; +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 org.evomaster.client.java.utils.SimpleLogger; + +import java.net.InetAddress; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.*; + +public class CassandraHeuristicsCalculator { + + private static final double C = DistanceHelper.H_NOT_NULL; + private static final double C_BETTER = 0.15; + + private static final Truthness TRUE_TRUTHNESS = new Truthness(1.0, C); + private static final Truthness FALSE_TRUTHNESS = new Truthness(C, 1.0); + private static final Truthness FALSE_TRUTHNESS_BETTER = new Truthness(C_BETTER, 1.0); + + private static final Object MISSING = new Object(); + + private enum ComparisonType { EQUALS, GT, GTE, LT, LTE } + + public double computeDistance(String cqlQuery, Iterable> allRows) { + return 1.0 - computeHQuery(cqlQuery, allRows).getOfTrue(); + } + + private Truthness computeHQuery(String cqlQuery, Iterable> allRows) { + if (!CqlParserUtils.canParseCqlCommand(cqlQuery)) { + return FALSE_TRUTHNESS; + } + + List> rows = toList(allRows); + + 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 (Map row : rows) { + double ofTrue = calculateDistance(condition, row).getOfTrue(); + if (ofTrue >= 1.0) { + return TRUE_TRUTHNESS; + } + if (ofTrue > maxOfTrue) { + maxOfTrue = ofTrue; + } + } + + return TruthnessUtils.buildScaledTruthness(C, maxOfTrue); + } + + private Truthness calculateDistance(CqlQueryOperation op, Map row) { + if (op instanceof AndOperation) + return calculateDistanceForAnd((AndOperation) op, row); + if (op instanceof EqualsOperation) + return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.EQUALS); + if (op instanceof GreaterThanOperation) + return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.GT); + if (op instanceof GreaterThanEqualsOperation) + return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.GTE); + if (op instanceof LessThanOperation) + return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.LT); + if (op instanceof LessThanEqualsOperation) + return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.LTE); + if (op instanceof InOperation) + return calculateDistanceForIn((InOperation) op, row); + if (op instanceof ContainsOperation) + return calculateDistanceForContains((ContainsOperation) op, row); + if (op instanceof ContainsKeyOperation) + return calculateDistanceForContainsKey((ContainsKeyOperation) op, row); + + return FALSE_TRUTHNESS; + } + + private Truthness calculateDistanceForAnd(AndOperation op, Map row) { + List results = new ArrayList<>(); + for (CqlQueryOperation condition : op.getConditions()) { + results.add(calculateDistance(condition, row)); + } + return TruthnessUtils.buildAndAggregationTruthness(results.toArray(new Truthness[0])); + } + + private Truthness calculateDistanceForComparison( + ComparisonOperation op, + Map row, + ComparisonType type) { + + Object rowValue = getRowValue(row, op.getColumnName()); + Object queryValue = op.getValue(); + + if (rowValue == MISSING) rowValue = null; + + if (rowValue == null && queryValue == null) return FALSE_TRUTHNESS; + if (rowValue == null || queryValue == null) return FALSE_TRUTHNESS_BETTER; + + Truthness typeResult = compareByType(rowValue, queryValue, type); + if (typeResult.isTrue()) { + return typeResult; + } + return TruthnessUtils.buildScaledTruthness(C_BETTER, typeResult.getOfTrue()); + } + + private Truthness compareByType(Object rowVal, Object queryVal, ComparisonType type) { + if (rowVal instanceof Number && queryVal instanceof Number) + return compareNumeric(rowVal, queryVal, type); + if (rowVal instanceof String || rowVal instanceof InetAddress) + return compareString(rowVal, queryVal, type); + if (rowVal instanceof Boolean) + return compareBoolean(rowVal, queryVal, type); + if (rowVal instanceof UUID) + return compareUuid(rowVal, queryVal, type); + if (isTemporalType(rowVal)) + return compareTemporal(rowVal, queryVal, type); + if (isCqlDuration(rowVal)) + return compareDuration(rowVal, queryVal, type); + + SimpleLogger.uniqueWarn("CassandraHeuristicsCalculator: unsupported type " + + rowVal.getClass().getName() + " — returning FALSE_TRUTHNESS"); + return FALSE_TRUTHNESS; + } + + private Truthness compareNumeric(Object rowVal, Object queryVal, ComparisonType type) { + double x = ((Number) rowVal).doubleValue(); + double y = ((Number) queryVal).doubleValue(); + switch (type) { + 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: return FALSE_TRUTHNESS; + } + } + + private Truthness compareString(Object rowVal, Object queryVal, ComparisonType type) { + if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; + String a = (rowVal instanceof InetAddress) + ? ((InetAddress) rowVal).getHostAddress() + : (String) rowVal; + String b = (String) queryVal; + return TruthnessUtils.getStringEqualityTruthness(a, b); + } + + private Truthness compareBoolean(Object rowVal, Object queryVal, ComparisonType type) { + if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; + double x = ((Boolean) rowVal) ? 1.0 : 0.0; + double y = ((Boolean) queryVal) ? 1.0 : 0.0; + return TruthnessUtils.getEqualityTruthness(x, y); + } + + private Truthness compareUuid(Object rowVal, Object queryVal, ComparisonType type) { + if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; + return TruthnessUtils.getEqualityTruthness((UUID) rowVal, (UUID) queryVal); + } + + private static boolean isTemporalType(Object v) { + return v instanceof LocalDate + || v instanceof LocalTime + || v instanceof Instant; + } + + private Truthness compareTemporal(Object rowVal, Object queryVal, ComparisonType type) { + try { + double x = (double) toLong(rowVal, rowVal); + double y = (double) toLong(queryVal, rowVal); + switch (type) { + 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: return FALSE_TRUTHNESS; + } + } catch (Exception e) { + return FALSE_TRUTHNESS; + } + } + + private static long toLong(Object value, Object rowValueHint) { + if (rowValueHint instanceof LocalDate) { + if (value instanceof String) return LocalDate.parse((String) value).toEpochDay(); + return ((LocalDate) value).toEpochDay(); + } + if (rowValueHint instanceof LocalTime) { + if (value instanceof String) return LocalTime.parse((String) value).toNanoOfDay(); + return ((LocalTime) value).toNanoOfDay(); + } + if (rowValueHint instanceof Instant) { + if (value instanceof String) return Instant.parse((String) value).toEpochMilli(); + return ((Instant) value).toEpochMilli(); + } + throw new IllegalArgumentException("Unrecognized temporal type: " + rowValueHint.getClass()); + } + + private static boolean isCqlDuration(Object v) { + return v.getClass().getName() + .equals("com.datastax.oss.driver.api.core.data.CqlDuration"); + } + + private static int getDurationMonths(Object d) throws Exception { + return (int) d.getClass().getMethod("getMonths").invoke(d); + } + + private static int getDurationDays(Object d) throws Exception { + return (int) d.getClass().getMethod("getDays").invoke(d); + } + + private static long getDurationNanos(Object d) throws Exception { + return (long) d.getClass().getMethod("getNanoseconds").invoke(d); + } + + private Truthness compareDuration(Object rowVal, Object queryVal, ComparisonType type) { + if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; + try { + int rm = getDurationMonths(rowVal); + int rd = getDurationDays(rowVal); + long rn = getDurationNanos(rowVal); + + CqlDurationLiteral q = CqlDurationLiteral.parse((String) queryVal); + + return TruthnessUtils.buildAndAggregationTruthness( + TruthnessUtils.getEqualityTruthness((long) rm, (long) q.months), + TruthnessUtils.getEqualityTruthness((long) rd, (long) q.days), + TruthnessUtils.getEqualityTruthness(rn, q.nanos) + ); + } catch (Exception e) { + return FALSE_TRUTHNESS; + } + } + + private Truthness calculateDistanceForIn(InOperation op, Map row) { + Object rowValue = getRowValue(row, op.getColumnName()); + if (rowValue == MISSING) rowValue = null; + return computeEqualsAny(rowValue, op.getValues()); + } + + private Truthness calculateDistanceForContains(ContainsOperation op, + Map row) { + Object rawCol = getRowValue(row, op.getColumnName()); + if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; + // Elements are the authoritative side — pass them first so compareByType dispatches on their type. + return computeEqualsAnyElements(toElementList(rawCol), op.getValue()); + } + + private Truthness calculateDistanceForContainsKey(ContainsKeyOperation op, + Map row) { + Object rawCol = getRowValue(row, op.getColumnName()); + if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; + if (!(rawCol instanceof Map)) return FALSE_TRUTHNESS; + List keys = new ArrayList<>(((Map) rawCol).keySet()); + // Keys are the authoritative side — pass them first so compareByType dispatches on their type. + return computeEqualsAnyElements(keys, op.getValue()); + } + + /** + * Used by IN: value is the row column (authoritative type), candidates are the query IN-list values. + * compareByType dispatches on value. + */ + private Truthness computeEqualsAny(Object value, List candidates) { + if (candidates.isEmpty()) return FALSE_TRUTHNESS; + + double maxOfTrue = 0.0; + for (Object candidate : candidates) { + Truthness t = evaluateEquals(value, candidate); + if (t.isTrue()) return TRUE_TRUTHNESS; + if (t.getOfTrue() > maxOfTrue) maxOfTrue = t.getOfTrue(); + } + return new Truthness(maxOfTrue, 1.0); + } + + /** + * Used by CONTAINS / CONTAINS KEY: elements are the collection elements (authoritative type), + * queryValue is what we search for. compareByType dispatches on the element. + */ + private Truthness computeEqualsAnyElements(List elements, Object queryValue) { + if (elements.isEmpty()) return FALSE_TRUTHNESS; + + double maxOfTrue = 0.0; + for (Object element : elements) { + Truthness t = evaluateEquals(element, queryValue); + if (t.isTrue()) return TRUE_TRUTHNESS; + if (t.getOfTrue() > maxOfTrue) maxOfTrue = t.getOfTrue(); + } + return new Truthness(maxOfTrue, 1.0); + } + + private Truthness evaluateEquals(Object a, Object b) { + if (a == null && b == null) return FALSE_TRUTHNESS; + if (a == null || b == null) return FALSE_TRUTHNESS_BETTER; + Truthness raw = compareByType(a, b, ComparisonType.EQUALS); + if (raw.isTrue()) return raw; + return TruthnessUtils.buildScaledTruthness(C_BETTER, raw.getOfTrue()); + } + + private static List toElementList(Object collection) { + if (collection instanceof List) return (List) collection; + if (collection instanceof Set) return new ArrayList<>((Set) collection); + if (collection instanceof Map) return new ArrayList<>(((Map) collection).values()); + return Collections.emptyList(); + } + + private static String normalizeColumnName(String name) { + if (name == null) return null; + if (name.startsWith("\"") && name.endsWith("\"")) { + return name.substring(1, name.length() - 1).toLowerCase(); + } + return name.toLowerCase(); + } + + private Object getRowValue(Map row, String rawColumnName) { + String key = normalizeColumnName(rawColumnName); + if (!row.containsKey(key)) return MISSING; + return row.get(key); + } + + private static List> toList(Iterable> iterable) { + if (iterable instanceof List) { + return (List>) iterable; + } + List> list = new ArrayList<>(); + for (Map 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/CqlDurationLiteral.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CqlDurationLiteral.java new file mode 100644 index 0000000000..998fa453fa --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CqlDurationLiteral.java @@ -0,0 +1,101 @@ +package org.evomaster.client.java.controller.cassandra; + +import java.time.Duration; +import java.time.Period; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses a Cassandra duration string literal (as produced by CqlParserUtils) into + * its three components: months, days, and nanoseconds. + * + * Supports two formats: + * - ISO 8601: P1Y2M3DT4H5M6S + * - Standard Cassandra: 1y2mo3d4h5m6s (units may appear in any order) + */ +public class CqlDurationLiteral { + + public final int months; + public final int days; + public final long nanos; + + private CqlDurationLiteral(int months, int days, long nanos) { + this.months = months; + this.days = days; + this.nanos = nanos; + } + + public static CqlDurationLiteral parse(String text) { + if (text == null || text.isEmpty()) { + throw new IllegalArgumentException("Empty duration literal"); + } + String t = text.trim(); + if (t.startsWith("P") || t.startsWith("p")) { + return parseIso(t); + } + return parseCassandra(t); + } + + private static CqlDurationLiteral parseIso(String text) { + // Split on T (or t) to separate date and time parts + String upper = text.toUpperCase(); + int tIndex = upper.indexOf('T'); + + int months = 0; + int days = 0; + long nanos = 0L; + + if (tIndex < 0) { + // Date part only: P1Y2M3D + Period p = Period.parse(upper); + months = p.getYears() * 12 + p.getMonths(); + days = p.getDays(); + } else { + String datePart = upper.substring(0, tIndex); // e.g. "P1Y2M3D" + String timePart = upper.substring(tIndex); // e.g. "T4H5M6S" + + if (datePart.length() > 1) { + Period p = Period.parse(datePart); + months = p.getYears() * 12 + p.getMonths(); + days = p.getDays(); + } + if (timePart.length() > 1) { + Duration d = Duration.parse("P" + timePart); + nanos = d.toNanos(); + } + } + return new CqlDurationLiteral(months, days, nanos); + } + + // Matches: optional digits followed by a unit token. + // Units (longest-first to avoid 'm' consuming 'mo' or 'ms' consuming 'm'): + // mo, ms, us, µs, ns, y, d, h, m, s + private static final Pattern TOKEN = + Pattern.compile("(\\d+)(mo|ms|µs|us|ns|y|d|h|m|s)", Pattern.CASE_INSENSITIVE); + + private static CqlDurationLiteral parseCassandra(String text) { + int months = 0; + int days = 0; + long nanos = 0L; + + Matcher m = TOKEN.matcher(text); + while (m.find()) { + long value = Long.parseLong(m.group(1)); + String unit = m.group(2).toLowerCase(); + switch (unit) { + case "y": months += (int)(value * 12); break; + case "mo": months += (int) value; break; + case "d": days += (int) value; break; + case "h": nanos += value * 3_600_000_000_000L; break; + case "m": nanos += value * 60_000_000_000L; break; + case "s": nanos += value * 1_000_000_000L; break; + case "ms": nanos += value * 1_000_000L; break; + case "us": + case "µs": nanos += value * 1_000L; break; + case "ns": nanos += value; break; + default: break; + } + } + return new CqlDurationLiteral(months, days, nanos); + } +} \ No newline at end of file diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java new file mode 100644 index 0000000000..32f877e971 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java @@ -0,0 +1,605 @@ +package org.evomaster.client.java.controller.cassandra; + +import org.evomaster.client.java.distance.heuristics.DistanceHelper; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class CassandraHeuristicsCalculatorTest { + + private final CassandraHeuristicsCalculator calc = new CassandraHeuristicsCalculator(); + + private static Map row(Object... kv) { + Map m = new LinkedHashMap<>(); + for (int i = 0; i < kv.length; i += 2) m.put((String) kv[i], kv[i + 1]); + return m; + } + + @SafeVarargs + private final double dist(String cql, Map... rows) { + return calc.computeDistance(cql, Arrays.asList(rows)); + } + + private double distNoRows(String cql) { + return calc.computeDistance(cql, Collections.emptyList()); + } + + private static final double DELTA = 1e-9; + + @Test + void noWhere_emptyTable_maxDistance() { + // H-Table(0 rows) = FALSE_TRUTHNESS → ofTrue = C → distance = 1 - C + assertEquals(1.0 - DistanceHelper.H_NOT_NULL, distNoRows("SELECT * FROM t"), DELTA); + } + + @Test + void noWhere_nonEmptyTable_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t", row("id", 1L)), DELTA); + } + + @Test + void noWhere_multipleRows_zeroDistance() { + assertEquals(0.0, + dist("SELECT * FROM t", row("id", 1L), row("id", 2L)), + DELTA); + } + + @Test + void where_emptyTable_highDistance() { + double d = distNoRows("SELECT * FROM t WHERE id = 1"); + // andAggregation(FALSE, FALSE) → ofTrue ≈ C → distance ≈ 1 - C + assertTrue(d > 0.5); + } + + // Numeric equality (=) + + @Test + void numericEquals_long_exactMatch() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col = 42", row("col", 42L)), DELTA); + } + + @Test + void numericEquals_integer_exactMatch() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col = 5", row("col", 5)), DELTA); + } + + @Test + void numericEquals_double_exactMatch() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col = 3.14", row("col", 3.14)), DELTA); + } + + @Test + void numericEquals_float_exactMatch() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col = 1", row("col", 1.0f)), DELTA); + } + + @Test + void numericEquals_noMatch_nonZero() { + double d = dist("SELECT * FROM t WHERE col = 42", row("col", 43L)); + assertTrue(d > 0.0 && d < 1.0); + } + + @Test + void numericEquals_closerValueGivesLowerDistance() { + double dClose = dist("SELECT * FROM t WHERE col = 10", row("col", 11L)); + double dFar = dist("SELECT * FROM t WHERE col = 10", row("col", 100L)); + assertTrue(dClose < dFar); + } + + // Numeric ordering (>, >=, <, <=) + + @Test + void numericGT_satisfied_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col > 5", row("col", 10L)), DELTA); + } + + @Test + void numericGT_boundary_notSatisfied() { + assertTrue(dist("SELECT * FROM t WHERE col > 10", row("col", 10L)) > 0.0); + } + + @Test + void numericGT_notSatisfied_nonZero() { + assertTrue(dist("SELECT * FROM t WHERE col > 10", row("col", 5L)) > 0.0); + } + + @Test + void numericGTE_boundary_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col >= 10", row("col", 10L)), DELTA); + } + + @Test + void numericGTE_notSatisfied_nonZero() { + assertTrue(dist("SELECT * FROM t WHERE col >= 10", row("col", 9L)) > 0.0); + } + + @Test + void numericLT_satisfied_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col < 10", row("col", 5L)), DELTA); + } + + @Test + void numericLT_boundary_notSatisfied() { + assertTrue(dist("SELECT * FROM t WHERE col < 10", row("col", 10L)) > 0.0); + } + + @Test + void numericLTE_boundary_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col <= 10", row("col", 10L)), DELTA); + } + + @Test + void numericLTE_notSatisfied_nonZero() { + assertTrue(dist("SELECT * FROM t WHERE col <= 10", row("col", 11L)) > 0.0); + } + + @Test + void numericOrdering_closerToThresholdGivesBetterScore() { + // Col must be > 100; row with 99 is closer than row with 50 + double dClose = dist("SELECT * FROM t WHERE col > 100", row("col", 99L)); + double dFar = dist("SELECT * FROM t WHERE col > 100", row("col", 50L)); + assertTrue(dClose < dFar); + } + + // String equality + + @Test + void stringEquals_exactMatch_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE s = 'hello'", row("s", "hello")), DELTA); + } + + @Test + void stringEquals_differentString_nonZero() { + double d = dist("SELECT * FROM t WHERE s = 'hello'", row("s", "world")); + assertTrue(d > 0.0 && d < 1.0); + } + + @Test + void stringEquals_prefixCloserThanUnrelated() { + double dClose = dist("SELECT * FROM t WHERE s = 'hello'", row("s", "hell")); + double dFar = dist("SELECT * FROM t WHERE s = 'hello'", row("s", "xyz")); + assertTrue(dClose < dFar); + } + + @Test + void stringEquals_emptyString_exactMatch() { + assertEquals(0.0, dist("SELECT * FROM t WHERE s = ''", row("s", "")), DELTA); + } + + // Boolean equality + + @Test + void booleanEquals_trueMatch_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE active = true", row("active", true)), DELTA); + } + + @Test + void booleanEquals_falseMatch_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE active = false", row("active", false)), DELTA); + } + + @Test + void booleanEquals_mismatch_nonZero() { + double d = dist("SELECT * FROM t WHERE active = true", row("active", false)); + assertTrue(d > 0.0 && d < 1.0); + } + + // UUID equality + + private static final UUID UUID_A = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"); + private static final UUID UUID_B = UUID.fromString("550e8400-e29b-41d4-a716-446655440001"); + private static final UUID UUID_C = UUID.fromString("00000000-0000-0000-0000-000000000000"); + + @Test + void uuidEquals_sameUuid_zeroDistance() { + assertEquals(0.0, + dist("SELECT * FROM t WHERE id = 550e8400-e29b-41d4-a716-446655440000", + row("id", UUID_A)), + DELTA); + } + + @Test + void uuidEquals_differentUuid_nonZero() { + double d = dist("SELECT * FROM t WHERE id = 550e8400-e29b-41d4-a716-446655440000", + row("id", UUID_B)); + assertTrue(d > 0.0 && d < 1.0); + } + + @Test + void uuidEquals_veryDifferentUuid_higherDistanceThanCloseUuid() { + double dClose = dist("SELECT * FROM t WHERE id = 550e8400-e29b-41d4-a716-446655440000", + row("id", UUID_B)); // differs in one bit of the last byte + double dFar = dist("SELECT * FROM t WHERE id = 550e8400-e29b-41d4-a716-446655440000", + row("id", UUID_C)); // all-zero UUID — many bits differ + assertTrue(dClose < dFar); + } + + // AND operator + + @Test + void and_bothSatisfied_zeroDistance() { + assertEquals(0.0, + dist("SELECT * FROM t WHERE a = 1 AND b = 'x'", + row("a", 1L, "b", "x")), + DELTA); + } + + @Test + void and_neitherSatisfied_highDistance() { + double d = dist("SELECT * FROM t WHERE a = 1 AND b = 'x'", + row("a", 999L, "b", "zzz")); + assertTrue(d > 0.0); + } + + @Test + void and_oneSatisfied_betterThanNoneSatisfied() { + double dOne = dist("SELECT * FROM t WHERE a = 1 AND b = 'x'", + row("a", 1L, "b", "zzz")); + double dNone = dist("SELECT * FROM t WHERE a = 1 AND b = 'x'", + row("a", 999L, "b", "zzz")); + assertTrue(dOne < dNone); + } + + @Test + void and_threeConditions_allSatisfied() { + assertEquals(0.0, + dist("SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3", + row("a", 1L, "b", 2L, "c", 3L)), + DELTA); + } + + // IN operator + + @Test + void in_valueInList_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col IN (1, 2, 3)", row("col", 2L)), DELTA); + } + + @Test + void in_valueFirstInList_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col IN (1, 2, 3)", row("col", 1L)), DELTA); + } + + @Test + void in_valueLastInList_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col IN (1, 2, 3)", row("col", 3L)), DELTA); + } + + @Test + void in_valueNotInList_nonZero() { + assertTrue(dist("SELECT * FROM t WHERE col IN (10, 20)", row("col", 100L)) > 0.0); + } + + @Test + void in_closerCandidateGivesBetterScore() { + double dClose = dist("SELECT * FROM t WHERE col IN (10, 20)", row("col", 11L)); + double dFar = dist("SELECT * FROM t WHERE col IN (10, 20)", row("col", 100L)); + assertTrue(dClose < dFar); + } + + @Test + void in_singleElementList_match_zeroDistance() { + assertEquals(0.0, dist("SELECT * FROM t WHERE col IN (42)", row("col", 42L)), DELTA); + } + + // CONTAINS operator + + @Test + void contains_list_valuePresent_zeroDistance() { + assertEquals(0.0, + dist("SELECT * FROM t WHERE tags CONTAINS 2", + row("tags", Arrays.asList(1L, 2L, 3L))), + DELTA); + } + + @Test + void contains_list_valueMissing_nonZero() { + double d = dist("SELECT * FROM t WHERE tags CONTAINS 5", + row("tags", Arrays.asList(10L, 20L))); + assertTrue(d > 0.0); + } + + @Test + void contains_list_closerElementGivesBetterScore() { + double dClose = dist("SELECT * FROM t WHERE tags CONTAINS 10", + row("tags", Arrays.asList(11L))); + double dFar = dist("SELECT * FROM t WHERE tags CONTAINS 10", + row("tags", Arrays.asList(100L))); + assertTrue(dClose < dFar); + } + + @Test + void contains_set_valuePresent_zeroDistance() { + Set s = new LinkedHashSet<>(Arrays.asList(1L, 2L, 3L)); + assertEquals(0.0, dist("SELECT * FROM t WHERE col CONTAINS 2", row("col", s)), DELTA); + } + + @Test + void contains_map_valueInValues_zeroDistance() { + Map scores = new LinkedHashMap<>(); + scores.put("alice", 10L); + scores.put("bob", 20L); + assertEquals(0.0, + dist("SELECT * FROM t WHERE scores CONTAINS 10", row("scores", scores)), + DELTA); + } + + @Test + void contains_map_valueMissing_nonZero() { + Map scores = new LinkedHashMap<>(); + scores.put("alice", 10L); + double d = dist("SELECT * FROM t WHERE scores CONTAINS 99", row("scores", scores)); + assertTrue(d > 0.0); + } + + @Test + void contains_nullColumn_nonZeroDistance() { + assertTrue(dist("SELECT * FROM t WHERE col CONTAINS 1", row("col", null)) > 0.0); + } + + @Test + void contains_missingColumn_nonZeroDistance() { + assertTrue(dist("SELECT * FROM t WHERE col CONTAINS 1", row("other", 99L)) > 0.0); + } + + @Test + void contains_stringList_exactMatch_zeroDistance() { + assertEquals(0.0, + dist("SELECT * FROM t WHERE tags CONTAINS 'hello'", + row("tags", Arrays.asList("hello", "world"))), + DELTA); + } + + @Test + void contains_stringList_closerStringGivesBetterScore() { + double dClose = dist("SELECT * FROM t WHERE tags CONTAINS 'hello'", + row("tags", Arrays.asList("hell"))); + double dFar = dist("SELECT * FROM t WHERE tags CONTAINS 'hello'", + row("tags", Arrays.asList("xyz"))); + assertTrue(dClose < dFar); + } + + // CONTAINS KEY operator + + @Test + void containsKey_keyExists_zeroDistance() { + Map m = new LinkedHashMap<>(); + m.put("alice", 10L); + assertEquals(0.0, + dist("SELECT * FROM t WHERE meta CONTAINS KEY 'alice'", row("meta", m)), + DELTA); + } + + @Test + void containsKey_keyAbsent_nonZero() { + Map m = new LinkedHashMap<>(); + m.put("alice", 10L); + double d = dist("SELECT * FROM t WHERE meta CONTAINS KEY 'bob'", row("meta", m)); + assertTrue(d > 0.0); + } + + @Test + void containsKey_closerKeyGivesBetterScore() { + Map m = new LinkedHashMap<>(); + m.put("bob", 10L); // close to target "alice" (different string but not totally different) + double dClose = dist("SELECT * FROM t WHERE meta CONTAINS KEY 'alice'", row("meta", m)); + + Map m2 = new LinkedHashMap<>(); + m2.put("zzzzz", 10L); + double dFar = dist("SELECT * FROM t WHERE meta CONTAINS KEY 'alice'", row("meta", m2)); + + // "bob" vs "alice" vs "zzzzz" vs "alice" — both are non-matches but distances differ + // We only assert that both are > 0 (ordering may vary depending on leftAlignmentDistance) + assertTrue(dClose > 0.0); + assertTrue(dFar > 0.0); + } + + @Test + void containsKey_nullColumn_nonZeroDistance() { + assertTrue(dist("SELECT * FROM t WHERE col CONTAINS KEY 'x'", row("col", null)) > 0.0); + } + + @Test + void containsKey_nonMapColumn_nonZeroDistance() { + assertTrue(dist("SELECT * FROM t WHERE col CONTAINS KEY 'x'", + row("col", Arrays.asList("a", "b"))) > 0.0); + } + + // NULL handling + + @Test + void nullRowValue_returnsHighDistance() { + double d = dist("SELECT * FROM t WHERE col = 1", row("col", null)); + assertTrue(d > 0.0); + } + + @Test + void missingColumn_returnsHighDistance() { + double d = dist("SELECT * FROM t WHERE col = 1", row("other", 99L)); + assertTrue(d > 0.0); + } + + @Test + void nullCell_notWorseThanMissingColumn() { + // NULL cell → FALSE_TRUTHNESS_BETTER (C_BETTER=0.15 > C=0.1) + // Missing column → FALSE_TRUTHNESS (C=0.1) + // So NULL cell ofTrue > missing column ofTrue → distance(null) < distance(missing) + double dNull = dist("SELECT * FROM t WHERE col = 1", row("col", null)); + double dMissing = dist("SELECT * FROM t WHERE col = 1", row("other", 99L)); + assertTrue(dNull <= dMissing); + } + + @Test + void bothNullQueryAndRow_falseDistance() { + // Both NULL → FALSE_TRUTHNESS, not zero distance + double d = dist("SELECT * FROM t WHERE col = NULL", row("col", null)); + assertTrue(d > 0.0); + } + + // Multi-row scenarios + + @Test + void multiRow_oneMatchingRow_zeroDistance() { + assertEquals(0.0, + dist("SELECT * FROM t WHERE col = 5", + row("col", 1L), row("col", 5L), row("col", 10L)), + DELTA); + } + + @Test + void multiRow_noMatch_bestRowDrivesScore() { + double dWithClose = dist("SELECT * FROM t WHERE col = 10", + row("col", 11L), row("col", 100L)); + double dAllFar = dist("SELECT * FROM t WHERE col = 10", + row("col", 99L), row("col", 100L)); + assertTrue(dWithClose < dAllFar); + } + + @Test + void multiRow_earlyExitOnMatch() { + // Matching row is first — should short-circuit and not evaluate the rest + assertEquals(0.0, + dist("SELECT * FROM t WHERE col = 1", + row("col", 1L), row("col", 999L), row("col", 999L)), + DELTA); + } + + // Quoted column names + + @Test + void quotedColumnName_match_zeroDistance() { + // Parser preserves quotes; normalizeColumnName strips them and lowercases + assertEquals(0.0, + dist("SELECT * FROM t WHERE \"myCol\" = 42", row("mycol", 42L)), + DELTA); + } + + @Test + void quotedColumnName_mismatch_nonZero() { + double d = dist("SELECT * FROM t WHERE \"myCol\" = 42", row("mycol", 99L)); + assertTrue(d > 0.0); + } + + // Temporal types — date (LocalDate) + + @Test + void dateEquals_exactMatch_zeroDistance() { + LocalDate d = LocalDate.of(2023, 1, 15); + assertEquals(0.0, + dist("SELECT * FROM t WHERE d = '2023-01-15'", row("d", d)), + DELTA); + } + + @Test + void dateEquals_differentDay_nonZero() { + LocalDate d = LocalDate.of(2023, 1, 16); + double distance = dist("SELECT * FROM t WHERE d = '2023-01-15'", row("d", d)); + assertTrue(distance > 0.0 && distance < 1.0); + } + + @Test + void dateLT_satisfied_zeroDistance() { + LocalDate d = LocalDate.of(2022, 12, 31); + assertEquals(0.0, + dist("SELECT * FROM t WHERE d < '2023-01-01'", row("d", d)), + DELTA); + } + + @Test + void dateGT_satisfied_zeroDistance() { + LocalDate d = LocalDate.of(2023, 6, 1); + assertEquals(0.0, + dist("SELECT * FROM t WHERE d > '2023-01-01'", row("d", d)), + DELTA); + } + + @Test + void dateGTE_boundary_zeroDistance() { + LocalDate d = LocalDate.of(2023, 1, 1); + assertEquals(0.0, + dist("SELECT * FROM t WHERE d >= '2023-01-01'", row("d", d)), + DELTA); + } + + @Test + void dateLTE_boundary_zeroDistance() { + LocalDate d = LocalDate.of(2023, 1, 1); + assertEquals(0.0, + dist("SELECT * FROM t WHERE d <= '2023-01-01'", row("d", d)), + DELTA); + } + + @Test + void date_closerDayGivesBetterScore() { + double dClose = dist("SELECT * FROM t WHERE d = '2023-01-15'", + row("d", LocalDate.of(2023, 1, 16))); // 1 day away + double dFar = dist("SELECT * FROM t WHERE d = '2023-01-15'", + row("d", LocalDate.of(2023, 6, 1))); // months away + assertTrue(dClose < dFar); + } + + // Temporal types — time (LocalTime) + + @Test + void timeEquals_exactMatch_zeroDistance() { + LocalTime t = LocalTime.of(14, 30, 0); + assertEquals(0.0, + dist("SELECT * FROM t WHERE t = '14:30:00'", row("t", t)), + DELTA); + } + + @Test + void timeGT_satisfied_zeroDistance() { + LocalTime t = LocalTime.of(15, 0, 0); + assertEquals(0.0, + dist("SELECT * FROM t WHERE t > '14:30:00'", row("t", t)), + DELTA); + } + + @Test + void timeLT_notSatisfied_nonZero() { + LocalTime t = LocalTime.of(15, 0, 0); + double d = dist("SELECT * FROM t WHERE t < '14:30:00'", row("t", t)); + assertTrue(d > 0.0); + } + + // Temporal types — timestamp (Instant) + + @Test + void timestampEquals_exactMatch_zeroDistance() { + Instant ts = Instant.parse("2023-01-15T14:30:00Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2023-01-15T14:30:00Z'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_differentTime_nonZero() { + Instant ts = Instant.parse("2023-01-15T15:00:00Z"); + double d = dist("SELECT * FROM t WHERE ts = '2023-01-15T14:30:00Z'", row("ts", ts)); + assertTrue(d > 0.0 && d < 1.0); + } + + @Test + void timestampGTE_boundary_zeroDistance() { + Instant ts = Instant.parse("2023-01-15T00:00:00Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts >= '2023-01-15T00:00:00Z'", row("ts", ts)), + DELTA); + } + + @Test + void timestamp_closerTimeGivesBetterScore() { + Instant target = Instant.parse("2023-01-15T12:00:00Z"); + double dClose = dist("SELECT * FROM t WHERE ts = '2023-01-15T12:00:00Z'", + row("ts", Instant.parse("2023-01-15T12:01:00Z"))); // 1 minute away + double dFar = dist("SELECT * FROM t WHERE ts = '2023-01-15T12:00:00Z'", + row("ts", Instant.parse("2023-01-16T12:00:00Z"))); // 1 day away + assertTrue(dClose < dFar); + } +} \ No newline at end of file diff --git a/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java b/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java index ef172d9590..4df33ec882 100644 --- a/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java +++ b/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java @@ -319,4 +319,15 @@ public static Truthness getEqualityTruthness(UUID left, UUID right) { ); } + public static Truthness getStringEqualityTruthness(String a, String b) { + Objects.requireNonNull(a); + Objects.requireNonNull(b); + if (a.equals(b)) { + return new Truthness(1.0d, DistanceHelper.H_NOT_NULL); + } + long dist = DistanceHelper.getLeftAlignmentDistance(a, b); + double ofTrue = DistanceHelper.heuristicFromScaledDistanceWithBase(DistanceHelper.H_NOT_NULL, (double) dist); + return new Truthness(ofTrue, 1.0d); + } + } From 6b6ad91fef15a8c8286fb9ebcd115500e1870c33 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 30 Jun 2026 17:10:41 -0300 Subject: [PATCH 2/8] Fix comparison of time-related values --- .../CassandraHeuristicsCalculator.java | 56 ++++++------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java index 96522ae85a..0573e1aa1a 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java @@ -10,7 +10,9 @@ import java.net.InetAddress; import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneOffset; import java.util.*; public class CassandraHeuristicsCalculator { @@ -204,12 +206,16 @@ private Truthness compareTemporal(Object rowVal, Object queryVal, ComparisonType private static long toLong(Object value, Object rowValueHint) { if (rowValueHint instanceof LocalDate) { - if (value instanceof String) return LocalDate.parse((String) value).toEpochDay(); - return ((LocalDate) value).toEpochDay(); + LocalDate d = (value instanceof String) + ? LocalDate.parse((String) value) + : (LocalDate) value; + return d.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); } if (rowValueHint instanceof LocalTime) { - if (value instanceof String) return LocalTime.parse((String) value).toNanoOfDay(); - return ((LocalTime) value).toNanoOfDay(); + LocalTime t = (value instanceof String) + ? LocalTime.parse((String) value) + : (LocalTime) value; + return LocalDateTime.of(LocalDate.of(1970, 1, 1), t).toInstant(ZoneOffset.UTC).toEpochMilli(); } if (rowValueHint instanceof Instant) { if (value instanceof String) return Instant.parse((String) value).toEpochMilli(); @@ -257,15 +263,14 @@ private Truthness compareDuration(Object rowVal, Object queryVal, ComparisonType private Truthness calculateDistanceForIn(InOperation op, Map row) { Object rowValue = getRowValue(row, op.getColumnName()); if (rowValue == MISSING) rowValue = null; - return computeEqualsAny(rowValue, op.getValues()); + return any(rowValue, op.getValues()); } private Truthness calculateDistanceForContains(ContainsOperation op, Map row) { Object rawCol = getRowValue(row, op.getColumnName()); if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; - // Elements are the authoritative side — pass them first so compareByType dispatches on their type. - return computeEqualsAnyElements(toElementList(rawCol), op.getValue()); + return any(op.getValue(), toElementList(rawCol)); } private Truthness calculateDistanceForContainsKey(ContainsKeyOperation op, @@ -274,40 +279,15 @@ private Truthness calculateDistanceForContainsKey(ContainsKeyOperation op, if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; if (!(rawCol instanceof Map)) return FALSE_TRUTHNESS; List keys = new ArrayList<>(((Map) rawCol).keySet()); - // Keys are the authoritative side — pass them first so compareByType dispatches on their type. - return computeEqualsAnyElements(keys, op.getValue()); + return any(op.getValue(), keys); } - /** - * Used by IN: value is the row column (authoritative type), candidates are the query IN-list values. - * compareByType dispatches on value. - */ - private Truthness computeEqualsAny(Object value, List candidates) { + private Truthness any(Object value, List candidates) { if (candidates.isEmpty()) return FALSE_TRUTHNESS; - - double maxOfTrue = 0.0; - for (Object candidate : candidates) { - Truthness t = evaluateEquals(value, candidate); - if (t.isTrue()) return TRUE_TRUTHNESS; - if (t.getOfTrue() > maxOfTrue) maxOfTrue = t.getOfTrue(); - } - return new Truthness(maxOfTrue, 1.0); - } - - /** - * Used by CONTAINS / CONTAINS KEY: elements are the collection elements (authoritative type), - * queryValue is what we search for. compareByType dispatches on the element. - */ - private Truthness computeEqualsAnyElements(List elements, Object queryValue) { - if (elements.isEmpty()) return FALSE_TRUTHNESS; - - double maxOfTrue = 0.0; - for (Object element : elements) { - Truthness t = evaluateEquals(element, queryValue); - if (t.isTrue()) return TRUE_TRUTHNESS; - if (t.getOfTrue() > maxOfTrue) maxOfTrue = t.getOfTrue(); - } - return new Truthness(maxOfTrue, 1.0); + Truthness[] truthnesses = candidates.stream() + .map(candidate -> evaluateEquals(value, candidate)) + .toArray(Truthness[]::new); + return TruthnessUtils.buildOrAggregationTruthness(truthnesses); } private Truthness evaluateEquals(Object a, Object b) { From 059e9a5d2ea5fd167359a9500ebf389240cc45e4 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 30 Jun 2026 17:33:54 -0300 Subject: [PATCH 3/8] Improve test class coverage --- client-java/controller/pom.xml | 5 + .../CassandraHeuristicsCalculatorTest.java | 102 ++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/client-java/controller/pom.xml b/client-java/controller/pom.xml index 738d11e897..3ba75d47be 100644 --- a/client-java/controller/pom.xml +++ b/client-java/controller/pom.xml @@ -193,6 +193,11 @@ libthrift test + + org.apache.cassandra + java-driver-core + test + org.mongodb bson diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java index 32f877e971..ee932abd16 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java @@ -3,6 +3,8 @@ import org.evomaster.client.java.distance.heuristics.DistanceHelper; import org.junit.jupiter.api.Test; +import com.datastax.oss.driver.api.core.data.CqlDuration; +import java.net.InetAddress; import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; @@ -219,6 +221,68 @@ void uuidEquals_veryDifferentUuid_higherDistanceThanCloseUuid() { assertTrue(dClose < dFar); } + // Duration equality (CqlDuration) + + @Test + void durationEquals_exactMatch_zeroDistance() { + // 1y3d4h = months:12, days:3, nanos:4h in nanoseconds + CqlDuration dur = CqlDuration.newInstance(12, 3, 14_400_000_000_000L); + assertEquals(0.0, + dist("SELECT * FROM t WHERE d = 1y3d4h", row("d", dur)), + DELTA); + } + + @Test + void durationEquals_differentMonths_nonZero() { + CqlDuration dur = CqlDuration.newInstance(1, 0, 0); // 1mo + double d = dist("SELECT * FROM t WHERE d = 2mo", row("d", dur)); + assertTrue(d > 0.0 && d < 1.0); + } + + @Test + void durationEquals_allComponentsDiffer_nonZero() { + CqlDuration dur = CqlDuration.newInstance(2, 1, 0); + double d = dist("SELECT * FROM t WHERE d = 1mo3d", row("d", dur)); + assertTrue(d > 0.0 && d < 1.0); + } + + @Test + void durationEquals_closerMonthsGivesBetterScore() { + CqlDuration close = CqlDuration.newInstance(1, 0, 0); // 1 month away from 2mo + CqlDuration far = CqlDuration.newInstance(10, 0, 0); // 8 months away from 2mo + double dClose = dist("SELECT * FROM t WHERE d = 2mo", row("d", close)); + double dFar = dist("SELECT * FROM t WHERE d = 2mo", row("d", far)); + assertTrue(dClose < dFar); + } + + // InetAddress equality + + @Test + void inetEquals_exactMatch_zeroDistance() throws Exception { + InetAddress addr = InetAddress.getByName("192.168.1.1"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ip = '192.168.1.1'", row("ip", addr)), + DELTA); + } + + @Test + void inetEquals_differentAddress_nonZero() throws Exception { + InetAddress addr = InetAddress.getByName("192.168.1.2"); + double d = dist("SELECT * FROM t WHERE ip = '192.168.1.1'", row("ip", addr)); + assertTrue(d > 0.0 && d < 1.0); + } + + @Test + void inetEquals_closerAddressGivesBetterScore() throws Exception { + // '192.168.1.1' vs '192.168.1.2' — one character differs + // '192.168.1.1' vs '10.0.0.1' — many characters differ + double dClose = dist("SELECT * FROM t WHERE ip = '192.168.1.1'", + row("ip", InetAddress.getByName("192.168.1.2"))); + double dFar = dist("SELECT * FROM t WHERE ip = '192.168.1.1'", + row("ip", InetAddress.getByName("10.0.0.1"))); + assertTrue(dClose < dFar); + } + // AND operator @Test @@ -287,6 +351,16 @@ void in_singleElementList_match_zeroDistance() { assertEquals(0.0, dist("SELECT * FROM t WHERE col IN (42)", row("col", 42L)), DELTA); } + @Test + void in_nullRowValue_nonZeroDistance() { + assertTrue(dist("SELECT * FROM t WHERE col IN (1, 2, 3)", row("col", null)) > 0.0); + } + + @Test + void in_missingColumn_nonZeroDistance() { + assertTrue(dist("SELECT * FROM t WHERE col IN (1, 2, 3)", row("other", 99L)) > 0.0); + } + // CONTAINS operator @Test @@ -568,6 +642,34 @@ void timeLT_notSatisfied_nonZero() { assertTrue(d > 0.0); } + @Test + void timeGTE_boundary_zeroDistance() { + LocalTime t = LocalTime.of(14, 30, 0); + assertEquals(0.0, + dist("SELECT * FROM t WHERE t >= '14:30:00'", row("t", t)), + DELTA); + } + + @Test + void timeGTE_notSatisfied_nonZero() { + LocalTime t = LocalTime.of(14, 0, 0); + assertTrue(dist("SELECT * FROM t WHERE t >= '14:30:00'", row("t", t)) > 0.0); + } + + @Test + void timeLTE_boundary_zeroDistance() { + LocalTime t = LocalTime.of(14, 30, 0); + assertEquals(0.0, + dist("SELECT * FROM t WHERE t <= '14:30:00'", row("t", t)), + DELTA); + } + + @Test + void timeLTE_notSatisfied_nonZero() { + LocalTime t = LocalTime.of(15, 0, 0); + assertTrue(dist("SELECT * FROM t WHERE t <= '14:30:00'", row("t", t)) > 0.0); + } + // Temporal types — timestamp (Instant) @Test From 8455eb6e810c4f5357c833def81ab9d496519284 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 30 Jun 2026 18:58:28 -0300 Subject: [PATCH 4/8] Add support for integer constants in time-related types --- .../CassandraHeuristicsCalculator.java | 49 ++++++-- .../CassandraHeuristicsCalculatorTest.java | 117 ++++++++++++++++++ 2 files changed, 158 insertions(+), 8 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java index 0573e1aa1a..16fffe12c3 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java @@ -12,7 +12,11 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; import java.util.*; public class CassandraHeuristicsCalculator { @@ -206,24 +210,53 @@ private Truthness compareTemporal(Object rowVal, Object queryVal, ComparisonType private static long toLong(Object value, Object rowValueHint) { if (rowValueHint instanceof LocalDate) { - LocalDate d = (value instanceof String) - ? LocalDate.parse((String) value) - : (LocalDate) value; + LocalDate d = (value instanceof Long) ? LocalDate.ofEpochDay((Long) value) + : (value instanceof String) ? LocalDate.parse((String) value) + : (LocalDate) value; return d.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); } if (rowValueHint instanceof LocalTime) { - LocalTime t = (value instanceof String) - ? LocalTime.parse((String) value) - : (LocalTime) value; + LocalTime t = (value instanceof Long) ? LocalTime.ofNanoOfDay((Long) value) + : (value instanceof String) ? LocalTime.parse((String) value) + : (LocalTime) value; return LocalDateTime.of(LocalDate.of(1970, 1, 1), t).toInstant(ZoneOffset.UTC).toEpochMilli(); } if (rowValueHint instanceof Instant) { - if (value instanceof String) return Instant.parse((String) value).toEpochMilli(); - return ((Instant) value).toEpochMilli(); + if (value instanceof Long) return (Long) value; + if (value instanceof Instant) return ((Instant) value).toEpochMilli(); + if (value instanceof String) return parseTimestampString((String) value).toEpochMilli(); + throw new IllegalArgumentException("Unexpected timestamp value type: " + value.getClass()); } throw new IllegalArgumentException("Unrecognized temporal type: " + rowValueHint.getClass()); } + 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"); + + private static Instant parseTimestampString(String s) { + try { return Instant.parse(s); } catch (DateTimeParseException ignored) {} + for (DateTimeFormatter formatter : TIMESTAMP_FORMATTERS) { + try { return OffsetDateTime.parse(s, formatter).toInstant(); } catch (DateTimeParseException ignored) {} + } + // date-only with offset ("2011-02-03+0000"): OffsetDateTime.parse fails without a time + // component, so extract LocalDate and ZoneOffset from the TemporalAccessor directly. + try { + TemporalAccessor accessor = DATE_WITH_OFFSET.parse(s); + return LocalDate.from(accessor).atStartOfDay(ZoneOffset.from(accessor)).toInstant(); + } catch (DateTimeParseException ignored) {} + // date-only without offset ("2011-02-03"): treat as midnight UTC + try { return LocalDate.parse(s).atStartOfDay(ZoneOffset.UTC).toInstant(); } catch (DateTimeParseException ignored) {} + throw new IllegalArgumentException("Cannot parse timestamp string: " + s); + } + private static boolean isCqlDuration(Object v) { return v.getClass().getName() .equals("com.datastax.oss.driver.api.core.data.CqlDuration"); diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java index ee932abd16..099b0a1a15 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java @@ -569,6 +569,21 @@ void dateEquals_exactMatch_zeroDistance() { DELTA); } + @Test + void dateEquals_integerDaysSinceEpoch_zeroDistance() { + LocalDate d = LocalDate.of(2023, 1, 15); + assertEquals(0.0, + dist("SELECT * FROM t WHERE d = " + d.toEpochDay(), row("d", d)), + DELTA); + } + + @Test + void dateEquals_integerDaysSinceEpoch_nonZero() { + LocalDate d = LocalDate.of(2023, 1, 16); + double distance = dist("SELECT * FROM t WHERE d = " + LocalDate.of(2023, 1, 15).toEpochDay(), row("d", d)); + assertTrue(distance > 0.0 && distance < 1.0); + } + @Test void dateEquals_differentDay_nonZero() { LocalDate d = LocalDate.of(2023, 1, 16); @@ -619,6 +634,21 @@ void date_closerDayGivesBetterScore() { // Temporal types — time (LocalTime) + @Test + void timeEquals_integerNanosSinceMidnight_zeroDistance() { + LocalTime t = LocalTime.of(14, 30, 0); + assertEquals(0.0, + dist("SELECT * FROM t WHERE t = " + t.toNanoOfDay(), row("t", t)), + DELTA); + } + + @Test + void timeEquals_integerNanosSinceMidnight_nonZero() { + LocalTime t = LocalTime.of(15, 0, 0); + double d = dist("SELECT * FROM t WHERE t = " + LocalTime.of(14, 30, 0).toNanoOfDay(), row("t", t)); + assertTrue(d > 0.0 && d < 1.0); + } + @Test void timeEquals_exactMatch_zeroDistance() { LocalTime t = LocalTime.of(14, 30, 0); @@ -704,4 +734,91 @@ void timestamp_closerTimeGivesBetterScore() { row("ts", Instant.parse("2023-01-16T12:00:00Z"))); // 1 day away assertTrue(dClose < dFar); } + + // Temporal types — timestamp additional constant formats + + @Test + void timestampEquals_integerEpochMs_zeroDistance() { + Instant ts = Instant.ofEpochMilli(1_299_038_700_000L); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = 1299038700000", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_integerEpochMs_nonZero() { + Instant ts = Instant.ofEpochMilli(1_299_038_700_000L + 3_600_000L); // 1 hour later + assertTrue(dist("SELECT * FROM t WHERE ts = 1299038700000", row("ts", ts)) > 0.0); + } + + @Test + void timestampEquals_spaceSeparatorNoSeconds_zeroDistance() { + Instant ts = Instant.parse("2011-02-03T04:05:00Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2011-02-03 04:05+0000'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_spaceSeparatorWithSeconds_zeroDistance() { + Instant ts = Instant.parse("2011-02-03T04:05:00Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2011-02-03 04:05:00+0000'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_spaceSeparatorWithMillis_zeroDistance() { + Instant ts = Instant.parse("2011-02-03T04:05:00.123Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2011-02-03 04:05:00.123+0000'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_tSeparatorNoSeconds_zeroDistance() { + Instant ts = Instant.parse("2011-02-03T04:05:00Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2011-02-03T04:05+0000'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_tSeparatorWithSeconds_zeroDistance() { + Instant ts = Instant.parse("2011-02-03T04:05:00Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2011-02-03T04:05:00+0000'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_tSeparatorWithMillis_zeroDistance() { + Instant ts = Instant.parse("2011-02-03T04:05:00.123Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2011-02-03T04:05:00.123+0000'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_dateOnly_zeroDistance() { + Instant ts = Instant.parse("2011-02-03T00:00:00Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2011-02-03'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_dateOnlyWithOffset_zeroDistance() { + Instant ts = Instant.parse("2011-02-03T00:00:00Z"); + assertEquals(0.0, + dist("SELECT * FROM t WHERE ts = '2011-02-03+0000'", row("ts", ts)), + DELTA); + } + + @Test + void timestampEquals_spaceSeparator_differentTime_nonZero() { + Instant ts = Instant.parse("2011-02-03T05:00:00Z"); + double d = dist("SELECT * FROM t WHERE ts = '2011-02-03 04:05:00+0000'", row("ts", ts)); + assertTrue(d > 0.0 && d < 1.0); + } } \ No newline at end of file From a7964945f9c3743f2520a29c25d821bb120400ac Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Wed, 1 Jul 2026 17:52:47 -0300 Subject: [PATCH 5/8] Created CassandraOperationEvaluator --- .../CassandraHeuristicsCalculator.java | 298 +---------------- .../CassandraOperationEvaluator.java | 311 ++++++++++++++++++ 2 files changed, 318 insertions(+), 291 deletions(-) create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java index 16fffe12c3..3093600495 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java @@ -5,32 +5,19 @@ 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 org.evomaster.client.java.utils.SimpleLogger; -import java.net.InetAddress; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.temporal.TemporalAccessor; import java.util.*; public class CassandraHeuristicsCalculator { - private static final double C = DistanceHelper.H_NOT_NULL; - private static final double C_BETTER = 0.15; + public static final double C = DistanceHelper.H_NOT_NULL; + public static final double C_BETTER = 0.15; - private static final Truthness TRUE_TRUTHNESS = new Truthness(1.0, C); - private static final Truthness FALSE_TRUTHNESS = new Truthness(C, 1.0); - private static final Truthness FALSE_TRUTHNESS_BETTER = new Truthness(C_BETTER, 1.0); + 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 static final Object MISSING = new Object(); - - private enum ComparisonType { EQUALS, GT, GTE, LT, LTE } + private final CassandraOperationEvaluator evaluator = new CassandraOperationEvaluator(); public double computeDistance(String cqlQuery, Iterable> allRows) { return 1.0 - computeHQuery(cqlQuery, allRows).getOfTrue(); @@ -69,7 +56,7 @@ private Truthness computeHCondition(CqlQueryOperation condition, double maxOfTrue = 0.0; for (Map row : rows) { - double ofTrue = calculateDistance(condition, row).getOfTrue(); + double ofTrue = evaluator.evaluate(condition, row).getOfTrue(); if (ofTrue >= 1.0) { return TRUE_TRUTHNESS; } @@ -81,277 +68,6 @@ private Truthness computeHCondition(CqlQueryOperation condition, return TruthnessUtils.buildScaledTruthness(C, maxOfTrue); } - private Truthness calculateDistance(CqlQueryOperation op, Map row) { - if (op instanceof AndOperation) - return calculateDistanceForAnd((AndOperation) op, row); - if (op instanceof EqualsOperation) - return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.EQUALS); - if (op instanceof GreaterThanOperation) - return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.GT); - if (op instanceof GreaterThanEqualsOperation) - return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.GTE); - if (op instanceof LessThanOperation) - return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.LT); - if (op instanceof LessThanEqualsOperation) - return calculateDistanceForComparison((ComparisonOperation) op, row, ComparisonType.LTE); - if (op instanceof InOperation) - return calculateDistanceForIn((InOperation) op, row); - if (op instanceof ContainsOperation) - return calculateDistanceForContains((ContainsOperation) op, row); - if (op instanceof ContainsKeyOperation) - return calculateDistanceForContainsKey((ContainsKeyOperation) op, row); - - return FALSE_TRUTHNESS; - } - - private Truthness calculateDistanceForAnd(AndOperation op, Map row) { - List results = new ArrayList<>(); - for (CqlQueryOperation condition : op.getConditions()) { - results.add(calculateDistance(condition, row)); - } - return TruthnessUtils.buildAndAggregationTruthness(results.toArray(new Truthness[0])); - } - - private Truthness calculateDistanceForComparison( - ComparisonOperation op, - Map row, - ComparisonType type) { - - Object rowValue = getRowValue(row, op.getColumnName()); - Object queryValue = op.getValue(); - - if (rowValue == MISSING) rowValue = null; - - if (rowValue == null && queryValue == null) return FALSE_TRUTHNESS; - if (rowValue == null || queryValue == null) return FALSE_TRUTHNESS_BETTER; - - Truthness typeResult = compareByType(rowValue, queryValue, type); - if (typeResult.isTrue()) { - return typeResult; - } - return TruthnessUtils.buildScaledTruthness(C_BETTER, typeResult.getOfTrue()); - } - - private Truthness compareByType(Object rowVal, Object queryVal, ComparisonType type) { - if (rowVal instanceof Number && queryVal instanceof Number) - return compareNumeric(rowVal, queryVal, type); - if (rowVal instanceof String || rowVal instanceof InetAddress) - return compareString(rowVal, queryVal, type); - if (rowVal instanceof Boolean) - return compareBoolean(rowVal, queryVal, type); - if (rowVal instanceof UUID) - return compareUuid(rowVal, queryVal, type); - if (isTemporalType(rowVal)) - return compareTemporal(rowVal, queryVal, type); - if (isCqlDuration(rowVal)) - return compareDuration(rowVal, queryVal, type); - - SimpleLogger.uniqueWarn("CassandraHeuristicsCalculator: unsupported type " - + rowVal.getClass().getName() + " — returning FALSE_TRUTHNESS"); - return FALSE_TRUTHNESS; - } - - private Truthness compareNumeric(Object rowVal, Object queryVal, ComparisonType type) { - double x = ((Number) rowVal).doubleValue(); - double y = ((Number) queryVal).doubleValue(); - switch (type) { - 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: return FALSE_TRUTHNESS; - } - } - - private Truthness compareString(Object rowVal, Object queryVal, ComparisonType type) { - if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; - String a = (rowVal instanceof InetAddress) - ? ((InetAddress) rowVal).getHostAddress() - : (String) rowVal; - String b = (String) queryVal; - return TruthnessUtils.getStringEqualityTruthness(a, b); - } - - private Truthness compareBoolean(Object rowVal, Object queryVal, ComparisonType type) { - if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; - double x = ((Boolean) rowVal) ? 1.0 : 0.0; - double y = ((Boolean) queryVal) ? 1.0 : 0.0; - return TruthnessUtils.getEqualityTruthness(x, y); - } - - private Truthness compareUuid(Object rowVal, Object queryVal, ComparisonType type) { - if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; - return TruthnessUtils.getEqualityTruthness((UUID) rowVal, (UUID) queryVal); - } - - private static boolean isTemporalType(Object v) { - return v instanceof LocalDate - || v instanceof LocalTime - || v instanceof Instant; - } - - private Truthness compareTemporal(Object rowVal, Object queryVal, ComparisonType type) { - try { - double x = (double) toLong(rowVal, rowVal); - double y = (double) toLong(queryVal, rowVal); - switch (type) { - 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: return FALSE_TRUTHNESS; - } - } catch (Exception e) { - return FALSE_TRUTHNESS; - } - } - - private static long toLong(Object value, Object rowValueHint) { - if (rowValueHint instanceof LocalDate) { - LocalDate d = (value instanceof Long) ? LocalDate.ofEpochDay((Long) value) - : (value instanceof String) ? LocalDate.parse((String) value) - : (LocalDate) value; - return d.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); - } - if (rowValueHint instanceof LocalTime) { - LocalTime t = (value instanceof Long) ? LocalTime.ofNanoOfDay((Long) value) - : (value instanceof String) ? LocalTime.parse((String) value) - : (LocalTime) value; - return LocalDateTime.of(LocalDate.of(1970, 1, 1), t).toInstant(ZoneOffset.UTC).toEpochMilli(); - } - if (rowValueHint instanceof Instant) { - if (value instanceof Long) return (Long) value; - if (value instanceof Instant) return ((Instant) value).toEpochMilli(); - if (value instanceof String) return parseTimestampString((String) value).toEpochMilli(); - throw new IllegalArgumentException("Unexpected timestamp value type: " + value.getClass()); - } - throw new IllegalArgumentException("Unrecognized temporal type: " + rowValueHint.getClass()); - } - - 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"); - - private static Instant parseTimestampString(String s) { - try { return Instant.parse(s); } catch (DateTimeParseException ignored) {} - for (DateTimeFormatter formatter : TIMESTAMP_FORMATTERS) { - try { return OffsetDateTime.parse(s, formatter).toInstant(); } catch (DateTimeParseException ignored) {} - } - // date-only with offset ("2011-02-03+0000"): OffsetDateTime.parse fails without a time - // component, so extract LocalDate and ZoneOffset from the TemporalAccessor directly. - try { - TemporalAccessor accessor = DATE_WITH_OFFSET.parse(s); - return LocalDate.from(accessor).atStartOfDay(ZoneOffset.from(accessor)).toInstant(); - } catch (DateTimeParseException ignored) {} - // date-only without offset ("2011-02-03"): treat as midnight UTC - try { return LocalDate.parse(s).atStartOfDay(ZoneOffset.UTC).toInstant(); } catch (DateTimeParseException ignored) {} - throw new IllegalArgumentException("Cannot parse timestamp string: " + s); - } - - private static boolean isCqlDuration(Object v) { - return v.getClass().getName() - .equals("com.datastax.oss.driver.api.core.data.CqlDuration"); - } - - private static int getDurationMonths(Object d) throws Exception { - return (int) d.getClass().getMethod("getMonths").invoke(d); - } - - private static int getDurationDays(Object d) throws Exception { - return (int) d.getClass().getMethod("getDays").invoke(d); - } - - private static long getDurationNanos(Object d) throws Exception { - return (long) d.getClass().getMethod("getNanoseconds").invoke(d); - } - - private Truthness compareDuration(Object rowVal, Object queryVal, ComparisonType type) { - if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; - try { - int rm = getDurationMonths(rowVal); - int rd = getDurationDays(rowVal); - long rn = getDurationNanos(rowVal); - - CqlDurationLiteral q = CqlDurationLiteral.parse((String) queryVal); - - return TruthnessUtils.buildAndAggregationTruthness( - TruthnessUtils.getEqualityTruthness((long) rm, (long) q.months), - TruthnessUtils.getEqualityTruthness((long) rd, (long) q.days), - TruthnessUtils.getEqualityTruthness(rn, q.nanos) - ); - } catch (Exception e) { - return FALSE_TRUTHNESS; - } - } - - private Truthness calculateDistanceForIn(InOperation op, Map row) { - Object rowValue = getRowValue(row, op.getColumnName()); - if (rowValue == MISSING) rowValue = null; - return any(rowValue, op.getValues()); - } - - private Truthness calculateDistanceForContains(ContainsOperation op, - Map row) { - Object rawCol = getRowValue(row, op.getColumnName()); - if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; - return any(op.getValue(), toElementList(rawCol)); - } - - private Truthness calculateDistanceForContainsKey(ContainsKeyOperation op, - Map row) { - Object rawCol = getRowValue(row, op.getColumnName()); - if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; - if (!(rawCol instanceof Map)) return FALSE_TRUTHNESS; - List keys = new ArrayList<>(((Map) rawCol).keySet()); - return any(op.getValue(), keys); - } - - private Truthness any(Object value, List candidates) { - if (candidates.isEmpty()) return FALSE_TRUTHNESS; - 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; - if (a == null || b == null) return FALSE_TRUTHNESS_BETTER; - Truthness raw = compareByType(a, b, ComparisonType.EQUALS); - if (raw.isTrue()) return raw; - return TruthnessUtils.buildScaledTruthness(C_BETTER, raw.getOfTrue()); - } - - private static List toElementList(Object collection) { - if (collection instanceof List) return (List) collection; - if (collection instanceof Set) return new ArrayList<>((Set) collection); - if (collection instanceof Map) return new ArrayList<>(((Map) collection).values()); - return Collections.emptyList(); - } - - private static String normalizeColumnName(String name) { - if (name == null) return null; - if (name.startsWith("\"") && name.endsWith("\"")) { - return name.substring(1, name.length() - 1).toLowerCase(); - } - return name.toLowerCase(); - } - - private Object getRowValue(Map row, String rawColumnName) { - String key = normalizeColumnName(rawColumnName); - if (!row.containsKey(key)) return MISSING; - return row.get(key); - } - private static List> toList(Iterable> iterable) { if (iterable instanceof List) { return (List>) iterable; diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java new file mode 100644 index 0000000000..e088437018 --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java @@ -0,0 +1,311 @@ +package org.evomaster.client.java.controller.cassandra; + +import org.evomaster.client.java.controller.cassandra.operations.*; +import org.evomaster.client.java.distance.heuristics.Truthness; +import org.evomaster.client.java.distance.heuristics.TruthnessUtils; +import org.evomaster.client.java.utils.SimpleLogger; + +import java.net.InetAddress; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.util.*; + +import static org.evomaster.client.java.controller.cassandra.CassandraHeuristicsCalculator.*; + +public class CassandraOperationEvaluator { + + private static final Object MISSING = new Object(); + + private enum ComparisonType { EQUALS, GT, GTE, LT, LTE } + + public Truthness evaluate(CqlQueryOperation op, Map row) { + if (op instanceof AndOperation) + return evaluateAnd((AndOperation) op, row); + if (op instanceof EqualsOperation) + return evaluateEquals((EqualsOperation) op, row); + if (op instanceof GreaterThanOperation) + return evaluateGreaterThan((GreaterThanOperation) op, row); + if (op instanceof GreaterThanEqualsOperation) + return evaluateGreaterThanEquals((GreaterThanEqualsOperation) op, row); + if (op instanceof LessThanOperation) + return evaluateLessThan((LessThanOperation) op, row); + if (op instanceof LessThanEqualsOperation) + return evaluateLessThanEquals((LessThanEqualsOperation) op, row); + if (op instanceof InOperation) + return evaluateIn((InOperation) op, row); + if (op instanceof ContainsOperation) + return evaluateContains((ContainsOperation) op, row); + if (op instanceof ContainsKeyOperation) + return evaluateContainsKey((ContainsKeyOperation) op, row); + return FALSE_TRUTHNESS; + } + + private Truthness evaluateAnd(AndOperation op, Map row) { + List results = new ArrayList<>(); + for (CqlQueryOperation condition : op.getConditions()) { + results.add(evaluate(condition, row)); + } + return TruthnessUtils.buildAndAggregationTruthness(results.toArray(new Truthness[0])); + } + + private Truthness evaluateEquals(EqualsOperation op, Map row) { + return evaluateComparison(op, row, ComparisonType.EQUALS); + } + + private Truthness evaluateGreaterThan(GreaterThanOperation op, Map row) { + return evaluateComparison(op, row, ComparisonType.GT); + } + + private Truthness evaluateGreaterThanEquals(GreaterThanEqualsOperation op, Map row) { + return evaluateComparison(op, row, ComparisonType.GTE); + } + + private Truthness evaluateLessThan(LessThanOperation op, Map row) { + return evaluateComparison(op, row, ComparisonType.LT); + } + + private Truthness evaluateLessThanEquals(LessThanEqualsOperation op, Map row) { + return evaluateComparison(op, row, ComparisonType.LTE); + } + + private Truthness evaluateIn(InOperation op, Map row) { + Object rowValue = getRowValue(row, op.getColumnName()); + if (rowValue == MISSING) rowValue = null; + return any(rowValue, op.getValues()); + } + + private Truthness evaluateContains(ContainsOperation op, Map row) { + Object rawCol = getRowValue(row, op.getColumnName()); + if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; + return any(op.getValue(), toElementList(rawCol)); + } + + private Truthness evaluateContainsKey(ContainsKeyOperation op, Map row) { + Object rawCol = getRowValue(row, op.getColumnName()); + if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; + if (!(rawCol instanceof Map)) return FALSE_TRUTHNESS; + List keys = new ArrayList<>(((Map) rawCol).keySet()); + return any(op.getValue(), keys); + } + + private Truthness evaluateComparison(ComparisonOperation op, Map row, ComparisonType type) { + Object rowValue = getRowValue(row, op.getColumnName()); + Object queryValue = op.getValue(); + + if (rowValue == MISSING) rowValue = null; + + if (rowValue == null && queryValue == null) return FALSE_TRUTHNESS; + if (rowValue == null || queryValue == null) return FALSE_TRUTHNESS_BETTER; + + Truthness typeResult = compareByType(rowValue, queryValue, type); + if (typeResult.isTrue()) { + return typeResult; + } + return TruthnessUtils.buildScaledTruthness(C_BETTER, typeResult.getOfTrue()); + } + + private Truthness compareByType(Object rowVal, Object queryVal, ComparisonType type) { + if (rowVal instanceof Number && queryVal instanceof Number) + return compareNumeric(rowVal, queryVal, type); + if (rowVal instanceof String || rowVal instanceof InetAddress) + return compareString(rowVal, queryVal, type); + if (rowVal instanceof Boolean) + return compareBoolean(rowVal, queryVal, type); + if (rowVal instanceof UUID) + return compareUuid(rowVal, queryVal, type); + if (isTemporalType(rowVal)) + return compareTemporal(rowVal, queryVal, type); + if (isCqlDuration(rowVal)) + return compareDuration(rowVal, queryVal, type); + + SimpleLogger.uniqueWarn("CassandraHeuristicsCalculator: unsupported type " + + rowVal.getClass().getName() + " — returning FALSE_TRUTHNESS"); + return FALSE_TRUTHNESS; + } + + private Truthness compareNumeric(Object rowVal, Object queryVal, ComparisonType type) { + double x = ((Number) rowVal).doubleValue(); + double y = ((Number) queryVal).doubleValue(); + switch (type) { + 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: return FALSE_TRUTHNESS; + } + } + + private Truthness compareString(Object rowVal, Object queryVal, ComparisonType type) { + if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; + String a = (rowVal instanceof InetAddress) + ? ((InetAddress) rowVal).getHostAddress() + : (String) rowVal; + String b = (String) queryVal; + return TruthnessUtils.getStringEqualityTruthness(a, b); + } + + private Truthness compareBoolean(Object rowVal, Object queryVal, ComparisonType type) { + if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; + double x = ((Boolean) rowVal) ? 1.0 : 0.0; + double y = ((Boolean) queryVal) ? 1.0 : 0.0; + return TruthnessUtils.getEqualityTruthness(x, y); + } + + private Truthness compareUuid(Object rowVal, Object queryVal, ComparisonType type) { + if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; + return TruthnessUtils.getEqualityTruthness((UUID) rowVal, (UUID) queryVal); + } + + private static boolean isTemporalType(Object v) { + return v instanceof LocalDate + || v instanceof LocalTime + || v instanceof Instant; + } + + private Truthness compareTemporal(Object rowVal, Object queryVal, ComparisonType type) { + try { + double x = (double) toLong(rowVal, rowVal); + double y = (double) toLong(queryVal, rowVal); + switch (type) { + 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: return FALSE_TRUTHNESS; + } + } catch (Exception e) { + return FALSE_TRUTHNESS; + } + } + + private static long toLong(Object value, Object rowValueHint) { + if (rowValueHint instanceof LocalDate) { + LocalDate d = (value instanceof Long) ? LocalDate.ofEpochDay((Long) value) + : (value instanceof String) ? LocalDate.parse((String) value) + : (LocalDate) value; + return d.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); + } + if (rowValueHint instanceof LocalTime) { + LocalTime t = (value instanceof Long) ? LocalTime.ofNanoOfDay((Long) value) + : (value instanceof String) ? LocalTime.parse((String) value) + : (LocalTime) value; + return LocalDateTime.of(LocalDate.of(1970, 1, 1), t).toInstant(ZoneOffset.UTC).toEpochMilli(); + } + if (rowValueHint instanceof Instant) { + if (value instanceof Long) return (Long) value; + if (value instanceof Instant) return ((Instant) value).toEpochMilli(); + if (value instanceof String) return parseTimestampString((String) value).toEpochMilli(); + throw new IllegalArgumentException("Unexpected timestamp value type: " + value.getClass()); + } + throw new IllegalArgumentException("Unrecognized temporal type: " + rowValueHint.getClass()); + } + + 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"); + + private static Instant parseTimestampString(String s) { + try { return Instant.parse(s); } catch (DateTimeParseException ignored) {} + for (DateTimeFormatter formatter : TIMESTAMP_FORMATTERS) { + try { return OffsetDateTime.parse(s, formatter).toInstant(); } catch (DateTimeParseException ignored) {} + } + // date-only with offset ("2011-02-03+0000"): OffsetDateTime.parse fails without a time + // component, so extract LocalDate and ZoneOffset from the TemporalAccessor directly. + try { + TemporalAccessor accessor = DATE_WITH_OFFSET.parse(s); + return LocalDate.from(accessor).atStartOfDay(ZoneOffset.from(accessor)).toInstant(); + } catch (DateTimeParseException ignored) {} + // date-only without offset ("2011-02-03"): treat as midnight UTC + try { return LocalDate.parse(s).atStartOfDay(ZoneOffset.UTC).toInstant(); } catch (DateTimeParseException ignored) {} + throw new IllegalArgumentException("Cannot parse timestamp string: " + s); + } + + private static boolean isCqlDuration(Object v) { + return v.getClass().getName() + .equals("com.datastax.oss.driver.api.core.data.CqlDuration"); + } + + private static int getDurationMonths(Object d) throws Exception { + return (int) d.getClass().getMethod("getMonths").invoke(d); + } + + private static int getDurationDays(Object d) throws Exception { + return (int) d.getClass().getMethod("getDays").invoke(d); + } + + private static long getDurationNanos(Object d) throws Exception { + return (long) d.getClass().getMethod("getNanoseconds").invoke(d); + } + + private Truthness compareDuration(Object rowVal, Object queryVal, ComparisonType type) { + if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; + try { + int rm = getDurationMonths(rowVal); + int rd = getDurationDays(rowVal); + long rn = getDurationNanos(rowVal); + + CqlDurationLiteral q = CqlDurationLiteral.parse((String) queryVal); + + return TruthnessUtils.buildAndAggregationTruthness( + TruthnessUtils.getEqualityTruthness((long) rm, (long) q.months), + TruthnessUtils.getEqualityTruthness((long) rd, (long) q.days), + TruthnessUtils.getEqualityTruthness(rn, q.nanos) + ); + } catch (Exception e) { + return FALSE_TRUTHNESS; + } + } + + private Truthness any(Object value, List candidates) { + if (candidates.isEmpty()) return FALSE_TRUTHNESS; + 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; + if (a == null || b == null) return FALSE_TRUTHNESS_BETTER; + Truthness raw = compareByType(a, b, ComparisonType.EQUALS); + if (raw.isTrue()) return raw; + return TruthnessUtils.buildScaledTruthness(C_BETTER, raw.getOfTrue()); + } + + private static List toElementList(Object collection) { + if (collection instanceof List) return (List) collection; + if (collection instanceof Set) return new ArrayList<>((Set) collection); + if (collection instanceof Map) return new ArrayList<>(((Map) collection).values()); + return Collections.emptyList(); + } + + private static String normalizeColumnName(String name) { + if (name == null) return null; + if (name.startsWith("\"") && name.endsWith("\"")) { + return name.substring(1, name.length() - 1).toLowerCase(); + } + return name.toLowerCase(); + } + + private Object getRowValue(Map row, String rawColumnName) { + String key = normalizeColumnName(rawColumnName); + if (!row.containsKey(key)) return MISSING; + return row.get(key); + } +} \ No newline at end of file From bcf43e2330a597f0c8cad219f6f6959692862e84 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Wed, 1 Jul 2026 18:26:40 -0300 Subject: [PATCH 6/8] Minor refactorings --- .../CassandraOperationEvaluator.java | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java index e088437018..f60d0c419b 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java @@ -133,14 +133,7 @@ private Truthness compareByType(Object rowVal, Object queryVal, ComparisonType t private Truthness compareNumeric(Object rowVal, Object queryVal, ComparisonType type) { double x = ((Number) rowVal).doubleValue(); double y = ((Number) queryVal).doubleValue(); - switch (type) { - 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: return FALSE_TRUTHNESS; - } + return getTruthness(type, x, y); } private Truthness compareString(Object rowVal, Object queryVal, ComparisonType type) { @@ -164,41 +157,45 @@ private Truthness compareUuid(Object rowVal, Object queryVal, ComparisonType typ return TruthnessUtils.getEqualityTruthness((UUID) rowVal, (UUID) queryVal); } - private static boolean isTemporalType(Object v) { - return v instanceof LocalDate - || v instanceof LocalTime - || v instanceof Instant; + private static boolean isTemporalType(Object value) { + return value instanceof LocalDate + || value instanceof LocalTime + || value instanceof Instant; } private Truthness compareTemporal(Object rowVal, Object queryVal, ComparisonType type) { try { double x = (double) toLong(rowVal, rowVal); double y = (double) toLong(queryVal, rowVal); - switch (type) { - 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: return FALSE_TRUTHNESS; - } + return getTruthness(type, x, y); } catch (Exception e) { return FALSE_TRUTHNESS; } } + private Truthness getTruthness(ComparisonType type, double x, double y) { + switch (type) { + 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: return FALSE_TRUTHNESS; + } + } + private static long toLong(Object value, Object rowValueHint) { if (rowValueHint instanceof LocalDate) { - LocalDate d = (value instanceof Long) ? LocalDate.ofEpochDay((Long) value) + LocalDate dateVal = (value instanceof Long) ? LocalDate.ofEpochDay((Long) value) : (value instanceof String) ? LocalDate.parse((String) value) : (LocalDate) value; - return d.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); + return dateVal.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); } if (rowValueHint instanceof LocalTime) { - LocalTime t = (value instanceof Long) ? LocalTime.ofNanoOfDay((Long) value) + LocalTime timeVal = (value instanceof Long) ? LocalTime.ofNanoOfDay((Long) value) : (value instanceof String) ? LocalTime.parse((String) value) : (LocalTime) value; - return LocalDateTime.of(LocalDate.of(1970, 1, 1), t).toInstant(ZoneOffset.UTC).toEpochMilli(); + return LocalDateTime.of(LocalDate.of(1970, 1, 1), timeVal).toInstant(ZoneOffset.UTC).toEpochMilli(); } if (rowValueHint instanceof Instant) { if (value instanceof Long) return (Long) value; @@ -236,36 +233,36 @@ private static Instant parseTimestampString(String s) { throw new IllegalArgumentException("Cannot parse timestamp string: " + s); } - private static boolean isCqlDuration(Object v) { - return v.getClass().getName() + private static boolean isCqlDuration(Object value) { + return value.getClass().getName() .equals("com.datastax.oss.driver.api.core.data.CqlDuration"); } - private static int getDurationMonths(Object d) throws Exception { - return (int) d.getClass().getMethod("getMonths").invoke(d); + private static int getDurationMonths(Object durationVal) throws Exception { + return (int) durationVal.getClass().getMethod("getMonths").invoke(durationVal); } - private static int getDurationDays(Object d) throws Exception { - return (int) d.getClass().getMethod("getDays").invoke(d); + private static int getDurationDays(Object durationVal) throws Exception { + return (int) durationVal.getClass().getMethod("getDays").invoke(durationVal); } - private static long getDurationNanos(Object d) throws Exception { - return (long) d.getClass().getMethod("getNanoseconds").invoke(d); + private static long getDurationNanos(Object durationVal) throws Exception { + return (long) durationVal.getClass().getMethod("getNanoseconds").invoke(durationVal); } private Truthness compareDuration(Object rowVal, Object queryVal, ComparisonType type) { if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; try { - int rm = getDurationMonths(rowVal); - int rd = getDurationDays(rowVal); - long rn = getDurationNanos(rowVal); + int months = getDurationMonths(rowVal); + int days = getDurationDays(rowVal); + long nanos = getDurationNanos(rowVal); CqlDurationLiteral q = CqlDurationLiteral.parse((String) queryVal); return TruthnessUtils.buildAndAggregationTruthness( - TruthnessUtils.getEqualityTruthness((long) rm, (long) q.months), - TruthnessUtils.getEqualityTruthness((long) rd, (long) q.days), - TruthnessUtils.getEqualityTruthness(rn, q.nanos) + TruthnessUtils.getEqualityTruthness((long) months, (long) q.months), + TruthnessUtils.getEqualityTruthness((long) days, (long) q.days), + TruthnessUtils.getEqualityTruthness(nanos, q.nanos) ); } catch (Exception e) { return FALSE_TRUTHNESS; From 288be95f0cc79bba8ac055279184ee5cc16d30ba Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Wed, 8 Jul 2026 17:32:35 -0300 Subject: [PATCH 7/8] PR review comments - Small refactorings to follow dev guidelines - Refactor parsing of Duration literals and reorganise packages - Unify distance calculation for strings - Refactor operation evaluator - Add javadoc for calculator and CassandraRow abstraction --- .../CassandraOperationEvaluator.java | 308 -------------- .../CassandraHeuristicsCalculator.java | 54 ++- .../CassandraOperationEvaluator.java | 378 ++++++++++++++++++ .../cassandra/model/CassandraRow.java | 48 +++ .../cassandra/model/CqlDurationLiteral.java | 21 + .../cassandra/operations/EqualsOperation.java | 3 + .../GreaterThanEqualsOperation.java | 3 + .../operations/GreaterThanOperation.java | 3 + .../operations/LessThanEqualsOperation.java | 3 + .../operations/LessThanOperation.java | 3 + .../CqlDurationLiteralParser.java} | 53 +-- .../cassandra/parser/CqlParserUtils.java | 5 + .../CassandraHeuristicsCalculatorTest.java | 15 +- .../distance/heuristics/TruthnessUtils.java | 15 +- .../sql/heuristic/SqlExpressionEvaluator.java | 28 +- 15 files changed, 562 insertions(+), 378 deletions(-) delete mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java rename client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/{ => calculator}/CassandraHeuristicsCalculator.java (50%) create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraOperationEvaluator.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/model/CassandraRow.java create mode 100644 client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/model/CqlDurationLiteral.java rename client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/{CqlDurationLiteral.java => parser/CqlDurationLiteralParser.java} (66%) rename client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/{ => calculator}/CassandraHeuristicsCalculatorTest.java (98%) diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java deleted file mode 100644 index f60d0c419b..0000000000 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraOperationEvaluator.java +++ /dev/null @@ -1,308 +0,0 @@ -package org.evomaster.client.java.controller.cassandra; - -import org.evomaster.client.java.controller.cassandra.operations.*; -import org.evomaster.client.java.distance.heuristics.Truthness; -import org.evomaster.client.java.distance.heuristics.TruthnessUtils; -import org.evomaster.client.java.utils.SimpleLogger; - -import java.net.InetAddress; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.temporal.TemporalAccessor; -import java.util.*; - -import static org.evomaster.client.java.controller.cassandra.CassandraHeuristicsCalculator.*; - -public class CassandraOperationEvaluator { - - private static final Object MISSING = new Object(); - - private enum ComparisonType { EQUALS, GT, GTE, LT, LTE } - - public Truthness evaluate(CqlQueryOperation op, Map row) { - if (op instanceof AndOperation) - return evaluateAnd((AndOperation) op, row); - if (op instanceof EqualsOperation) - return evaluateEquals((EqualsOperation) op, row); - if (op instanceof GreaterThanOperation) - return evaluateGreaterThan((GreaterThanOperation) op, row); - if (op instanceof GreaterThanEqualsOperation) - return evaluateGreaterThanEquals((GreaterThanEqualsOperation) op, row); - if (op instanceof LessThanOperation) - return evaluateLessThan((LessThanOperation) op, row); - if (op instanceof LessThanEqualsOperation) - return evaluateLessThanEquals((LessThanEqualsOperation) op, row); - if (op instanceof InOperation) - return evaluateIn((InOperation) op, row); - if (op instanceof ContainsOperation) - return evaluateContains((ContainsOperation) op, row); - if (op instanceof ContainsKeyOperation) - return evaluateContainsKey((ContainsKeyOperation) op, row); - return FALSE_TRUTHNESS; - } - - private Truthness evaluateAnd(AndOperation op, Map row) { - List results = new ArrayList<>(); - for (CqlQueryOperation condition : op.getConditions()) { - results.add(evaluate(condition, row)); - } - return TruthnessUtils.buildAndAggregationTruthness(results.toArray(new Truthness[0])); - } - - private Truthness evaluateEquals(EqualsOperation op, Map row) { - return evaluateComparison(op, row, ComparisonType.EQUALS); - } - - private Truthness evaluateGreaterThan(GreaterThanOperation op, Map row) { - return evaluateComparison(op, row, ComparisonType.GT); - } - - private Truthness evaluateGreaterThanEquals(GreaterThanEqualsOperation op, Map row) { - return evaluateComparison(op, row, ComparisonType.GTE); - } - - private Truthness evaluateLessThan(LessThanOperation op, Map row) { - return evaluateComparison(op, row, ComparisonType.LT); - } - - private Truthness evaluateLessThanEquals(LessThanEqualsOperation op, Map row) { - return evaluateComparison(op, row, ComparisonType.LTE); - } - - private Truthness evaluateIn(InOperation op, Map row) { - Object rowValue = getRowValue(row, op.getColumnName()); - if (rowValue == MISSING) rowValue = null; - return any(rowValue, op.getValues()); - } - - private Truthness evaluateContains(ContainsOperation op, Map row) { - Object rawCol = getRowValue(row, op.getColumnName()); - if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; - return any(op.getValue(), toElementList(rawCol)); - } - - private Truthness evaluateContainsKey(ContainsKeyOperation op, Map row) { - Object rawCol = getRowValue(row, op.getColumnName()); - if (rawCol == MISSING || rawCol == null) return FALSE_TRUTHNESS; - if (!(rawCol instanceof Map)) return FALSE_TRUTHNESS; - List keys = new ArrayList<>(((Map) rawCol).keySet()); - return any(op.getValue(), keys); - } - - private Truthness evaluateComparison(ComparisonOperation op, Map row, ComparisonType type) { - Object rowValue = getRowValue(row, op.getColumnName()); - Object queryValue = op.getValue(); - - if (rowValue == MISSING) rowValue = null; - - if (rowValue == null && queryValue == null) return FALSE_TRUTHNESS; - if (rowValue == null || queryValue == null) return FALSE_TRUTHNESS_BETTER; - - Truthness typeResult = compareByType(rowValue, queryValue, type); - if (typeResult.isTrue()) { - return typeResult; - } - return TruthnessUtils.buildScaledTruthness(C_BETTER, typeResult.getOfTrue()); - } - - private Truthness compareByType(Object rowVal, Object queryVal, ComparisonType type) { - if (rowVal instanceof Number && queryVal instanceof Number) - return compareNumeric(rowVal, queryVal, type); - if (rowVal instanceof String || rowVal instanceof InetAddress) - return compareString(rowVal, queryVal, type); - if (rowVal instanceof Boolean) - return compareBoolean(rowVal, queryVal, type); - if (rowVal instanceof UUID) - return compareUuid(rowVal, queryVal, type); - if (isTemporalType(rowVal)) - return compareTemporal(rowVal, queryVal, type); - if (isCqlDuration(rowVal)) - return compareDuration(rowVal, queryVal, type); - - SimpleLogger.uniqueWarn("CassandraHeuristicsCalculator: unsupported type " - + rowVal.getClass().getName() + " — returning FALSE_TRUTHNESS"); - return FALSE_TRUTHNESS; - } - - private Truthness compareNumeric(Object rowVal, Object queryVal, ComparisonType type) { - double x = ((Number) rowVal).doubleValue(); - double y = ((Number) queryVal).doubleValue(); - return getTruthness(type, x, y); - } - - private Truthness compareString(Object rowVal, Object queryVal, ComparisonType type) { - if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; - String a = (rowVal instanceof InetAddress) - ? ((InetAddress) rowVal).getHostAddress() - : (String) rowVal; - String b = (String) queryVal; - return TruthnessUtils.getStringEqualityTruthness(a, b); - } - - private Truthness compareBoolean(Object rowVal, Object queryVal, ComparisonType type) { - if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; - double x = ((Boolean) rowVal) ? 1.0 : 0.0; - double y = ((Boolean) queryVal) ? 1.0 : 0.0; - return TruthnessUtils.getEqualityTruthness(x, y); - } - - private Truthness compareUuid(Object rowVal, Object queryVal, ComparisonType type) { - if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; - return TruthnessUtils.getEqualityTruthness((UUID) rowVal, (UUID) queryVal); - } - - private static boolean isTemporalType(Object value) { - return value instanceof LocalDate - || value instanceof LocalTime - || value instanceof Instant; - } - - private Truthness compareTemporal(Object rowVal, Object queryVal, ComparisonType type) { - try { - double x = (double) toLong(rowVal, rowVal); - double y = (double) toLong(queryVal, rowVal); - return getTruthness(type, x, y); - } catch (Exception e) { - return FALSE_TRUTHNESS; - } - } - - private Truthness getTruthness(ComparisonType type, double x, double y) { - switch (type) { - 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: return FALSE_TRUTHNESS; - } - } - - private static long toLong(Object value, Object rowValueHint) { - if (rowValueHint instanceof LocalDate) { - LocalDate dateVal = (value instanceof Long) ? LocalDate.ofEpochDay((Long) value) - : (value instanceof String) ? LocalDate.parse((String) value) - : (LocalDate) value; - return dateVal.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(); - } - if (rowValueHint instanceof LocalTime) { - LocalTime timeVal = (value instanceof Long) ? LocalTime.ofNanoOfDay((Long) value) - : (value instanceof String) ? LocalTime.parse((String) value) - : (LocalTime) value; - return LocalDateTime.of(LocalDate.of(1970, 1, 1), timeVal).toInstant(ZoneOffset.UTC).toEpochMilli(); - } - if (rowValueHint instanceof Instant) { - if (value instanceof Long) return (Long) value; - if (value instanceof Instant) return ((Instant) value).toEpochMilli(); - if (value instanceof String) return parseTimestampString((String) value).toEpochMilli(); - throw new IllegalArgumentException("Unexpected timestamp value type: " + value.getClass()); - } - throw new IllegalArgumentException("Unrecognized temporal type: " + rowValueHint.getClass()); - } - - 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"); - - private static Instant parseTimestampString(String s) { - try { return Instant.parse(s); } catch (DateTimeParseException ignored) {} - for (DateTimeFormatter formatter : TIMESTAMP_FORMATTERS) { - try { return OffsetDateTime.parse(s, formatter).toInstant(); } catch (DateTimeParseException ignored) {} - } - // date-only with offset ("2011-02-03+0000"): OffsetDateTime.parse fails without a time - // component, so extract LocalDate and ZoneOffset from the TemporalAccessor directly. - try { - TemporalAccessor accessor = DATE_WITH_OFFSET.parse(s); - return LocalDate.from(accessor).atStartOfDay(ZoneOffset.from(accessor)).toInstant(); - } catch (DateTimeParseException ignored) {} - // date-only without offset ("2011-02-03"): treat as midnight UTC - try { return LocalDate.parse(s).atStartOfDay(ZoneOffset.UTC).toInstant(); } catch (DateTimeParseException ignored) {} - throw new IllegalArgumentException("Cannot parse timestamp string: " + s); - } - - private static boolean isCqlDuration(Object value) { - return value.getClass().getName() - .equals("com.datastax.oss.driver.api.core.data.CqlDuration"); - } - - private static int getDurationMonths(Object durationVal) throws Exception { - return (int) durationVal.getClass().getMethod("getMonths").invoke(durationVal); - } - - private static int getDurationDays(Object durationVal) throws Exception { - return (int) durationVal.getClass().getMethod("getDays").invoke(durationVal); - } - - private static long getDurationNanos(Object durationVal) throws Exception { - return (long) durationVal.getClass().getMethod("getNanoseconds").invoke(durationVal); - } - - private Truthness compareDuration(Object rowVal, Object queryVal, ComparisonType type) { - if (type != ComparisonType.EQUALS) return FALSE_TRUTHNESS; - try { - int months = getDurationMonths(rowVal); - int days = getDurationDays(rowVal); - long nanos = getDurationNanos(rowVal); - - CqlDurationLiteral q = CqlDurationLiteral.parse((String) queryVal); - - return TruthnessUtils.buildAndAggregationTruthness( - TruthnessUtils.getEqualityTruthness((long) months, (long) q.months), - TruthnessUtils.getEqualityTruthness((long) days, (long) q.days), - TruthnessUtils.getEqualityTruthness(nanos, q.nanos) - ); - } catch (Exception e) { - return FALSE_TRUTHNESS; - } - } - - private Truthness any(Object value, List candidates) { - if (candidates.isEmpty()) return FALSE_TRUTHNESS; - 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; - if (a == null || b == null) return FALSE_TRUTHNESS_BETTER; - Truthness raw = compareByType(a, b, ComparisonType.EQUALS); - if (raw.isTrue()) return raw; - return TruthnessUtils.buildScaledTruthness(C_BETTER, raw.getOfTrue()); - } - - private static List toElementList(Object collection) { - if (collection instanceof List) return (List) collection; - if (collection instanceof Set) return new ArrayList<>((Set) collection); - if (collection instanceof Map) return new ArrayList<>(((Map) collection).values()); - return Collections.emptyList(); - } - - private static String normalizeColumnName(String name) { - if (name == null) return null; - if (name.startsWith("\"") && name.endsWith("\"")) { - return name.substring(1, name.length() - 1).toLowerCase(); - } - return name.toLowerCase(); - } - - private Object getRowValue(Map row, String rawColumnName) { - String key = normalizeColumnName(rawColumnName); - if (!row.containsKey(key)) return MISSING; - return row.get(key); - } -} \ No newline at end of file diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculator.java similarity index 50% rename from client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java rename to client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculator.java index 3093600495..c74259062c 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculator.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculator.java @@ -1,13 +1,23 @@ -package org.evomaster.client.java.controller.cassandra; +package org.evomaster.client.java.controller.cassandra.calculator; -import org.evomaster.client.java.controller.cassandra.operations.*; +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.*; - +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; @@ -19,16 +29,26 @@ public class CassandraHeuristicsCalculator { private final CassandraOperationEvaluator evaluator = new CassandraOperationEvaluator(); - public double computeDistance(String cqlQuery, Iterable> allRows) { - return 1.0 - computeHQuery(cqlQuery, allRows).getOfTrue(); + /** + * 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> allRows) { + private Truthness computeHQuery(String cqlQuery, Iterable returnedRows) { if (!CqlParserUtils.canParseCqlCommand(cqlQuery)) { return FALSE_TRUTHNESS; } - List> rows = toList(allRows); + List rows = toList(returnedRows); org.evomaster.client.java.controller.cassandra.parser.CqlParser.RootContext root = CqlParserUtils.parseCqlCommand(cqlQuery); @@ -44,23 +64,22 @@ private Truthness computeHQuery(String cqlQuery, Iterable> a return TruthnessUtils.buildAndAggregationTruthness(hRowSet, hCondition); } - private Truthness computeHRowSet(List> rows) { + private Truthness computeHRowSet(List rows) { return TruthnessUtils.getTruthnessToEmpty(rows.size()).invert(); } private Truthness computeHCondition(CqlQueryOperation condition, - List> rows) { + List rows) { if (rows.isEmpty()) { return FALSE_TRUTHNESS; } double maxOfTrue = 0.0; - for (Map row : rows) { + for (CassandraRow row : rows) { double ofTrue = evaluator.evaluate(condition, row).getOfTrue(); if (ofTrue >= 1.0) { return TRUE_TRUTHNESS; - } - if (ofTrue > maxOfTrue) { + } else if (ofTrue > maxOfTrue) { maxOfTrue = ofTrue; } } @@ -68,12 +87,13 @@ private Truthness computeHCondition(CqlQueryOperation condition, return TruthnessUtils.buildScaledTruthness(C, maxOfTrue); } - private static List> toList(Iterable> iterable) { + private static List toList(Iterable iterable) { if (iterable instanceof List) { - return (List>) iterable; + return (List) iterable; } - List> list = new ArrayList<>(); - for (Map row : iterable) { + + List list = new ArrayList<>(); + for (CassandraRow row : iterable) { list.add(row); } return list; 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..3ccf97c12d --- /dev/null +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraOperationEvaluator.java @@ -0,0 +1,378 @@ +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 org.evomaster.client.java.utils.SimpleLogger; + +import java.net.InetAddress; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.util.*; + +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 } + + 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; + } + + 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; + } + + 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; + } + + Truthness typeResult = compareByType(rowValue, queryValue, type); + if (typeResult.isTrue()) { + return typeResult; + } + + 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); + } + + SimpleLogger.uniqueWarn("CassandraHeuristicsCalculator: unsupported type " + + rowValue.getClass().getName() + " — returning FALSE_TRUTHNESS"); + + return FALSE_TRUTHNESS; + } + + 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); + } + + 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); + } + + 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); + } + + 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) { + try { + return Instant.parse(rowValue); + } catch (DateTimeParseException ignored) {} + + for (DateTimeFormatter formatter : TIMESTAMP_FORMATTERS) { + try { + return OffsetDateTime.parse(rowValue, formatter).toInstant(); + } catch (DateTimeParseException ignored) {} + } + + // date-only with offset ("2011-02-03+0000"): OffsetDateTime.parse fails without a time + // component, so extract LocalDate and ZoneOffset from the TemporalAccessor directly. + try { + TemporalAccessor accessor = DATE_WITH_OFFSET.parse(rowValue); + return LocalDate.from(accessor).atStartOfDay(ZoneOffset.from(accessor)).toInstant(); + } catch (DateTimeParseException ignored) {} + + // date-only without offset ("2011-02-03"): treat as midnight UTC + try { + return LocalDate.parse(rowValue).atStartOfDay(ZoneOffset.UTC).toInstant(); + } catch (DateTimeParseException ignored) {} + + throw new IllegalArgumentException("Cannot parse timestamp string: " + rowValue); + } + + private static boolean isCqlDuration(Object rowValue) { + return rowValue.getClass().getName() + .equals("com.datastax.oss.driver.api.core.data.CqlDuration"); + } + + private static int getDurationMonths(Object durationRowValue) throws Exception { + return (int) durationRowValue.getClass().getMethod("getMonths").invoke(durationRowValue); + } + + private static int getDurationDays(Object durationRowValue) throws Exception { + return (int) durationRowValue.getClass().getMethod("getDays").invoke(durationRowValue); + } + + private static long getDurationNanos(Object durationRowValue) throws Exception { + return (long) durationRowValue.getClass().getMethod("getNanoseconds").invoke(durationRowValue); + } + + private Truthness compareDuration(Object rowValue, Object literalValue, ComparisonType comparisonType) { + if (comparisonType != ComparisonType.EQUALS) { + throw new IllegalArgumentException("Unsupported operator for duration literals: " + comparisonType); + } + + try { + int months = getDurationMonths(rowValue); + int days = getDurationDays(rowValue); + long nanos = getDurationNanos(rowValue); + + CqlDurationLiteral literalDurationValue = CqlDurationLiteralParser.parse((String) literalValue); + + return TruthnessUtils.buildAndAggregationTruthness( + TruthnessUtils.getEqualityTruthness((long) months, (long) literalDurationValue.months), + TruthnessUtils.getEqualityTruthness((long) days, (long) literalDurationValue.days), + TruthnessUtils.getEqualityTruthness(nanos, literalDurationValue.nanos) + ); + } catch (Exception e) { + return FALSE_TRUTHNESS; + } + } + + private Truthness any(Object value, List candidates) { + if (candidates.isEmpty()) { + return FALSE_TRUTHNESS; + } + + 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; + } + + Truthness raw = compareByType(a, b, ComparisonType.EQUALS); + + if (raw.isTrue()) { + return raw; + } + + 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/CqlDurationLiteral.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java similarity index 66% rename from client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CqlDurationLiteral.java rename to client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java index 998fa453fa..da4e1f1b21 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/CqlDurationLiteral.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java @@ -1,4 +1,6 @@ -package org.evomaster.client.java.controller.cassandra; +package org.evomaster.client.java.controller.cassandra.parser; + +import org.evomaster.client.java.controller.cassandra.model.CqlDurationLiteral; import java.time.Duration; import java.time.Period; @@ -6,39 +8,50 @@ import java.util.regex.Pattern; /** - * Parses a Cassandra duration string literal (as produced by CqlParserUtils) into - * its three components: months, days, and nanoseconds. + * Parses a Cassandra duration string literal into a {@link CqlDurationLiteral}. * * Supports two formats: * - ISO 8601: P1Y2M3DT4H5M6S * - Standard Cassandra: 1y2mo3d4h5m6s (units may appear in any order) */ -public class CqlDurationLiteral { +public class CqlDurationLiteralParser { - public final int months; - public final int days; - public final long nanos; + private CqlDurationLiteralParser() {} - private CqlDurationLiteral(int months, int days, long nanos) { - this.months = months; - this.days = days; - this.nanos = nanos; - } + /** + * Matches an optional digit sequence followed by a unit token. Units are ordered + * longest-first ({@code mo, ms, us, µs, ns, y, d, h, m, s}) to avoid {@code m} consuming + * {@code mo}, or {@code ms} consuming {@code m}. + */ + private static final Pattern TOKEN = + Pattern.compile("(\\d+)(mo|ms|µs|us|ns|y|d|h|m|s)", Pattern.CASE_INSENSITIVE); - public static CqlDurationLiteral parse(String text) { - if (text == null || text.isEmpty()) { + /** + * Parses a CQL duration literal into a {@link CqlDurationLiteral}, auto-detecting the + * format: strings starting with {@code P}/{@code p} are parsed as ISO 8601 (e.g. + * {@code P1Y2M3DT4H5M6S}), everything else as the standard Cassandra format (e.g. + * {@code 1y2mo3d4h5m6s}). + * + * @param stringDuration the duration literal to parse; must not be {@code null} or empty + * @return the parsed duration, decomposed into months, days and nanoseconds + * @throws IllegalArgumentException if {@code stringDuration} is {@code null} or empty + */ + public static CqlDurationLiteral parse(String stringDuration) { + if (stringDuration == null || stringDuration.isEmpty()) { throw new IllegalArgumentException("Empty duration literal"); } - String t = text.trim(); + + String t = stringDuration.trim(); if (t.startsWith("P") || t.startsWith("p")) { return parseIso(t); } + return parseCassandra(t); } - private static CqlDurationLiteral parseIso(String text) { + private static CqlDurationLiteral parseIso(String isoDuration) { // Split on T (or t) to separate date and time parts - String upper = text.toUpperCase(); + String upper = isoDuration.toUpperCase(); int tIndex = upper.indexOf('T'); int months = 0; @@ -67,12 +80,6 @@ private static CqlDurationLiteral parseIso(String text) { return new CqlDurationLiteral(months, days, nanos); } - // Matches: optional digits followed by a unit token. - // Units (longest-first to avoid 'm' consuming 'mo' or 'ms' consuming 'm'): - // mo, ms, us, µs, ns, y, d, h, m, s - private static final Pattern TOKEN = - Pattern.compile("(\\d+)(mo|ms|µs|us|ns|y|d|h|m|s)", Pattern.CASE_INSENSITIVE); - private static CqlDurationLiteral parseCassandra(String text) { int months = 0; int days = 0; 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..01f284ad17 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,6 +8,11 @@ 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() {} diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculatorTest.java similarity index 98% rename from client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java rename to client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculatorTest.java index 099b0a1a15..5e814b6477 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/CassandraHeuristicsCalculatorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/calculator/CassandraHeuristicsCalculatorTest.java @@ -1,29 +1,30 @@ -package org.evomaster.client.java.controller.cassandra; +package org.evomaster.client.java.controller.cassandra.calculator; +import com.datastax.oss.driver.api.core.data.CqlDuration; +import org.evomaster.client.java.controller.cassandra.model.CassandraRow; import org.evomaster.client.java.distance.heuristics.DistanceHelper; import org.junit.jupiter.api.Test; -import com.datastax.oss.driver.api.core.data.CqlDuration; import java.net.InetAddress; import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; import java.util.*; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class CassandraHeuristicsCalculatorTest { private final CassandraHeuristicsCalculator calc = new CassandraHeuristicsCalculator(); - private static Map row(Object... kv) { + private static CassandraRow row(Object... kv) { Map m = new LinkedHashMap<>(); for (int i = 0; i < kv.length; i += 2) m.put((String) kv[i], kv[i + 1]); - return m; + return new CassandraRow(m); } - @SafeVarargs - private final double dist(String cql, Map... rows) { + private double dist(String cql, CassandraRow... rows) { return calc.computeDistance(cql, Arrays.asList(rows)); } diff --git a/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java b/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java index 4df33ec882..46c3d46056 100644 --- a/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java +++ b/client-java/distance-heuristics/src/main/java/org/evomaster/client/java/distance/heuristics/TruthnessUtils.java @@ -319,15 +319,26 @@ public static Truthness getEqualityTruthness(UUID left, UUID right) { ); } + /** + * Computes the {@link Truthness} of the predicate {@code a.equals(b)}. + * If the strings are equal, {@code ofTrue} is maximal (1.0). Otherwise, {@code ofTrue} is + * derived from a left-alignment distance between the two strings (via + * {@link DistanceHelper#getLeftAlignmentDistance}), so that strings sharing a longer common + * prefix yield a higher (closer-to-true) heuristic value. + * + * @param a the first string, must not be {@code null} + * @param b the second string, must not be {@code null} + * @return the Truthness of {@code a} and {@code b} being equal + */ public static Truthness getStringEqualityTruthness(String a, String b) { Objects.requireNonNull(a); Objects.requireNonNull(b); if (a.equals(b)) { - return new Truthness(1.0d, DistanceHelper.H_NOT_NULL); + return new Truthness(1d, DistanceHelper.H_NOT_NULL); } long dist = DistanceHelper.getLeftAlignmentDistance(a, b); double ofTrue = DistanceHelper.heuristicFromScaledDistanceWithBase(DistanceHelper.H_NOT_NULL, (double) dist); - return new Truthness(ofTrue, 1.0d); + return new Truthness(ofTrue, 1d); } } diff --git a/client-java/sql/src/main/java/org/evomaster/client/java/sql/heuristic/SqlExpressionEvaluator.java b/client-java/sql/src/main/java/org/evomaster/client/java/sql/heuristic/SqlExpressionEvaluator.java index af342f8517..ced3ac493a 100644 --- a/client-java/sql/src/main/java/org/evomaster/client/java/sql/heuristic/SqlExpressionEvaluator.java +++ b/client-java/sql/src/main/java/org/evomaster/client/java/sql/heuristic/SqlExpressionEvaluator.java @@ -12,7 +12,6 @@ import net.sf.jsqlparser.statement.select.AllTableColumns; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Select; -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 org.evomaster.client.java.instrumentation.shared.RegexSharedUtils; @@ -20,19 +19,20 @@ import org.evomaster.client.java.sql.heuristic.function.FunctionFinder; import org.evomaster.client.java.sql.heuristic.function.SqlAggregateFunction; import org.evomaster.client.java.sql.heuristic.function.SqlFunction; -import org.evomaster.client.java.sql.internal.*; - +import org.evomaster.client.java.sql.internal.TaintHandler; import java.sql.Timestamp; -import java.time.*; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.OffsetTime; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; import static org.evomaster.client.java.sql.heuristic.ConversionHelper.*; import static org.evomaster.client.java.sql.heuristic.SqlCastHelper.castTo; import static org.evomaster.client.java.sql.heuristic.SqlHeuristicsCalculator.*; -import static org.evomaster.client.java.distance.heuristics.TruthnessUtils.*; import static org.evomaster.client.java.sql.heuristic.SqlStringUtils.nullSafeEqualsIgnoreCase; public class SqlExpressionEvaluator extends ExpressionVisitorAdapter { @@ -396,20 +396,6 @@ private static double toDouble(Boolean booleanValue) { return (booleanValue ? 1d : 0d); } - public static Truthness getEqualityTruthness(String a, String b) { - Objects.requireNonNull(a); - Objects.requireNonNull(b); - - if (a.equals(b)) { - return TRUE_TRUTHNESS; - } else { - final double base = C; - final double distance = DistanceHelper.getLeftAlignmentDistance(a, b); - final double h = DistanceHelper.heuristicFromScaledDistanceWithBase(base, distance); - return new Truthness(h, 1d); - } - } - private Truthness calculateTruthnessForStringComparison(String leftString, String rightString, ComparisonOperatorType comparisonOperatorType) { Objects.requireNonNull(leftString); Objects.requireNonNull(rightString); @@ -419,9 +405,9 @@ private Truthness calculateTruthnessForStringComparison(String leftString, Strin if (taintHandler != null) { taintHandler.handleTaintForStringEquals(leftString, rightString, false); } - return getEqualityTruthness(leftString, rightString); + return TruthnessUtils.getStringEqualityTruthness(leftString, rightString); case NOT_EQUALS_TO: - return getEqualityTruthness(leftString, rightString).invert(); + return TruthnessUtils.getStringEqualityTruthness(leftString, rightString).invert(); case GREATER_THAN: return leftString.compareTo(rightString) > 0 ? TRUE_TRUTHNESS : FALSE_TRUTHNESS; case GREATER_THAN_EQUALS: From 8a566ee3e4351c0a4ccb961a9aa4bb39a90b3b5e Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Wed, 8 Jul 2026 20:18:54 -0300 Subject: [PATCH 8/8] Improve CqlParserUtils - More refactorings in parser utils - Add missing docs in parser utils --- .../cassandra/parser/CqlParserUtils.java | 149 ++++++++++++------ 1 file changed, 102 insertions(+), 47 deletions(-) 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 01f284ad17..9544cdbd4c 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 @@ -17,12 +17,26 @@ public class CqlParserUtils { private CqlParserUtils() {} + /** + * 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); @@ -32,25 +46,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"); } + /** + * @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"); } + /** + * @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"); } + /** + * 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; + } } /** @@ -61,70 +101,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 values = new ArrayList<>(); + if (rel.functionArgs() != null) { for (CqlParser.ConstantContext cc : rel.functionArgs().constant()) { values.add(parseConstant(cc)); } } return new InOperation(col, values); + } else { + // Comparison: col OP constant + CqlParser.ConstantContext constant = rel.constant(); + if (constant != null && rel.OBJECT_NAME(0) != null) { + String col = rel.OBJECT_NAME(0).getText(); + Object value = parseConstant(constant); + if (rel.OPERATOR_EQ() != null) { + return new EqualsOperation<>(col, value); + } else if (rel.OPERATOR_GT() != null) { + return new GreaterThanOperation<>(col, value); + } else if (rel.OPERATOR_GTE() != null) { + return new GreaterThanEqualsOperation<>(col, value); + } else if (rel.OPERATOR_LT() != null) { + return new LessThanOperation<>(col, value); + } else if (rel.OPERATOR_LTE() != null) { + return new LessThanEqualsOperation<>(col, value); + } else { + return null; + } + } else { + return null; + } } - - // Comparison: col OP constant - CqlParser.ConstantContext constant = rel.constant(); - if (constant != null && rel.OBJECT_NAME(0) != null) { - String col = rel.OBJECT_NAME(0).getText(); - Object value = parseConstant(constant); - if (rel.OPERATOR_EQ() != null) return new EqualsOperation<>(col, value); - if (rel.OPERATOR_GT() != null) return new GreaterThanOperation<>(col, value); - if (rel.OPERATOR_GTE() != null) return new GreaterThanEqualsOperation<>(col, value); - if (rel.OPERATOR_LT() != null) return new LessThanOperation<>(col, value); - if (rel.OPERATOR_LTE() != null) return new LessThanEqualsOperation<>(col, value); - } - - return null; } private static Object parseConstant(CqlParser.ConstantContext ctx) { - if (ctx.UUID() != null) return UUID.fromString(ctx.UUID().getText()); - if (ctx.stringLiteral() != null) { + if (ctx.UUID() != null) { + return UUID.fromString(ctx.UUID().getText()); + } else if (ctx.stringLiteral() != null) { String raw = ctx.stringLiteral().getText(); return raw.substring(1, raw.length() - 1); // strip surrounding single quotes + } else if (ctx.decimalLiteral() != null) { + return Long.parseLong(ctx.decimalLiteral().getText()); + } else if (ctx.floatLiteral() != null) { + return Double.parseDouble(ctx.floatLiteral().getText()); + } else if (ctx.booleanLiteral() != null) { + return ctx.booleanLiteral().getText().equalsIgnoreCase("true"); + } else if (ctx.durationLiteral() != null) { + return ctx.durationLiteral().getText(); + } else { + return null; // kwNull, codeBlock, hexadecimal } - if (ctx.decimalLiteral() != null) return Long.parseLong(ctx.decimalLiteral().getText()); - if (ctx.floatLiteral() != null) return Double.parseDouble(ctx.floatLiteral().getText()); - if (ctx.booleanLiteral() != null) return ctx.booleanLiteral().getText().equalsIgnoreCase("true"); - if (ctx.durationLiteral() != null) return ctx.durationLiteral().getText(); - return null; // kwNull, codeBlock, hexadecimal } } \ No newline at end of file