From 92aa301730ae5d4e0f903d96cae4dd10a99ceb3f Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 23 Jun 2026 21:59:18 -0300 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 6dd22678c689ee4efe44071ebfb839cc84f95c97 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 6 Jul 2026 17:40:36 -0300 Subject: [PATCH 07/14] Add more replacements for overloaded execute method --- .../CqlSessionClassReplacement.java | 56 ++++++++++-- .../CqlSessionClassReplacementTest.java | 86 +++++++++++++++++++ 2 files changed, 136 insertions(+), 6 deletions(-) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java index 641a68ebd0..b568ea1474 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java @@ -2,6 +2,7 @@ import org.evomaster.client.java.instrumentation.ExecutedCqlCommand; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement; +import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass; import org.evomaster.client.java.instrumentation.coverage.methodreplacement.UsageFilter; import org.evomaster.client.java.instrumentation.shared.ReplacementCategory; @@ -10,30 +11,52 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Map; public class CqlSessionClassReplacement extends ThirdPartyMethodReplacementClass { private static final CqlSessionClassReplacement singleton = new CqlSessionClassReplacement(); - public static final String CASSANDRA_FIND_STRING_SYNC = "cassandraExecuteStringSync"; + public static final String CASSANDRA_EXECUTE_STRING_SYNC = "cassandraExecuteStringSync"; + public static final String CASSANDRA_EXECUTE_STRING_POSITIONAL_VALUES_SYNC = "cassandraExecuteStringPositionalValuesSync"; + public static final String CASSANDRA_EXECUTE_STRING_NAMED_VALUES_SYNC = "cassandraExecuteStringNamedValuesSync"; + public static final String CASSANDRA_EXECUTE_STATEMENT_SYNC = "cassandraExecuteStatementSync"; + + private static final String RESULT_SET_CLASS = "com.datastax.oss.driver.api.core.cql.ResultSet"; + private static final String STATEMENT_CLASS = "com.datastax.oss.driver.api.core.cql.Statement"; @Override protected String getNameOfThirdPartyTargetClass() { return "com.datastax.oss.driver.api.core.CqlSession"; } - @Replacement(type = ReplacementType.TRACKER, id = CASSANDRA_FIND_STRING_SYNC, usageFilter = UsageFilter.ANY, category = ReplacementCategory.CASSANDRA, castTo = "com.datastax.oss.driver.api.core.cql.ResultSet") + @Replacement(type = ReplacementType.TRACKER, id = CASSANDRA_EXECUTE_STRING_SYNC, usageFilter = UsageFilter.ANY, category = ReplacementCategory.CASSANDRA, castTo = RESULT_SET_CLASS) public static Object execute(Object cqlSession, String query) { - return handleCqlExecute(CASSANDRA_FIND_STRING_SYNC, cqlSession, query); + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_SYNC, cqlSession, query, query); + } + + @Replacement(type = ReplacementType.TRACKER, id = CASSANDRA_EXECUTE_STRING_POSITIONAL_VALUES_SYNC, usageFilter = UsageFilter.ANY, category = ReplacementCategory.CASSANDRA, castTo = RESULT_SET_CLASS) + public static Object execute(Object cqlSession, String query, Object... values) { + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_POSITIONAL_VALUES_SYNC, cqlSession, query, query, values); + } + + @Replacement(type = ReplacementType.TRACKER, id = CASSANDRA_EXECUTE_STRING_NAMED_VALUES_SYNC, usageFilter = UsageFilter.ANY, category = ReplacementCategory.CASSANDRA, castTo = RESULT_SET_CLASS) + public static Object execute(Object cqlSession, String query, Map values) { + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_NAMED_VALUES_SYNC, cqlSession, query, query, values); + } + + @Replacement(type = ReplacementType.TRACKER, id = CASSANDRA_EXECUTE_STATEMENT_SYNC, usageFilter = UsageFilter.ANY, category = ReplacementCategory.CASSANDRA, castTo = RESULT_SET_CLASS) + public static Object execute(Object cqlSession, @ThirdPartyCast(actualType = STATEMENT_CLASS) Object statement) { + return handleCqlExecute(CASSANDRA_EXECUTE_STATEMENT_SYNC, cqlSession, extractQueryText(statement), statement); } - private static Object handleCqlExecute(String id, Object cqlSession, String query) { + private static Object handleCqlExecute(String id, Object cqlSession, String queryForTracking, Object... invokeArgs) { long start = System.currentTimeMillis(); try { Method executeMethod = retrieveExecuteMethod(id, cqlSession); - Object result = executeMethod.invoke(cqlSession, query); + Object result = executeMethod.invoke(cqlSession, invokeArgs); long end = System.currentTimeMillis(); long executionTime = end - start; - ExecutedCqlCommand info = new ExecutedCqlCommand(query, false, executionTime); + ExecutedCqlCommand info = new ExecutedCqlCommand(queryForTracking, false, executionTime); ExecutionTracer.addCqlInfo(info); return result; } catch (IllegalAccessException e) { @@ -43,6 +66,27 @@ private static Object handleCqlExecute(String id, Object cqlSession, String quer } } + /** + * Statement is a generic driver type; only SimpleStatement exposes the original + * CQL text directly, while BoundStatement requires going through its PreparedStatement. + */ + private static String extractQueryText(Object statement) { + try { + Method getQuery = statement.getClass().getMethod("getQuery"); + return (String) getQuery.invoke(statement); + } catch (ReflectiveOperationException e) { + // not a SimpleStatement (eg BoundStatement) -- fall through + } + try { + Method getPreparedStatement = statement.getClass().getMethod("getPreparedStatement"); + Object prepared = getPreparedStatement.invoke(statement); + Method getQuery = prepared.getClass().getMethod("getQuery"); + return (String) getQuery.invoke(prepared); + } catch (ReflectiveOperationException e) { + return statement.toString(); // eg BatchStatement -- best effort + } + } + private static Method retrieveExecuteMethod(String id, Object cqlSession){ return getOriginal(singleton, id, cqlSession); } diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java index 38a80e78d4..8337dfed4b 100644 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java @@ -1,6 +1,9 @@ package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; import org.evomaster.client.java.instrumentation.AdditionalInfo; import org.evomaster.client.java.instrumentation.ExecutedCqlCommand; import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; @@ -13,8 +16,11 @@ import java.net.InetSocketAddress; import java.time.Duration; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; @@ -140,4 +146,84 @@ void testExecutingInitCassandraFlagSuppressesTracking() { assertEquals(1, additionalInfoList.size()); assertTrue(additionalInfoList.get(0).getCqlInfoData().isEmpty()); } + + @Test + void testExecuteWithPositionalValuesIsTracked() { + String query = "INSERT INTO " + TABLE + " (id, name, age) VALUES (?, ?, ?)"; + + CqlSessionClassReplacement.execute(cqlSession, query, UUID.randomUUID(), "Dave", 40); + + List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, additionalInfoList.size()); + + Set commands = additionalInfoList.get(0).getCqlInfoData(); + assertEquals(1, commands.size()); + + ExecutedCqlCommand cmd = commands.iterator().next(); + assertEquals(query, cmd.getCqlCommand()); + assertFalse(cmd.hasThrownCqlException()); + assertTrue(cmd.getExecutionTime() >= 0); + } + + @Test + void testExecuteWithNamedValuesIsTracked() { + String query = "INSERT INTO " + TABLE + " (id, name, age) VALUES (:id, :name, :age)"; + Map values = new HashMap<>(); + values.put("id", UUID.randomUUID()); + values.put("name", "Erin"); + values.put("age", 22); + + CqlSessionClassReplacement.execute(cqlSession, query, values); + + List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, additionalInfoList.size()); + + Set commands = additionalInfoList.get(0).getCqlInfoData(); + assertEquals(1, commands.size()); + + ExecutedCqlCommand cmd = commands.iterator().next(); + assertEquals(query, cmd.getCqlCommand()); + assertFalse(cmd.hasThrownCqlException()); + assertTrue(cmd.getExecutionTime() >= 0); + } + + @Test + void testExecuteWithSimpleStatementIsTracked() { + String query = "SELECT * FROM " + TABLE; + SimpleStatement statement = SimpleStatement.newInstance(query); + + CqlSessionClassReplacement.execute(cqlSession, statement); + + List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, additionalInfoList.size()); + + Set commands = additionalInfoList.get(0).getCqlInfoData(); + assertEquals(1, commands.size()); + + ExecutedCqlCommand cmd = commands.iterator().next(); + assertEquals(query, cmd.getCqlCommand()); + assertFalse(cmd.hasThrownCqlException()); + assertTrue(cmd.getExecutionTime() >= 0); + } + + @Test + void testExecuteWithBoundStatementIsTracked() { + String query = "INSERT INTO " + TABLE + " (id, name, age) VALUES (?, ?, ?)"; + // Preparing directly on the session so it is NOT intercepted by the replacement + PreparedStatement prepared = cqlSession.prepare(query); + BoundStatement bound = prepared.bind(UUID.randomUUID(), "Frank", 33); + + CqlSessionClassReplacement.execute(cqlSession, bound); + + List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); + assertEquals(1, additionalInfoList.size()); + + Set commands = additionalInfoList.get(0).getCqlInfoData(); + assertEquals(1, commands.size()); + + ExecutedCqlCommand cmd = commands.iterator().next(); + assertEquals(query, cmd.getCqlCommand()); + assertFalse(cmd.hasThrownCqlException()); + assertTrue(cmd.getExecutionTime() >= 0); + } } From ec7e7f39d73e4ff452d847dd86e7b6b5f1c3653b Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 6 Jul 2026 17:59:57 -0300 Subject: [PATCH 08/14] Add separate fields for keyspace and table in ExecutedCqlCommand --- .../instrumentation/ExecutedCqlCommand.java | 22 +++++++- .../CqlSessionClassReplacement.java | 50 ++++++++++++++++++- .../CqlSessionClassReplacementTest.java | 15 +++++- 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/ExecutedCqlCommand.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/ExecutedCqlCommand.java index e4bbdd5901..ac976a2527 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/ExecutedCqlCommand.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/ExecutedCqlCommand.java @@ -18,6 +18,16 @@ public class ExecutedCqlCommand implements Serializable { */ private final String cqlCommand; + /** + * The keyspace the command targets, if it could be determined from the query text; null otherwise + */ + private final String keyspaceName; + + /** + * The table the command targets, if it could be determined from the query text; null otherwise + */ + private final String tableName; + /** * Whether the CQL command failed, for any reason */ @@ -28,8 +38,10 @@ public class ExecutedCqlCommand implements Serializable { */ private final long executionTime; - public ExecutedCqlCommand(String cqlCommand, boolean threwCqlException, long executionTime) { + public ExecutedCqlCommand(String cqlCommand, String keyspaceName, String tableName, boolean threwCqlException, long executionTime) { this.cqlCommand = cqlCommand; + this.keyspaceName = keyspaceName; + this.tableName = tableName; this.threwCqlException = threwCqlException; this.executionTime = executionTime; } @@ -38,6 +50,14 @@ public String getCqlCommand() { return cqlCommand; } + public String getKeyspaceName() { + return keyspaceName; + } + + public String getTableName() { + return tableName; + } + public boolean hasThrownCqlException() { return threwCqlException; } diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java index b568ea1474..76f8463e36 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java @@ -12,6 +12,8 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class CqlSessionClassReplacement extends ThirdPartyMethodReplacementClass { private static final CqlSessionClassReplacement singleton = new CqlSessionClassReplacement(); @@ -56,7 +58,8 @@ private static Object handleCqlExecute(String id, Object cqlSession, String quer Object result = executeMethod.invoke(cqlSession, invokeArgs); long end = System.currentTimeMillis(); long executionTime = end - start; - ExecutedCqlCommand info = new ExecutedCqlCommand(queryForTracking, false, executionTime); + TableReference ref = extractTableReference(queryForTracking); + ExecutedCqlCommand info = new ExecutedCqlCommand(queryForTracking, ref.keyspaceName, ref.tableName, false, executionTime); ExecutionTracer.addCqlInfo(info); return result; } catch (IllegalAccessException e) { @@ -66,6 +69,51 @@ private static Object handleCqlExecute(String id, Object cqlSession, String quer } } + /** + * Matches the keyspace/table reference after FROM (SELECT/DELETE), INTO (INSERT), or + * UPDATE, e.g. "FROM ks.tbl", "INTO tbl", "UPDATE \"Ks\".\"Tbl\"". + */ + private static final Pattern TABLE_REFERENCE_PATTERN = Pattern.compile( + "(?i)\\b(?:FROM|INTO|UPDATE)\\s+(\"[^\"]+\"|[A-Za-z_]\\w*)(?:\\s*\\.\\s*(\"[^\"]+\"|[A-Za-z_]\\w*))?" + ); + + /** + * Best-effort extraction of keyspace/table from the CQL text. Returns both fields as + * null when the query doesn't match a recognised SELECT/INSERT/UPDATE/DELETE shape + * (eg DDL, batches). + */ + private static TableReference extractTableReference(String query) { + if (query == null) { + return new TableReference(null, null); + } + Matcher matcher = TABLE_REFERENCE_PATTERN.matcher(query); + if (!matcher.find()) { + return new TableReference(null, null); + } + String first = stripQuotes(matcher.group(1)); + String second = matcher.group(2) != null ? stripQuotes(matcher.group(2)) : null; + // if there is a "a.b" qualifier, a is the keyspace and b is the table; + // otherwise the single identifier is the table, and the keyspace is the session default + return second != null ? new TableReference(first, second) : new TableReference(null, first); + } + + private static String stripQuotes(String identifier) { + if (identifier.length() >= 2 && identifier.charAt(0) == '"' && identifier.charAt(identifier.length() - 1) == '"') { + return identifier.substring(1, identifier.length() - 1); + } + return identifier; + } + + private static class TableReference { + final String keyspaceName; + final String tableName; + + TableReference(String keyspaceName, String tableName) { + this.keyspaceName = keyspaceName; + this.tableName = tableName; + } + } + /** * Statement is a generic driver type; only SimpleStatement exposes the original * CQL text directly, while BoundStatement requires going through its PreparedStatement. diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java index 8337dfed4b..4c65bc797b 100644 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java @@ -37,7 +37,8 @@ public class CqlSessionClassReplacementTest { .withStartupTimeout(Duration.ofMinutes(2))); private static final String KEYSPACE = "testks"; - private static final String TABLE = KEYSPACE + ".users"; + private static final String TABLE_NAME = "users"; + private static final String TABLE = KEYSPACE + "." + TABLE_NAME; @BeforeAll static void startCassandra() { @@ -86,6 +87,8 @@ void testExecuteSelectIsTracked() { ExecutedCqlCommand cmd = commands.iterator().next(); assertEquals(query, cmd.getCqlCommand()); + assertEquals(KEYSPACE, cmd.getKeyspaceName()); + assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); assertTrue(cmd.getExecutionTime() >= 0); } @@ -104,6 +107,8 @@ void testExecuteInsertIsTracked() { ExecutedCqlCommand cmd = commands.iterator().next(); assertEquals(query, cmd.getCqlCommand()); + assertEquals(KEYSPACE, cmd.getKeyspaceName()); + assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); assertTrue(cmd.getExecutionTime() >= 0); } @@ -161,6 +166,8 @@ void testExecuteWithPositionalValuesIsTracked() { ExecutedCqlCommand cmd = commands.iterator().next(); assertEquals(query, cmd.getCqlCommand()); + assertEquals(KEYSPACE, cmd.getKeyspaceName()); + assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); assertTrue(cmd.getExecutionTime() >= 0); } @@ -183,6 +190,8 @@ void testExecuteWithNamedValuesIsTracked() { ExecutedCqlCommand cmd = commands.iterator().next(); assertEquals(query, cmd.getCqlCommand()); + assertEquals(KEYSPACE, cmd.getKeyspaceName()); + assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); assertTrue(cmd.getExecutionTime() >= 0); } @@ -202,6 +211,8 @@ void testExecuteWithSimpleStatementIsTracked() { ExecutedCqlCommand cmd = commands.iterator().next(); assertEquals(query, cmd.getCqlCommand()); + assertEquals(KEYSPACE, cmd.getKeyspaceName()); + assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); assertTrue(cmd.getExecutionTime() >= 0); } @@ -223,6 +234,8 @@ void testExecuteWithBoundStatementIsTracked() { ExecutedCqlCommand cmd = commands.iterator().next(); assertEquals(query, cmd.getCqlCommand()); + assertEquals(KEYSPACE, cmd.getKeyspaceName()); + assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); assertTrue(cmd.getExecutionTime() >= 0); } From 288be95f0cc79bba8ac055279184ee5cc16d30ba Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Wed, 8 Jul 2026 17:32:35 -0300 Subject: [PATCH 09/14] 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 10/14] 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 From d19881f31dc5706d0858ca14bd3f42231164403d Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Thu, 9 Jul 2026 01:45:28 -0300 Subject: [PATCH 11/14] Divide regex to improve readability --- .../CqlSessionClassReplacement.java | 41 ++++++++++++------- .../CqlSessionClassReplacementTest.java | 6 +-- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java index 76f8463e36..59a2bbd771 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacement.java @@ -26,6 +26,23 @@ public class CqlSessionClassReplacement extends ThirdPartyMethodReplacementClass private static final String RESULT_SET_CLASS = "com.datastax.oss.driver.api.core.cql.ResultSet"; private static final String STATEMENT_CLASS = "com.datastax.oss.driver.api.core.cql.Statement"; + // a CQL identifier: either quoted+case-sensitive (e.g. "Tbl") or unquoted (e.g. tbl) + private static final String IDENTIFIER = "(?:\"[^\"]+\"|[A-Za-z_]\\w*)"; + // clause keywords that introduce a table reference (SELECT/DELETE ... FROM, INSERT ... INTO, UPDATE ...) + private static final String CLAUSE_KEYWORDS = "(?:FROM|INTO|UPDATE)"; + + /** + * Matches the keyspace/table reference after FROM (SELECT/DELETE), INTO (INSERT), or + * UPDATE, e.g. "FROM ks.tbl", "INTO tbl", "UPDATE \"Ks\".\"Tbl\"". Group "first" is the + * keyspace when a "keyspace.table" qualifier ("second") is present, otherwise it's the + * table name on its own. + */ + private static final Pattern TABLE_REFERENCE_PATTERN = Pattern.compile( + "(?i)\\b" + CLAUSE_KEYWORDS + "\\s+" + + "(?" + IDENTIFIER + ")" + + "(?:\\s*\\.\\s*(?" + IDENTIFIER + "))?" + ); + @Override protected String getNameOfThirdPartyTargetClass() { return "com.datastax.oss.driver.api.core.CqlSession"; @@ -69,14 +86,6 @@ private static Object handleCqlExecute(String id, Object cqlSession, String quer } } - /** - * Matches the keyspace/table reference after FROM (SELECT/DELETE), INTO (INSERT), or - * UPDATE, e.g. "FROM ks.tbl", "INTO tbl", "UPDATE \"Ks\".\"Tbl\"". - */ - private static final Pattern TABLE_REFERENCE_PATTERN = Pattern.compile( - "(?i)\\b(?:FROM|INTO|UPDATE)\\s+(\"[^\"]+\"|[A-Za-z_]\\w*)(?:\\s*\\.\\s*(\"[^\"]+\"|[A-Za-z_]\\w*))?" - ); - /** * Best-effort extraction of keyspace/table from the CQL text. Returns both fields as * null when the query doesn't match a recognised SELECT/INSERT/UPDATE/DELETE shape @@ -86,22 +95,25 @@ private static TableReference extractTableReference(String query) { if (query == null) { return new TableReference(null, null); } + Matcher matcher = TABLE_REFERENCE_PATTERN.matcher(query); if (!matcher.find()) { return new TableReference(null, null); + } else { + String first = stripQuotes(matcher.group("first")); + String second = matcher.group("second") != null ? stripQuotes(matcher.group("second")) : null; + // if there is an "a.b" qualifier, "a" is the keyspace and "b" is the table; + // otherwise the single identifier is the table, and the keyspace is the session default + return second != null ? new TableReference(first, second) : new TableReference(null, first); } - String first = stripQuotes(matcher.group(1)); - String second = matcher.group(2) != null ? stripQuotes(matcher.group(2)) : null; - // if there is a "a.b" qualifier, a is the keyspace and b is the table; - // otherwise the single identifier is the table, and the keyspace is the session default - return second != null ? new TableReference(first, second) : new TableReference(null, first); } private static String stripQuotes(String identifier) { if (identifier.length() >= 2 && identifier.charAt(0) == '"' && identifier.charAt(identifier.length() - 1) == '"') { return identifier.substring(1, identifier.length() - 1); + } else { + return identifier; } - return identifier; } private static class TableReference { @@ -125,6 +137,7 @@ private static String extractQueryText(Object statement) { } catch (ReflectiveOperationException e) { // not a SimpleStatement (eg BoundStatement) -- fall through } + try { Method getPreparedStatement = statement.getClass().getMethod("getPreparedStatement"); Object prepared = getPreparedStatement.invoke(statement); diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java index 4c65bc797b..7cd2a85532 100644 --- a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/coverage/methodreplacement/thirdpartyclasses/CqlSessionClassReplacementTest.java @@ -16,11 +16,7 @@ import java.net.InetSocketAddress; import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import static org.junit.jupiter.api.Assertions.*; From 2cc704eba609c6bbd171dbcd6cb1cdec76a406d5 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Mon, 13 Jul 2026 02:17:09 -0300 Subject: [PATCH 12/14] Evaluator and parsers refactorings --- .../CassandraOperationEvaluator.java | 64 +++++++++++---- .../parser/CqlDurationLiteralParser.java | 78 +++++++++++++++---- .../cassandra/parser/CqlParserUtils.java | 10 ++- 3 files changed, 118 insertions(+), 34 deletions(-) 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 index 3ccf97c12d..e2275e4917 100644 --- 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 @@ -6,7 +6,6 @@ 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.*; @@ -14,6 +13,7 @@ import java.time.format.DateTimeParseException; import java.time.temporal.TemporalAccessor; import java.util.*; +import java.util.function.Function; import static org.evomaster.client.java.controller.cassandra.calculator.CassandraHeuristicsCalculator.*; @@ -27,6 +27,17 @@ public class CassandraOperationEvaluator { private enum ComparisonType { EQUALS, GT, GTE, LT, LTE } + /** + * Each entry attempts to parse a timestamp string under a different format, returning null + * if that format doesn't match, so {@link #parseTimestampString} can fall through to the next one. + */ + private static final List> TIMESTAMP_PARSERS = Arrays.asList( + CassandraOperationEvaluator::tryParseIsoInstant, + CassandraOperationEvaluator::tryParseWithTimestampFormatters, + CassandraOperationEvaluator::tryParseDateWithOffset, + CassandraOperationEvaluator::tryParseDateOnly + ); + private static final DateTimeFormatter[] TIMESTAMP_FORMATTERS = { DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSXX"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXX"), @@ -155,12 +166,9 @@ private Truthness compareByType(Object rowValue, Object literalValue, Comparison return compareTemporal(rowValue, literalValue, comparisonType); } else if (isCqlDuration(rowValue)) { return compareDuration(rowValue, literalValue, comparisonType); + } else { + throw new IllegalArgumentException("Unsupported row value type: " + rowValue.getClass().getName()); } - - SimpleLogger.uniqueWarn("CassandraHeuristicsCalculator: unsupported type " - + rowValue.getClass().getName() + " — returning FALSE_TRUTHNESS"); - - return FALSE_TRUTHNESS; } private Truthness compareNumeric(Object rowValue, Object literalValue, ComparisonType comparisonType) { @@ -271,29 +279,57 @@ private static long toLong(Object rowValue, Object rowValueHint) { } private static Instant parseTimestampString(String rowValue) { + for (Function parser : TIMESTAMP_PARSERS) { + Instant parsed = parser.apply(rowValue); + if (parsed != null) { + return parsed; + } + } + + throw new IllegalArgumentException("Cannot parse timestamp string: " + rowValue); + } + + private static Instant tryParseIsoInstant(String rowValue) { try { return Instant.parse(rowValue); - } catch (DateTimeParseException ignored) {} + } catch (DateTimeParseException e) { + return null; + } + } + private static Instant tryParseWithTimestampFormatters(String rowValue) { for (DateTimeFormatter formatter : TIMESTAMP_FORMATTERS) { try { return OffsetDateTime.parse(rowValue, formatter).toInstant(); } catch (DateTimeParseException ignored) {} } + return null; + } - // date-only with offset ("2011-02-03+0000"): OffsetDateTime.parse fails without a time - // component, so extract LocalDate and ZoneOffset from the TemporalAccessor directly. + /** + * Parses date-only strings with an offset (e.g. {@code "2011-02-03+0000"}), which {@link + * OffsetDateTime#parse} rejects since they carry no time component. The {@link LocalDate} + * and {@link ZoneOffset} are extracted from the parsed {@link TemporalAccessor} directly. + */ + private static Instant tryParseDateWithOffset(String rowValue) { try { TemporalAccessor accessor = DATE_WITH_OFFSET.parse(rowValue); return LocalDate.from(accessor).atStartOfDay(ZoneOffset.from(accessor)).toInstant(); - } catch (DateTimeParseException ignored) {} + } catch (DateTimeParseException e) { + return null; + } + } - // date-only without offset ("2011-02-03"): treat as midnight UTC + /** + * Parses date-only strings without an offset (e.g. {@code "2011-02-03"}), treating them as + * midnight UTC. + */ + private static Instant tryParseDateOnly(String rowValue) { try { return LocalDate.parse(rowValue).atStartOfDay(ZoneOffset.UTC).toInstant(); - } catch (DateTimeParseException ignored) {} - - throw new IllegalArgumentException("Cannot parse timestamp string: " + rowValue); + } catch (DateTimeParseException e) { + return null; + } } private static boolean isCqlDuration(Object rowValue) { diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java index da4e1f1b21..83e83868fb 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java @@ -18,13 +18,38 @@ public class CqlDurationLiteralParser { private CqlDurationLiteralParser() {} + private static final String UNIT_MONTH = "mo"; + private static final String UNIT_MILLISECOND = "ms"; + private static final String UNIT_MICROSECOND_MU = "µs"; + private static final String UNIT_MICROSECOND = "us"; + private static final String UNIT_NANOSECOND = "ns"; + private static final String UNIT_YEAR = "y"; + private static final String UNIT_DAY = "d"; + private static final String UNIT_HOUR = "h"; + private static final String UNIT_MINUTE = "m"; + private static final String UNIT_SECOND = "s"; + /** * 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 + * longest-first ({@code mo, ms, µs, us, 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); + private static final Pattern TOKEN = Pattern.compile( + "(\\d+)(" + String.join("|", UNIT_MONTH, UNIT_MILLISECOND, UNIT_MICROSECOND_MU, + UNIT_MICROSECOND, UNIT_NANOSECOND, UNIT_YEAR, UNIT_DAY, UNIT_HOUR, UNIT_MINUTE, UNIT_SECOND) + ")", + Pattern.CASE_INSENSITIVE); + + private static final int MONTHS_PER_YEAR = 12; + private static final long NANOS_PER_HOUR = 3_600_000_000_000L; + private static final long NANOS_PER_MINUTE = 60_000_000_000L; + private static final long NANOS_PER_SECOND = 1_000_000_000L; + private static final long NANOS_PER_MILLISECOND = 1_000_000L; + private static final long NANOS_PER_MICROSECOND = 1_000L; + + private static final char ISO_TIME_SEPARATOR = 'T'; + + private static final String ISO_DURATION_PREFIX_UPPER = "P"; + private static final String ISO_DURATION_PREFIX_LOWER = "p"; /** * Parses a CQL duration literal into a {@link CqlDurationLiteral}, auto-detecting the @@ -42,7 +67,7 @@ public static CqlDurationLiteral parse(String stringDuration) { } String t = stringDuration.trim(); - if (t.startsWith("P") || t.startsWith("p")) { + if (t.startsWith(ISO_DURATION_PREFIX_UPPER) || t.startsWith(ISO_DURATION_PREFIX_LOWER)) { return parseIso(t); } @@ -52,7 +77,7 @@ public static CqlDurationLiteral parse(String stringDuration) { private static CqlDurationLiteral parseIso(String isoDuration) { // Split on T (or t) to separate date and time parts String upper = isoDuration.toUpperCase(); - int tIndex = upper.indexOf('T'); + int tIndex = upper.indexOf(ISO_TIME_SEPARATOR); int months = 0; int days = 0; @@ -73,7 +98,7 @@ private static CqlDurationLiteral parseIso(String isoDuration) { days = p.getDays(); } if (timePart.length() > 1) { - Duration d = Duration.parse("P" + timePart); + Duration d = Duration.parse(ISO_DURATION_PREFIX_UPPER + timePart); nanos = d.toNanos(); } } @@ -90,17 +115,36 @@ private static CqlDurationLiteral parseCassandra(String text) { 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; + case UNIT_YEAR: + months += (int) (value * MONTHS_PER_YEAR); + break; + case UNIT_MONTH: + months += (int) value; + break; + case UNIT_DAY: + days += (int) value; + break; + case UNIT_HOUR: + nanos += value * NANOS_PER_HOUR; + break; + case UNIT_MINUTE: + nanos += value * NANOS_PER_MINUTE; + break; + case UNIT_SECOND: + nanos += value * NANOS_PER_SECOND; + break; + case UNIT_MILLISECOND: + nanos += value * NANOS_PER_MILLISECOND; + break; + case UNIT_MICROSECOND: + case UNIT_MICROSECOND_MU: + nanos += value * NANOS_PER_MICROSECOND; + break; + case UNIT_NANOSECOND: + nanos += value; + break; + default: + break; } } return new CqlDurationLiteral(months, days, nanos); 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 9544cdbd4c..2636e454e3 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlParserUtils.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlParserUtils.java @@ -17,6 +17,10 @@ public class CqlParserUtils { private CqlParserUtils() {} + private static final String KEYWORD_SELECT = "SELECT"; + private static final String KEYWORD_UPDATE = "UPDATE"; + private static final String KEYWORD_DELETE = "DELETE"; + /** * Parses a CQL command string into its ANTLR parse tree, with no validity checks * performed on the result (assumes {@link #canParseCqlCommand} was used first). @@ -51,7 +55,7 @@ public static boolean canParseCqlCommand(String cqlCommand) { * @return {@code true} if {@code cqlCommand} is a SELECT statement */ public static boolean isSelect(String cqlCommand) { - return cqlCommand.trim().toUpperCase().startsWith("SELECT"); + return cqlCommand.trim().toUpperCase().startsWith(KEYWORD_SELECT); } /** @@ -59,7 +63,7 @@ public static boolean isSelect(String cqlCommand) { * @return {@code true} if {@code cqlCommand} is an UPDATE statement */ public static boolean isUpdate(String cqlCommand) { - return cqlCommand.trim().toUpperCase().startsWith("UPDATE"); + return cqlCommand.trim().toUpperCase().startsWith(KEYWORD_UPDATE); } /** @@ -67,7 +71,7 @@ public static boolean isUpdate(String cqlCommand) { * @return {@code true} if {@code cqlCommand} is a DELETE statement */ public static boolean isDelete(String cqlCommand) { - return cqlCommand.trim().toUpperCase().startsWith("DELETE"); + return cqlCommand.trim().toUpperCase().startsWith(KEYWORD_DELETE); } /** From d2179accb54ab6b4c54df2250cbcfc502c368ab7 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 14 Jul 2026 17:29:50 -0300 Subject: [PATCH 13/14] Improve parsing and comparison of Duration values: - Use pattern for standard ISO - Improve CqlDurationLiteralParser and create unit tests class - Replace reflection with use of TemporalAmount --- .../CassandraOperationEvaluator.java | 46 ++-- .../parser/CqlDurationLiteralParser.java | 204 ++++++++++----- .../parser/CqlDurationLiteralParserTest.java | 234 ++++++++++++++++++ 3 files changed, 403 insertions(+), 81 deletions(-) create mode 100644 client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParserTest.java 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 index e2275e4917..c6570a170a 100644 --- 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 @@ -11,7 +11,10 @@ import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; import java.util.*; import java.util.function.Function; @@ -332,42 +335,37 @@ private static Instant tryParseDateOnly(String rowValue) { } } + /** + * Identifies the driver's {@code CqlDuration} by checking for its distinctive combination of supported temporal + * units rather than {@code instanceof}, since other JDK types (e.g. {@link Period}) also implement {@link TemporalAmount}. + */ private static boolean isCqlDuration(Object rowValue) { - 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); + if (!(rowValue instanceof TemporalAmount)) { + return false; + } else { + List units = ((TemporalAmount) rowValue).getUnits(); + return units.contains(ChronoUnit.MONTHS) + && units.contains(ChronoUnit.DAYS) + && units.contains(ChronoUnit.NANOS); + } } private Truthness compareDuration(Object rowValue, Object literalValue, ComparisonType comparisonType) { if (comparisonType != ComparisonType.EQUALS) { throw new IllegalArgumentException("Unsupported operator for duration literals: " + comparisonType); - } - - try { - int months = getDurationMonths(rowValue); - int days = getDurationDays(rowValue); - long nanos = getDurationNanos(rowValue); + } else { + TemporalAmount duration = (TemporalAmount) rowValue; + long months = duration.get(ChronoUnit.MONTHS); + long days = duration.get(ChronoUnit.DAYS); + long nanos = duration.get(ChronoUnit.NANOS); CqlDurationLiteral literalDurationValue = CqlDurationLiteralParser.parse((String) literalValue); return TruthnessUtils.buildAndAggregationTruthness( - TruthnessUtils.getEqualityTruthness((long) months, (long) literalDurationValue.months), - TruthnessUtils.getEqualityTruthness((long) days, (long) literalDurationValue.days), + TruthnessUtils.getEqualityTruthness(months, literalDurationValue.months), + TruthnessUtils.getEqualityTruthness(days, literalDurationValue.days), TruthnessUtils.getEqualityTruthness(nanos, literalDurationValue.nanos) ); - } catch (Exception e) { - return FALSE_TRUTHNESS; } } diff --git a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java index 83e83868fb..11ddce2c76 100644 --- a/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java +++ b/client-java/controller/src/main/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParser.java @@ -2,17 +2,22 @@ import org.evomaster.client.java.controller.cassandra.model.CqlDurationLiteral; -import java.time.Duration; -import java.time.Period; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 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) + *

Supports four formats, all optionally prefixed with {@code -} for a negative duration, + * mirroring {@code com.datastax.oss.driver.api.core.data.CqlDuration#from(String)}: + * + *

    + *
  • Standard Cassandra: {@code 1y2mo3w4d5h6m7s} (units, including weeks, may appear in any + * order) + *
  • ISO 8601: {@code P1Y2M3DT4H5M6S} + *
  • ISO 8601, weeks only: {@code P3W} + *
  • ISO 8601, alternative format: {@code P0001-02-03T04:05:06} + *
*/ public class CqlDurationLiteralParser { @@ -24,22 +29,14 @@ private CqlDurationLiteralParser() {} private static final String UNIT_MICROSECOND = "us"; private static final String UNIT_NANOSECOND = "ns"; private static final String UNIT_YEAR = "y"; + private static final String UNIT_WEEK = "w"; private static final String UNIT_DAY = "d"; private static final String UNIT_HOUR = "h"; private static final String UNIT_MINUTE = "m"; private static final String UNIT_SECOND = "s"; - /** - * Matches an optional digit sequence followed by a unit token. Units are ordered - * longest-first ({@code mo, ms, µs, us, 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+)(" + String.join("|", UNIT_MONTH, UNIT_MILLISECOND, UNIT_MICROSECOND_MU, - UNIT_MICROSECOND, UNIT_NANOSECOND, UNIT_YEAR, UNIT_DAY, UNIT_HOUR, UNIT_MINUTE, UNIT_SECOND) + ")", - Pattern.CASE_INSENSITIVE); - private static final int MONTHS_PER_YEAR = 12; + private static final int DAYS_PER_WEEK = 7; private static final long NANOS_PER_HOUR = 3_600_000_000_000L; private static final long NANOS_PER_MINUTE = 60_000_000_000L; private static final long NANOS_PER_SECOND = 1_000_000_000L; @@ -47,70 +44,110 @@ private CqlDurationLiteralParser() {} private static final long NANOS_PER_MICROSECOND = 1_000L; private static final char ISO_TIME_SEPARATOR = 'T'; + private static final char ISO_YEAR_DESIGNATOR = 'Y'; + private static final char ISO_MONTH_OR_MINUTE_DESIGNATOR = 'M'; + private static final char ISO_DAY_DESIGNATOR = 'D'; + private static final char ISO_HOUR_DESIGNATOR = 'H'; + private static final char ISO_SECOND_DESIGNATOR = 'S'; private static final String ISO_DURATION_PREFIX_UPPER = "P"; private static final String ISO_DURATION_PREFIX_LOWER = "p"; + private static final String ISO_WEEK_SUFFIX = "W"; + + /** + * Matches the standard Cassandra format: an optional digit sequence followed by a unit + * token. Units are ordered longest-first ({@code mo, ms, µs, us, ns, y, w, d, h, m, s}) to + * avoid {@code m} consuming {@code mo}, or {@code ms} consuming {@code m}. + */ + private static final Pattern STANDARD_PATTERN = Pattern.compile( + "(\\d+)(" + String.join("|", UNIT_MONTH, UNIT_MILLISECOND, UNIT_MICROSECOND_MU, + UNIT_MICROSECOND, UNIT_NANOSECOND, UNIT_YEAR, UNIT_WEEK, UNIT_DAY, UNIT_HOUR, UNIT_MINUTE, UNIT_SECOND) + ")", + Pattern.CASE_INSENSITIVE); /** - * 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}). + * Matches the general ISO 8601 format, e.g. {@code P1Y2M3DT4H5M6S}. Every component is + * optional, mirroring the driver's own grammar, so bare {@code P} or {@code PT} are valid + * (zero-length) durations. + */ + private static final Pattern ISO8601_PATTERN = Pattern.compile( + ISO_DURATION_PREFIX_UPPER + + "(?:(\\d+)" + ISO_YEAR_DESIGNATOR + ")?" + + "(?:(\\d+)" + ISO_MONTH_OR_MINUTE_DESIGNATOR + ")?" + + "(?:(\\d+)" + ISO_DAY_DESIGNATOR + ")?" + + "(?:" + ISO_TIME_SEPARATOR + + "(?:(\\d+)" + ISO_HOUR_DESIGNATOR + ")?" + + "(?:(\\d+)" + ISO_MONTH_OR_MINUTE_DESIGNATOR + ")?" + + "(?:(\\d+)" + ISO_SECOND_DESIGNATOR + ")?)?"); + + /** Matches the ISO 8601 weeks-only format, e.g. {@code P3W}. */ + private static final Pattern ISO8601_WEEK_PATTERN = + Pattern.compile(ISO_DURATION_PREFIX_UPPER + "(\\d+)" + ISO_WEEK_SUFFIX); + + /** Matches the ISO 8601 alternative format, e.g. {@code P0001-02-03T04:05:06}. */ + private static final Pattern ISO8601_ALTERNATIVE_PATTERN = Pattern.compile( + ISO_DURATION_PREFIX_UPPER + "(\\d{4})-(\\d{2})-(\\d{2})" + ISO_TIME_SEPARATOR + "(\\d{2}):(\\d{2}):(\\d{2})"); + + /** + * Parses a CQL duration literal into a {@link CqlDurationLiteral}. A leading {@code -} is + * stripped and applied as a sign to all three components. The remainder is dispatched to the + * matching pattern-specific parser by {@link #parseUnsigned}. * - * @param stringDuration the duration literal to parse; must not be {@code null} or empty + * @param stringDuration the duration literal to parse; must not be {@code null}, empty, or + * blank * @return the parsed duration, decomposed into months, days and nanoseconds - * @throws IllegalArgumentException if {@code stringDuration} is {@code null} or empty + * @throws IllegalArgumentException if {@code stringDuration} is {@code null}, empty, or blank */ public static CqlDurationLiteral parse(String stringDuration) { - if (stringDuration == null || stringDuration.isEmpty()) { + if (stringDuration == null) { throw new IllegalArgumentException("Empty duration literal"); + } else { + String t = stringDuration.trim(); + if (t.isEmpty()) { + throw new IllegalArgumentException("Empty duration literal"); + } else { + boolean isNegative = t.startsWith("-"); + String unsigned = isNegative ? t.substring(1) : t; + if (unsigned.isEmpty()) { + throw new IllegalArgumentException("Empty duration literal"); + } else { + CqlDurationLiteral magnitude = parseUnsigned(unsigned); + + return isNegative + ? new CqlDurationLiteral(-magnitude.months, -magnitude.days, -magnitude.nanos) + : magnitude; + } + } } - - String t = stringDuration.trim(); - if (t.startsWith(ISO_DURATION_PREFIX_UPPER) || t.startsWith(ISO_DURATION_PREFIX_LOWER)) { - return parseIso(t); - } - - return parseCassandra(t); } - private static CqlDurationLiteral parseIso(String isoDuration) { - // Split on T (or t) to separate date and time parts - String upper = isoDuration.toUpperCase(); - int tIndex = upper.indexOf(ISO_TIME_SEPARATOR); - - 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(); + /** + * Routes to one of the four pattern-specific parsers, mirroring the format detection in the + * driver's {@code CqlDuration.from(String)}: non-{@code P}-prefixed input is the standard + * Cassandra format; {@code P}-prefixed input ending in {@code W} is the ISO week-only format; + * {@code P}-prefixed input containing {@code -} is the ISO alternative format; anything else + * {@code P}-prefixed is the general ISO 8601 format. + */ + private static CqlDurationLiteral parseUnsigned(String unsigned) { + if (!unsigned.startsWith(ISO_DURATION_PREFIX_UPPER) && !unsigned.startsWith(ISO_DURATION_PREFIX_LOWER)) { + return parseStandardPattern(unsigned); } else { - String 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(ISO_DURATION_PREFIX_UPPER + timePart); - nanos = d.toNanos(); + String upper = unsigned.toUpperCase(); + if (upper.endsWith(ISO_WEEK_SUFFIX)) { + return parseIso8601WeekPattern(upper); + } else if (upper.contains("-")) { + return parseIso8601AlternativePattern(upper); + } else { + return parseIso8601Pattern(upper); } } - return new CqlDurationLiteral(months, days, nanos); } - private static CqlDurationLiteral parseCassandra(String text) { + private static CqlDurationLiteral parseStandardPattern(String text) { int months = 0; int days = 0; long nanos = 0L; - Matcher m = TOKEN.matcher(text); + Matcher m = STANDARD_PATTERN.matcher(text); while (m.find()) { long value = Long.parseLong(m.group(1)); String unit = m.group(2).toLowerCase(); @@ -121,6 +158,9 @@ private static CqlDurationLiteral parseCassandra(String text) { case UNIT_MONTH: months += (int) value; break; + case UNIT_WEEK: + days += (int) (value * DAYS_PER_WEEK); + break; case UNIT_DAY: days += (int) value; break; @@ -149,4 +189,54 @@ private static CqlDurationLiteral parseCassandra(String text) { } return new CqlDurationLiteral(months, days, nanos); } + + private static CqlDurationLiteral parseIso8601Pattern(String isoDuration) { + Matcher matcher = ISO8601_PATTERN.matcher(isoDuration); + if (!matcher.matches()) { + throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'"); + } else { + int years = groupAsInt(matcher, 1); + int months = groupAsInt(matcher, 2); + int days = groupAsInt(matcher, 3); + int hours = groupAsInt(matcher, 4); + int minutes = groupAsInt(matcher, 5); + int seconds = groupAsInt(matcher, 6); + + long nanos = hours * NANOS_PER_HOUR + minutes * NANOS_PER_MINUTE + seconds * NANOS_PER_SECOND; + return new CqlDurationLiteral(years * MONTHS_PER_YEAR + months, days, nanos); + } + } + + /** Returns the matched group as an {@code int}, or {@code 0} if the (optional) group didn't participate. */ + private static int groupAsInt(Matcher matcher, int group) { + String value = matcher.group(group); + return value == null ? 0 : Integer.parseInt(value); + } + + private static CqlDurationLiteral parseIso8601WeekPattern(String isoDuration) { + Matcher matcher = ISO8601_WEEK_PATTERN.matcher(isoDuration); + if (!matcher.matches()) { + throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'"); + } else { + int weeks = Integer.parseInt(matcher.group(1)); + return new CqlDurationLiteral(0, weeks * DAYS_PER_WEEK, 0L); + } + } + + private static CqlDurationLiteral parseIso8601AlternativePattern(String isoDuration) { + Matcher matcher = ISO8601_ALTERNATIVE_PATTERN.matcher(isoDuration); + if (!matcher.matches()) { + throw new IllegalArgumentException("Unable to parse duration literal '" + isoDuration + "'"); + } else { + int years = Integer.parseInt(matcher.group(1)); + int months = Integer.parseInt(matcher.group(2)); + int days = Integer.parseInt(matcher.group(3)); + int hours = Integer.parseInt(matcher.group(4)); + int minutes = Integer.parseInt(matcher.group(5)); + int seconds = Integer.parseInt(matcher.group(6)); + + long nanos = hours * NANOS_PER_HOUR + minutes * NANOS_PER_MINUTE + seconds * NANOS_PER_SECOND; + return new CqlDurationLiteral(years * MONTHS_PER_YEAR + months, days, nanos); + } + } } \ No newline at end of file diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParserTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParserTest.java new file mode 100644 index 0000000000..7dd0babf63 --- /dev/null +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/parser/CqlDurationLiteralParserTest.java @@ -0,0 +1,234 @@ +package org.evomaster.client.java.controller.cassandra.parser; + +import org.evomaster.client.java.controller.cassandra.model.CqlDurationLiteral; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class CqlDurationLiteralParserTest { + + private static final long NANOS_PER_HOUR = 3_600_000_000_000L; + private static final long NANOS_PER_MINUTE = 60_000_000_000L; + private static final long NANOS_PER_SECOND = 1_000_000_000L; + private static final long NANOS_PER_MILLISECOND = 1_000_000L; + private static final long NANOS_PER_MICROSECOND = 1_000L; + + private static void assertDuration(CqlDurationLiteral actual, int months, int days, long nanos) { + assertEquals(months, actual.months); + assertEquals(days, actual.days); + assertEquals(nanos, actual.nanos); + } + + // ------------------------------------------------------------------------- + // Standard Cassandra format + // ------------------------------------------------------------------------- + + @Test + void standardFormat_singleUnit_parsesDays() { + assertDuration(CqlDurationLiteralParser.parse("3d"), 0, 3, 0); + } + + @Test + void standardFormat_multipleUnitsCombined_parsesAllComponents() { + long expectedNanos = 4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND; + assertDuration(CqlDurationLiteralParser.parse("1y2mo3d4h5m6s"), 14, 3, expectedNanos); + } + + @Test + void standardFormat_subSecondUnits_parsesNanos() { + long expectedNanos = 7 * NANOS_PER_MILLISECOND + 8 * NANOS_PER_MICROSECOND + 9; + assertDuration(CqlDurationLiteralParser.parse("7ms8us9ns"), 0, 0, expectedNanos); + } + + @Test + void standardFormat_microsecondMuVariant_parsesNanos() { + assertDuration(CqlDurationLiteralParser.parse("8µs"), 0, 0, 8 * NANOS_PER_MICROSECOND); + } + + @Test + void standardFormat_caseInsensitive_parsesUppercaseUnits() { + assertDuration(CqlDurationLiteralParser.parse("1Y2MO3D"), 14, 3, 0); + } + + @Test + void standardFormat_repeatedUnit_accumulates() { + assertDuration(CqlDurationLiteralParser.parse("1d2d"), 0, 3, 0); + } + + @Test + void standardFormat_weeks_convertToDays() { + assertDuration(CqlDurationLiteralParser.parse("2w3d"), 0, 17, 0); + } + + @Test + void standardFormat_negative_negatesAllComponents() { + assertDuration(CqlDurationLiteralParser.parse("-1mo3d"), -1, -3, 0); + } + + @Test + void standardFormat_negativeWeeks_negatesDays() { + assertDuration(CqlDurationLiteralParser.parse("-2w"), 0, -14, 0); + } + + @Test + void standardFormat_isolatedHour_parsesNanos() { + assertDuration(CqlDurationLiteralParser.parse("5h"), 0, 0, 5 * NANOS_PER_HOUR); + } + + @Test + void standardFormat_isolatedMinute_parsesNanos() { + assertDuration(CqlDurationLiteralParser.parse("5m"), 0, 0, 5 * NANOS_PER_MINUTE); + } + + @Test + void standardFormat_isolatedSecond_parsesNanos() { + assertDuration(CqlDurationLiteralParser.parse("5s"), 0, 0, 5 * NANOS_PER_SECOND); + } + + @Test + void standardFormat_unrecognizedCharacters_areSilentlyIgnored() { + // Documents current lenient behavior: the standard-format branch has no "whole string + // consumed" check, so unrecognized characters (here, "5x") are silently skipped rather + // than rejected. + assertDuration(CqlDurationLiteralParser.parse("5x3d"), 0, 3, 0); + } + + // ------------------------------------------------------------------------- + // ISO 8601 format + // ------------------------------------------------------------------------- + + @Test + void isoFormat_dateOnly_parsesMonthsAndDays() { + assertDuration(CqlDurationLiteralParser.parse("P1Y2M3D"), 14, 3, 0); + } + + @Test + void isoFormat_timeOnly_parsesNanos() { + long expectedNanos = 4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND; + assertDuration(CqlDurationLiteralParser.parse("PT4H5M6S"), 0, 0, expectedNanos); + } + + @Test + void isoFormat_dateAndTimeCombined_parsesAllComponents() { + long expectedNanos = 4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND; + assertDuration(CqlDurationLiteralParser.parse("P1Y2M3DT4H5M6S"), 14, 3, expectedNanos); + } + + @Test + void isoFormat_lowercasePrefix_isAccepted() { + assertDuration(CqlDurationLiteralParser.parse("p1y2m3d"), 14, 3, 0); + } + + @Test + void isoFormat_negativeDateOnly_negatesMonthsAndDays() { + assertDuration(CqlDurationLiteralParser.parse("-P1Y2M3D"), -14, -3, 0); + } + + @Test + void isoFormat_negativeTimeOnly_negatesNanos() { + assertDuration(CqlDurationLiteralParser.parse("-PT5H"), 0, 0, -5 * NANOS_PER_HOUR); + } + + @Test + void isoFormat_negativeDateAndTimeCombined_negatesAllComponents() { + long expectedNanos = 4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND; + assertDuration(CqlDurationLiteralParser.parse("-P1Y2M3DT4H5M6S"), -14, -3, -expectedNanos); + } + + @Test + void isoFormat_barePrefix_parsesAsZeroDuration() { + // Mirrors the driver's own grammar, where every component is optional: a bare "P" is a + // valid, if degenerate, zero-length duration rather than an error. + assertDuration(CqlDurationLiteralParser.parse("P"), 0, 0, 0); + } + + @Test + void isoFormat_bareTimeDesignatorWithNoValue_parsesAsZeroDuration() { + assertDuration(CqlDurationLiteralParser.parse("PT"), 0, 0, 0); + } + + @Test + void isoFormat_dateOnlyWithBareTrailingTimeDesignator_parsesDateComponents() { + assertDuration(CqlDurationLiteralParser.parse("P1Y2M3DT"), 14, 3, 0); + } + + @Test + void isoFormat_malformedContent_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> CqlDurationLiteralParser.parse("P1X")); + } + + // ------------------------------------------------------------------------- + // ISO 8601, weeks-only format + // ------------------------------------------------------------------------- + + @Test + void isoWeekFormat_parsesDaysFromWeeks() { + assertDuration(CqlDurationLiteralParser.parse("P3W"), 0, 21, 0); + } + + @Test + void isoWeekFormat_negative_negatesDays() { + assertDuration(CqlDurationLiteralParser.parse("-P3W"), 0, -21, 0); + } + + @Test + void isoWeekFormat_lowercasePrefix_isAccepted() { + assertDuration(CqlDurationLiteralParser.parse("p3w"), 0, 21, 0); + } + + @Test + void isoWeekFormat_mixedWithOtherUnits_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> CqlDurationLiteralParser.parse("P1Y2W")); + } + + // ------------------------------------------------------------------------- + // ISO 8601, alternative format + // ------------------------------------------------------------------------- + + @Test + void isoAlternativeFormat_parsesAllComponents() { + long expectedNanos = 4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND; + assertDuration(CqlDurationLiteralParser.parse("P0001-02-03T04:05:06"), 14, 3, expectedNanos); + } + + @Test + void isoAlternativeFormat_negative_negatesAllComponents() { + long expectedNanos = 4 * NANOS_PER_HOUR + 5 * NANOS_PER_MINUTE + 6 * NANOS_PER_SECOND; + assertDuration(CqlDurationLiteralParser.parse("-P0001-02-03T04:05:06"), -14, -3, -expectedNanos); + } + + @Test + void isoAlternativeFormat_lowercasePrefix_isAccepted() { + assertDuration(CqlDurationLiteralParser.parse("p0000-01-00T00:00:00"), 1, 0, 0); + } + + @Test + void isoAlternativeFormat_missingTimePart_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> CqlDurationLiteralParser.parse("P2024-01")); + } + + // ------------------------------------------------------------------------- + // Errors + // ------------------------------------------------------------------------- + + @Test + void parse_null_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> CqlDurationLiteralParser.parse(null)); + } + + @Test + void parse_empty_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> CqlDurationLiteralParser.parse("")); + } + + @Test + void parse_blank_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> CqlDurationLiteralParser.parse(" ")); + } + + @Test + void parse_bareMinusSign_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> CqlDurationLiteralParser.parse("-")); + } +} \ No newline at end of file From bfbe32b2471dbf4bd4e6123c1f7531d7d116b46e Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 14 Jul 2026 19:34:41 -0300 Subject: [PATCH 14/14] More refactorings in evaluator --- .../CassandraOperationEvaluator.java | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) 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 index c6570a170a..6dfcbad855 100644 --- 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 @@ -121,9 +121,9 @@ private Truthness evaluateContains(ContainsOperation op, CassandraRow candida Object rawCol = candidateRow.getValue(op.getColumnName()); if (rawCol == null) { return FALSE_TRUTHNESS; + } else { + return any(op.getValue(), toElementList(rawCol)); } - - return any(op.getValue(), toElementList(rawCol)); } private Truthness evaluateContainsKey(ContainsKeyOperation op, CassandraRow candidateRow) { @@ -132,10 +132,10 @@ private Truthness evaluateContainsKey(ContainsKeyOperation op, CassandraRow c return FALSE_TRUTHNESS; } else if (!(rawCol instanceof Map)) { return FALSE_TRUTHNESS; + } else { + List keys = new ArrayList<>(((Map) rawCol).keySet()); + return any(op.getValue(), keys); } - - List keys = new ArrayList<>(((Map) rawCol).keySet()); - return any(op.getValue(), keys); } private Truthness evaluateComparison(ComparisonOperation op, CassandraRow candidateRow, ComparisonType type) { @@ -146,14 +146,14 @@ private Truthness evaluateComparison(ComparisonOperation op, CassandraRow can return FALSE_TRUTHNESS; } else if (rowValue == null || queryValue == null) { return FALSE_TRUTHNESS_BETTER; + } else { + Truthness typeResult = compareByType(rowValue, queryValue, type); + if (typeResult.isTrue()) { + return typeResult; + } else { + return TruthnessUtils.buildScaledTruthness(C_BETTER, typeResult.getOfTrue()); + } } - - 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) { @@ -184,33 +184,33 @@ private Truthness compareNumeric(Object rowValue, Object literalValue, Compariso 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; + } else { + String a = (rowValue instanceof InetAddress) + ? ((InetAddress) rowValue).getHostAddress() + : (String) rowValue; + String b = (String) literalValue; - return TruthnessUtils.getStringEqualityTruthness(a, b); + 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; + } else { + double x = ((Boolean) rowValue) ? 1.0 : 0.0; + double y = ((Boolean) literalValue) ? 1.0 : 0.0; - return TruthnessUtils.getEqualityTruthness(x, y); + return TruthnessUtils.getEqualityTruthness(x, y); + } } private Truthness compareUuid(Object rowValue, Object literalValue, ComparisonType comparisonType) { if (comparisonType != ComparisonType.EQUALS) { throw new IllegalArgumentException("Unsupported operator for UUID literals: " + comparisonType); + } else { + return TruthnessUtils.getEqualityTruthness((UUID) rowValue, (UUID) literalValue); } - - return TruthnessUtils.getEqualityTruthness((UUID) rowValue, (UUID) literalValue); } private static boolean isTemporalType(Object rowValue) { @@ -372,13 +372,13 @@ private Truthness compareDuration(Object rowValue, Object literalValue, Comparis 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); + } else { + Truthness[] truthnesses = candidates.stream() + .map(candidate -> evaluateEquals(value, candidate)) + .toArray(Truthness[]::new); - return TruthnessUtils.buildOrAggregationTruthness(truthnesses); + return TruthnessUtils.buildOrAggregationTruthness(truthnesses); + } } private Truthness evaluateEquals(Object a, Object b) { @@ -386,15 +386,14 @@ private Truthness evaluateEquals(Object a, Object b) { return FALSE_TRUTHNESS; } else if (a == null || b == null) { return FALSE_TRUTHNESS_BETTER; + } else { + Truthness raw = compareByType(a, b, ComparisonType.EQUALS); + if (raw.isTrue()) { + return raw; + } else { + return TruthnessUtils.buildScaledTruthness(C_BETTER, raw.getOfTrue()); + } } - - 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) {