From ef911a9153cd485e0f4931021df0ea05472756e8 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 29 Sep 2026 02:22:47 -0300 Subject: [PATCH 1/4] Replace bind markers with actual values in captured queries --- .../cassandra/CqlBindMarkerInterpolator.java | 448 ++++++++++++++++++ .../CqlSessionClassReplacement.java | 13 +- .../CqlBindMarkerInterpolatorTest.java | 187 ++++++++ .../CqlSessionClassReplacementTest.java | 49 +- 4 files changed, 688 insertions(+), 9 deletions(-) create mode 100644 client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolator.java create mode 100644 client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolatorTest.java diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolator.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolator.java new file mode 100644 index 0000000000..0f2dc03106 --- /dev/null +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolator.java @@ -0,0 +1,448 @@ +package org.evomaster.client.java.instrumentation.cassandra; + +import org.evomaster.client.java.utils.SimpleLogger; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Replaces the bind markers of an executed CQL statement with the values it was executed with, so + * that the heuristics compute a distance against real literals instead of a placeholder. + *

+ * Without this, a SUT that binds its queries (which is what Spring Data and any use of + * {@code PreparedStatement} do) only ever reports {@code WHERE col = ?}. The WHERE clause then + * carries no value to measure a row against, so every candidate scores the same and the search has + * no gradient to follow. The same problem was solved the same way for SQL, in + * {@code PreparedStatementClassReplacement.interpolateSqlStringWithJSqlParser}. + *

+ * None of the entry points here ever throws, and none ever returns null. This runs inside the + * SUT's own process, on the path of every intercepted query, so a statement that cannot be + * interpolated for any reason (unexpected driver shape, a value with no codec, a marker/value count + * mismatch) falls back to the parameterised text. That is exactly what was recorded before this + * class existed, so the worst case is no improvement rather than a broken SUT. It is a deliberate + * departure from the project's "public preconditions throw" rule, for that reason; the + * {@code requireNonNull} checks that remain guard against bugs in our own callers, not against SUT + * data. + *

+ * The driver is on the SUT's classpath, not this module's, so everything is reached by reflection, + * as in {@link CassandraSchemaTracer}. + */ +public class CqlBindMarkerInterpolator { + + /* + * Driver types reached by reflection. The interfaces are used, rather than the concrete classes + * of the objects at hand, so that the looked-up methods are always publicly accessible. + */ + private static final String CODEC_REGISTRY_CLASS = "com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry"; + private static final String TYPE_CODEC_CLASS = "com.datastax.oss.driver.api.core.type.codec.TypeCodec"; + + /* + * Names of the driver methods invoked via reflection throughout this class. + */ + private static final String METHOD_GET_CONTEXT = "getContext"; + private static final String METHOD_GET_CODEC_REGISTRY = "getCodecRegistry"; + private static final String METHOD_CODEC_FOR = "codecFor"; + private static final String METHOD_FORMAT = "format"; + private static final String METHOD_GET_PREPARED_STATEMENT = "getPreparedStatement"; + private static final String METHOD_SIZE = "size"; + private static final String METHOD_GET_OBJECT = "getObject"; + private static final String METHOD_GET_POSITIONAL_VALUES = "getPositionalValues"; + private static final String METHOD_GET_NAMED_VALUES = "getNamedValues"; + private static final String METHOD_AS_INTERNAL = "asInternal"; + + /** How a null bound value is written in CQL; no codec is consulted for it. */ + private static final String NULL_LITERAL = "NULL"; + + private static final String DOLLAR_QUOTE = "$$"; + private static final String BLOCK_COMMENT_END = "*/"; + + private CqlBindMarkerInterpolator() { + } + + /** + * Resolves the literal that a bind marker is replaced with. + */ + private interface MarkerResolver { + /** + * @param name the marker's name, for a named marker ({@code :name}), or + * {@code null} for a positional one ({@code ?}) + * @param positionalIndex the 0-based position of a positional marker among the positional + * markers of the statement, or -1 for a named one + * @return the CQL literal to write in the marker's place, or {@code null} if it cannot be + * resolved, which aborts the whole interpolation + */ + String literalFor(String name, int positionalIndex); + } + + /** + * Interpolates a {@code Statement} handed to {@code CqlSession.execute(Statement)}: a + * {@code BoundStatement}, or a {@code SimpleStatement} carrying positional or named values. + * + * @param cqlSession a live {@code com.datastax.oss.driver.api.core.CqlSession} + * @param statement the statement being executed + * @param fallbackCql the statement's parameterised text, as already extracted by the caller, + * returned unchanged when there is nothing to interpolate or when + * interpolating fails + * @return the CQL with its bind markers replaced by literals, or {@code fallbackCql} + */ + public static String forStatement(Object cqlSession, Object statement, String fallbackCql) { + Objects.requireNonNull(cqlSession); + Objects.requireNonNull(statement); + Objects.requireNonNull(fallbackCql); + + try { + Object codecRegistry = codecRegistryOf(cqlSession); + + List positional = boundLiterals(statement, codecRegistry); + if (positional == null) { + positional = simpleStatementPositionalLiterals(statement, codecRegistry); + } + if (positional != null) { + return interpolatePositional(fallbackCql, positional, fallbackCql); + } + + Map named = simpleStatementNamedLiterals(statement, codecRegistry); + if (named != null) { + return interpolateNamed(fallbackCql, named, fallbackCql); + } + + // no values bound: the text already carries its literals + return fallbackCql; + } catch (Exception e) { + warnFallback(fallbackCql, e); + return fallbackCql; + } + } + + /** + * Interpolates the positional values of {@code CqlSession.execute(String, Object...)}. + * + * @param cqlSession a live {@code com.datastax.oss.driver.api.core.CqlSession} + * @param cql the parameterised CQL, with one {@code ?} per value + * @param values the values bound to it, in order + * @return the CQL with its markers replaced by literals, or {@code cql} unchanged on any problem + */ + public static String forPositionalValues(Object cqlSession, String cql, Object[] values) { + Objects.requireNonNull(cqlSession); + Objects.requireNonNull(cql); + + if (values == null || values.length == 0) { + return cql; + } + + try { + Object codecRegistry = codecRegistryOf(cqlSession); + List literals = new ArrayList<>(values.length); + for (Object value : values) { + literals.add(formatValue(codecRegistry, value)); + } + return interpolatePositional(cql, literals, cql); + } catch (Exception e) { + warnFallback(cql, e); + return cql; + } + } + + /** + * Interpolates the named values of {@code CqlSession.execute(String, Map)}. + * + * @param cqlSession a live {@code com.datastax.oss.driver.api.core.CqlSession} + * @param cql the parameterised CQL, with a {@code :name} marker per entry + * @param values the values bound to it, by marker name + * @return the CQL with its markers replaced by literals, or {@code cql} unchanged on any problem + */ + public static String forNamedValues(Object cqlSession, String cql, Map values) { + Objects.requireNonNull(cqlSession); + Objects.requireNonNull(cql); + + if (values == null || values.isEmpty()) { + return cql; + } + + try { + Object codecRegistry = codecRegistryOf(cqlSession); + Map literals = new HashMap<>(); + for (Map.Entry e : values.entrySet()) { + literals.put(e.getKey(), formatValue(codecRegistry, e.getValue())); + } + return interpolateNamed(cql, literals, cql); + } catch (Exception e) { + warnFallback(cql, e); + return cql; + } + } + + /** + * @return the bound values of a {@code BoundStatement}, already rendered as CQL literals, or + * {@code null} if {@code statement} is not a {@code BoundStatement} + */ + private static List boundLiterals(Object statement, Object codecRegistry) throws ReflectiveOperationException { + if (!hasMethod(statement, METHOD_GET_PREPARED_STATEMENT)) { + return null; + } + + int size = (int) invoke(statement, METHOD_SIZE); + Method getObject = statement.getClass().getMethod(METHOD_GET_OBJECT, int.class); + + List literals = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + literals.add(formatValue(codecRegistry, getObject.invoke(statement, i))); + } + return literals; + } + + /** + * @return the positional values of a {@code SimpleStatement}, already rendered as CQL literals, + * or {@code null} if there are none + */ + private static List simpleStatementPositionalLiterals(Object statement, Object codecRegistry) throws ReflectiveOperationException { + if (!hasMethod(statement, METHOD_GET_POSITIONAL_VALUES)) { + return null; + } + + List values = (List) invoke(statement, METHOD_GET_POSITIONAL_VALUES); + if (values == null || values.isEmpty()) { + return null; + } + + List literals = new ArrayList<>(values.size()); + for (Object value : values) { + literals.add(formatValue(codecRegistry, value)); + } + return literals; + } + + /** + * @return the named values of a {@code SimpleStatement}, already rendered as CQL literals and + * keyed by marker name, or {@code null} if there are none + */ + private static Map simpleStatementNamedLiterals(Object statement, Object codecRegistry) throws ReflectiveOperationException { + if (!hasMethod(statement, METHOD_GET_NAMED_VALUES)) { + return null; + } + + Map values = (Map) invoke(statement, METHOD_GET_NAMED_VALUES); + if (values == null || values.isEmpty()) { + return null; + } + + Map literals = new HashMap<>(); + for (Map.Entry e : values.entrySet()) { + // the keys are CqlIdentifier, whose internal form is the marker's name as written + String name = (String) invoke(e.getKey(), METHOD_AS_INTERNAL); + literals.put(name, formatValue(codecRegistry, e.getValue())); + } + return literals; + } + + /** + * Substitutes each {@code ?} of {@code cql}, in order, with the corresponding entry of + * {@code literals}. Falls back when the two counts differ, since the statement is then not the + * one we think it is and a partial substitution would report a query that was never executed. + */ + private static String interpolatePositional(String cql, List literals, String fallbackCql) { + int[] consumed = new int[1]; + String interpolated = substitute(cql, (name, positionalIndex) -> { + if (name != null || positionalIndex >= literals.size()) { + return null; + } + consumed[0] = positionalIndex + 1; + return literals.get(positionalIndex); + }); + + if (interpolated == null || consumed[0] != literals.size()) { + return fallbackCql; + } + return interpolated; + } + + /** + * Substitutes each {@code :name} of {@code cql} with the matching entry of {@code literals}. + * Entries of {@code literals} that no marker refers to are tolerated; a marker with no entry + * falls back. + */ + private static String interpolateNamed(String cql, Map literals, String fallbackCql) { + String interpolated = substitute(cql, (name, positionalIndex) -> + name != null ? literals.get(name) : null); + + return interpolated != null ? interpolated : fallbackCql; + } + + /** + * Walks {@code cql} once and replaces every bind marker found in code position with the literal + * {@code resolver} gives for it. + *

+ * A {@code ?} or {@code :name} inside a string literal, a quoted identifier, a dollar-quoted + * string or a comment is not a bind marker, so those regions are copied over verbatim. A naive + * {@code replaceFirst("\\?", ...)} gets this wrong, which is why the SQL equivalent that did so + * ({@code PreparedStatementClassReplacement.interpolateSqlString}) is deprecated. + * + * @return the interpolated CQL, or {@code null} if any marker could not be resolved + */ + private static String substitute(String cql, MarkerResolver resolver) { + StringBuilder out = new StringBuilder(cql.length() + 32); + int n = cql.length(); + int i = 0; + int positionalIndex = 0; + + while (i < n) { + char c = cql.charAt(i); + + if (c == '\'' || c == '"') { + i = appendQuoted(cql, i, c, out); + } else if (c == '$' && next(cql, i) == '$') { + i = appendUntil(cql, i, DOLLAR_QUOTE, out); + } else if ((c == '-' && next(cql, i) == '-') || (c == '/' && next(cql, i) == '/')) { + i = appendLineComment(cql, i, out); + } else if (c == '/' && next(cql, i) == '*') { + i = appendUntil(cql, i, BLOCK_COMMENT_END, out); + } else if (c == '?') { + String literal = resolver.literalFor(null, positionalIndex++); + if (literal == null) { + return null; + } + out.append(literal); + i++; + } else if (c == ':' && isMarkerNameStart(next(cql, i))) { + int end = i + 1; + while (end < n && isMarkerNamePart(cql.charAt(end))) { + end++; + } + String literal = resolver.literalFor(cql.substring(i + 1, end), -1); + if (literal == null) { + return null; + } + out.append(literal); + i = end; + } else { + out.append(c); + i++; + } + } + + return out.toString(); + } + + /** + * Copies a quoted region verbatim, from its opening quote through its closing one. A doubled + * quote inside is an escaped quote rather than a terminator, as CQL defines it for both + * {@code '...'} literals and {@code "..."} identifiers. + * + * @return the index just past the closing quote, or the end of the text if it is unterminated + */ + private static int appendQuoted(String cql, int start, char quote, StringBuilder out) { + int n = cql.length(); + out.append(quote); + + int i = start + 1; + while (i < n) { + char c = cql.charAt(i); + if (c == quote) { + if (i + 1 < n && cql.charAt(i + 1) == quote) { + out.append(quote).append(quote); + i += 2; + continue; + } + out.append(quote); + return i + 1; + } + out.append(c); + i++; + } + return i; + } + + /** + * Copies a region verbatim, from {@code start} through the first occurrence of {@code terminator} + * after it, or to the end of the text if it never occurs. + * + * @return the index just past the terminator, or the end of the text + */ + private static int appendUntil(String cql, int start, String terminator, StringBuilder out) { + int end = cql.indexOf(terminator, start + terminator.length()); + int stop = end < 0 ? cql.length() : end + terminator.length(); + out.append(cql, start, stop); + return stop; + } + + /** + * Copies a line comment verbatim, up to but not including the newline that ends it, which the + * caller's loop then handles as ordinary text. + * + * @return the index of that newline, or the end of the text + */ + private static int appendLineComment(String cql, int start, StringBuilder out) { + int end = start; + int n = cql.length(); + while (end < n && cql.charAt(end) != '\n') { + end++; + } + out.append(cql, start, end); + return end; + } + + private static char next(String cql, int i) { + return i + 1 < cql.length() ? cql.charAt(i + 1) : '\0'; + } + + /* + * A named marker is ':' followed by an unquoted CQL identifier. Requiring a letter or underscore + * first keeps the ':' of a map literal such as {'k':1} from looking like a marker. + */ + private static boolean isMarkerNameStart(char c) { + return Character.isLetter(c) || c == '_'; + } + + private static boolean isMarkerNamePart(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + + /** + * Renders a bound value as the CQL literal the driver itself would write for it, by asking the + * session's codec registry for the value's codec. {@code TypeCodec.format} already quotes and + * escapes, so no literal rendering is done here. + */ + private static String formatValue(Object codecRegistry, Object value) throws ReflectiveOperationException { + if (value == null) { + return NULL_LITERAL; + } + + Method codecFor = Class.forName(CODEC_REGISTRY_CLASS).getMethod(METHOD_CODEC_FOR, Object.class); + Object codec = codecFor.invoke(codecRegistry, value); + + Method format = Class.forName(TYPE_CODEC_CLASS).getMethod(METHOD_FORMAT, Object.class); + return (String) format.invoke(codec, value); + } + + private static Object codecRegistryOf(Object cqlSession) throws ReflectiveOperationException { + Object context = invoke(cqlSession, METHOD_GET_CONTEXT); + return invoke(context, METHOD_GET_CODEC_REGISTRY); + } + + private static boolean hasMethod(Object target, String methodName) { + try { + target.getClass().getMethod(methodName); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + private static Object invoke(Object target, String methodName) throws ReflectiveOperationException { + return target.getClass().getMethod(methodName).invoke(target); + } + + /** + * Reports, at most once per distinct message, that a statement was recorded with its markers + * still in place. Worth knowing when reading a run's heuristics, since such a query gets no + * gradient, but not worth one line per execution. + */ + private static void warnFallback(String cql, Exception e) { + SimpleLogger.uniqueWarn("Failed to interpolate the bound values of a CQL statement, so it is" + + " recorded with its bind markers: " + cql + ". Cause: " + e); + } +} \ No newline at end of file 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 4a76e1d39b..2045ff748a 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.cassandra.CassandraSchemaTracer; +import org.evomaster.client.java.instrumentation.cassandra.CqlBindMarkerInterpolator; 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; @@ -64,17 +65,20 @@ public static Object execute(Object cqlSession, String 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); + String tracked = CqlBindMarkerInterpolator.forPositionalValues(cqlSession, query, values); + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_POSITIONAL_VALUES_SYNC, cqlSession, tracked, 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); + String tracked = CqlBindMarkerInterpolator.forNamedValues(cqlSession, query, values); + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_NAMED_VALUES_SYNC, cqlSession, tracked, 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); + String tracked = CqlBindMarkerInterpolator.forStatement(cqlSession, statement, extractQueryText(statement)); + return handleCqlExecute(CASSANDRA_EXECUTE_STATEMENT_SYNC, cqlSession, tracked, statement); } private static Object handleCqlExecute(String id, Object cqlSession, String queryForTracking, Object... invokeArgs) { @@ -176,6 +180,9 @@ private static class TableReference { /** * Statement is a generic driver type; only SimpleStatement exposes the original * CQL text directly, while BoundStatement requires going through its PreparedStatement. + *

+ * Either way the text returned here still carries its bind markers; the values bound to them + * are substituted in afterwards by {@link CqlBindMarkerInterpolator}. */ private static String extractQueryText(Object statement) { try { diff --git a/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolatorTest.java b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolatorTest.java new file mode 100644 index 0000000000..b5904d9897 --- /dev/null +++ b/client-java/instrumentation/src/test/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolatorTest.java @@ -0,0 +1,187 @@ +package org.evomaster.client.java.instrumentation.cassandra; + +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests for {@link CqlBindMarkerInterpolator}, covering the marker scanner and the rendering of + * values as CQL literals. No Cassandra instance is needed: the values are rendered by the driver's + * own {@link CodecRegistry#DEFAULT}, reached through a stand-in for the session, since the + * interpolator only ever asks a session for {@code getContext().getCodecRegistry()}. + */ +public class CqlBindMarkerInterpolatorTest { + + /** + * Stands in for a {@code CqlSession}. The interpolator resolves the codec registry by + * reflection on whatever object it is handed, so it needs no driver type here. + */ + public static class FakeSession { + public FakeContext getContext() { + return new FakeContext(); + } + } + + public static class FakeContext { + public CodecRegistry getCodecRegistry() { + return CodecRegistry.DEFAULT; + } + } + + private static final Object SESSION = new FakeSession(); + + private static String positional(String cql, Object... values) { + return CqlBindMarkerInterpolator.forPositionalValues(SESSION, cql, values); + } + + private static String named(String cql, Map values) { + return CqlBindMarkerInterpolator.forNamedValues(SESSION, cql, values); + } + + // ------------------------------------------------------------------ positional markers + + @Test + public void testSinglePositionalMarker() { + assertEquals("SELECT * FROM t WHERE a = 123", + positional("SELECT * FROM t WHERE a = ?", 123)); + } + + @Test + public void testSeveralPositionalMarkersAreSubstitutedInOrder() { + assertEquals("SELECT * FROM t WHERE a = 1 AND b = 'two' AND c = true", + positional("SELECT * FROM t WHERE a = ? AND b = ? AND c = ?", 1, "two", true)); + } + + @Test + public void testStringValueIsQuotedAndEscapedByTheDriver() { + assertEquals("SELECT * FROM t WHERE a = 'O''Brien'", + positional("SELECT * FROM t WHERE a = ?", "O'Brien")); + } + + @Test + public void testNullValueRendersAsNull() { + assertEquals("SELECT * FROM t WHERE a = NULL", + positional("SELECT * FROM t WHERE a = ?", new Object[]{null})); + } + + @Test + public void testNoValuesLeavesTheQueryUntouched() { + String cql = "SELECT * FROM t WHERE a = 1"; + assertEquals(cql, positional(cql)); + } + + // ------------------------------------------------------------------ named markers + + @Test + public void testNamedMarkersAreSubstitutedByName() { + Map values = new LinkedHashMap<>(); + values.put("first", 1); + values.put("second", "two"); + + assertEquals("SELECT * FROM t WHERE a = 1 AND b = 'two'", + named("SELECT * FROM t WHERE a = :first AND b = :second", values)); + } + + @Test + public void testSameNamedMarkerUsedTwiceIsSubstitutedBothTimes() { + Map values = new HashMap<>(); + values.put("v", 7); + + assertEquals("SELECT * FROM t WHERE a = 7 AND b = 7", + named("SELECT * FROM t WHERE a = :v AND b = :v", values)); + } + + @Test + public void testUnknownNamedMarkerFallsBack() { + Map values = new HashMap<>(); + values.put("other", 1); + + String cql = "SELECT * FROM t WHERE a = :v"; + assertEquals(cql, named(cql, values)); + } + + // ------------------------------------------------------------------ markers that are not markers + + @Test + public void testQuestionMarkInsideStringLiteralIsNotAMarker() { + assertEquals("SELECT * FROM t WHERE a = 'is it? yes' AND b = 1", + positional("SELECT * FROM t WHERE a = 'is it? yes' AND b = ?", 1)); + } + + @Test + public void testEscapedQuoteInsideStringLiteralDoesNotEndIt() { + assertEquals("SELECT * FROM t WHERE a = 'it''s a ? here' AND b = 1", + positional("SELECT * FROM t WHERE a = 'it''s a ? here' AND b = ?", 1)); + } + + @Test + public void testQuestionMarkInsideQuotedIdentifierIsNotAMarker() { + assertEquals("SELECT \"we?ird\" FROM t WHERE a = 1", + positional("SELECT \"we?ird\" FROM t WHERE a = ?", 1)); + } + + @Test + public void testQuestionMarkInsideDollarQuotedStringIsNotAMarker() { + assertEquals("SELECT * FROM t WHERE a = $$what? no$$ AND b = 1", + positional("SELECT * FROM t WHERE a = $$what? no$$ AND b = ?", 1)); + } + + @Test + public void testQuestionMarkInsideDashLineCommentIsNotAMarker() { + assertEquals("SELECT * FROM t -- is this ?\nWHERE a = 1", + positional("SELECT * FROM t -- is this ?\nWHERE a = ?", 1)); + } + + @Test + public void testQuestionMarkInsideSlashLineCommentIsNotAMarker() { + assertEquals("SELECT * FROM t // is this ?\nWHERE a = 1", + positional("SELECT * FROM t // is this ?\nWHERE a = ?", 1)); + } + + @Test + public void testQuestionMarkInsideBlockCommentIsNotAMarker() { + assertEquals("SELECT /* a ? here */ * FROM t WHERE a = 1", + positional("SELECT /* a ? here */ * FROM t WHERE a = ?", 1)); + } + + @Test + public void testNamedMarkerInsideStringLiteralIsNotAMarker() { + Map values = new HashMap<>(); + values.put("b", 2); + + assertEquals("SELECT * FROM t WHERE a = 'x:name y' AND b = 2", + named("SELECT * FROM t WHERE a = 'x:name y' AND b = :b", values)); + } + + @Test + public void testColonFollowedByNonIdentifierIsNotAMarker() { + // the ':' of a map literal is not a bind marker, so the statement interpolates normally + assertEquals("UPDATE t SET m = {'k':1} WHERE a = 5", + positional("UPDATE t SET m = {'k':1} WHERE a = ?", 5)); + } + + // ------------------------------------------------------------------ arity mismatches fall back + + @Test + public void testMoreMarkersThanValuesFallsBack() { + String cql = "SELECT * FROM t WHERE a = ? AND b = ?"; + assertEquals(cql, positional(cql, 1)); + } + + @Test + public void testMoreValuesThanMarkersFallsBack() { + String cql = "SELECT * FROM t WHERE a = ?"; + assertEquals(cql, positional(cql, 1, 2)); + } + + @Test + public void testPositionalValuesAgainstNamedMarkersFallBack() { + String cql = "SELECT * FROM t WHERE a = :v"; + assertEquals(cql, positional(cql, 1)); + } +} \ No newline at end of file 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 8077722f3b..5e7d96bc63 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 @@ -148,11 +148,17 @@ void testExecutingInitCassandraFlagSuppressesTracking() { assertTrue(additionalInfoList.get(0).getCqlInfoData().isEmpty()); } + /** + * The bound values must reach the tracked command, not the {@code ?} placeholders: the CQL + * heuristics have no value to measure a row against otherwise. See + * {@link org.evomaster.client.java.instrumentation.cassandra.CqlBindMarkerInterpolator}. + */ @Test void testExecuteWithPositionalValuesIsTracked() { String query = "INSERT INTO " + TABLE + " (id, name, age) VALUES (?, ?, ?)"; + UUID id = UUID.randomUUID(); - CqlSessionClassReplacement.execute(cqlSession, query, UUID.randomUUID(), "Dave", 40); + CqlSessionClassReplacement.execute(cqlSession, query, id, "Dave", 40); List additionalInfoList = ExecutionTracer.exposeAdditionalInfoList(); assertEquals(1, additionalInfoList.size()); @@ -161,7 +167,8 @@ void testExecuteWithPositionalValuesIsTracked() { assertEquals(1, commands.size()); ExecutedCqlCommand cmd = commands.iterator().next(); - assertEquals(query, cmd.getCqlCommand()); + assertEquals("INSERT INTO " + TABLE + " (id, name, age) VALUES (" + id + ", 'Dave', 40)", + cmd.getCqlCommand()); assertEquals(KEYSPACE, cmd.getKeyspaceName()); assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); @@ -171,8 +178,9 @@ void testExecuteWithPositionalValuesIsTracked() { @Test void testExecuteWithNamedValuesIsTracked() { String query = "INSERT INTO " + TABLE + " (id, name, age) VALUES (:id, :name, :age)"; + UUID id = UUID.randomUUID(); Map values = new HashMap<>(); - values.put("id", UUID.randomUUID()); + values.put("id", id); values.put("name", "Erin"); values.put("age", 22); @@ -185,7 +193,8 @@ void testExecuteWithNamedValuesIsTracked() { assertEquals(1, commands.size()); ExecutedCqlCommand cmd = commands.iterator().next(); - assertEquals(query, cmd.getCqlCommand()); + assertEquals("INSERT INTO " + TABLE + " (id, name, age) VALUES (" + id + ", 'Erin', 22)", + cmd.getCqlCommand()); assertEquals(KEYSPACE, cmd.getKeyspaceName()); assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); @@ -213,12 +222,39 @@ void testExecuteWithSimpleStatementIsTracked() { assertTrue(cmd.getExecutionTime() >= 0); } + /** + * A SimpleStatement can carry positional values of its own, without a PreparedStatement being + * involved, and those must be interpolated too. + */ + @Test + void testExecuteWithSimpleStatementCarryingValuesIsTracked() { + String query = "INSERT INTO " + TABLE + " (id, name, age) VALUES (?, ?, ?)"; + UUID id = UUID.randomUUID(); + SimpleStatement statement = SimpleStatement.newInstance(query, id, "Gwen", 51); + + 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("INSERT INTO " + TABLE + " (id, name, age) VALUES (" + id + ", 'Gwen', 51)", + cmd.getCqlCommand()); + assertEquals(KEYSPACE, cmd.getKeyspaceName()); + assertEquals(TABLE_NAME, cmd.getTableName()); + assertFalse(cmd.hasThrownCqlException()); + } + @Test void testExecuteWithBoundStatementIsTracked() { String query = "INSERT INTO " + TABLE + " (id, name, age) VALUES (?, ?, ?)"; + UUID id = UUID.randomUUID(); // 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); + BoundStatement bound = prepared.bind(id, "Frank", 33); CqlSessionClassReplacement.execute(cqlSession, bound); @@ -229,7 +265,8 @@ void testExecuteWithBoundStatementIsTracked() { assertEquals(1, commands.size()); ExecutedCqlCommand cmd = commands.iterator().next(); - assertEquals(query, cmd.getCqlCommand()); + assertEquals("INSERT INTO " + TABLE + " (id, name, age) VALUES (" + id + ", 'Frank', 33)", + cmd.getCqlCommand()); assertEquals(KEYSPACE, cmd.getKeyspaceName()); assertEquals(TABLE_NAME, cmd.getTableName()); assertFalse(cmd.hasThrownCqlException()); From fe9980d40c9ecfbd1f0ed99966a4458a2fabb4a3 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 29 Sep 2026 03:19:19 -0300 Subject: [PATCH 2/4] Modify min length for strings in partition key --- .../insertions/CassandraScriptRunnerTest.java | 69 +++++++++++++++++++ .../cassandra/CassandraColumnGeneBuilder.kt | 37 ++++++++-- .../CassandraColumnGeneBuilderTest.kt | 55 +++++++++++++++ 3 files changed, 157 insertions(+), 4 deletions(-) diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/insertions/CassandraScriptRunnerTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/insertions/CassandraScriptRunnerTest.java index 715d88b69f..d32e291f45 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/insertions/CassandraScriptRunnerTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/cassandra/insertions/CassandraScriptRunnerTest.java @@ -34,6 +34,13 @@ public class CassandraScriptRunnerTest { private static final String KEYSPACE = "testks"; private static final String TABLE = "users"; + /** + * A table whose partition key and clustering column are both of a text type, used to check which + * of the two rejects an empty value. Cassandra's rule is what decides the columns + * CassandraColumnGeneBuilder in the core module has to keep non-empty. + */ + private static final String KEYED_TABLE = "keyed"; + @BeforeAll public static void initClass() { cassandra.start(); @@ -48,6 +55,8 @@ public static void initClass() { connection.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE + "." + TABLE + " (id int PRIMARY KEY, name text, elapsed duration, ip inet," + " tags set, scores list, favs map)"); + connection.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE + "." + KEYED_TABLE + + " (pk text, ck text, val text, PRIMARY KEY ((pk), ck))"); } @AfterAll @@ -60,6 +69,7 @@ public static void cleanup() { @BeforeEach public void clearTable() { connection.execute("TRUNCATE " + KEYSPACE + "." + TABLE); + connection.execute("TRUNCATE " + KEYSPACE + "." + KEYED_TABLE); } @Test @@ -163,4 +173,63 @@ public void testInsertionFailureDoesNotStopFollowingInsertions() { //the third insertion must have been attempted, in spite of the second one having failed assertEquals(2, connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).all().size()); } + + /** + * Cassandra rejects an empty value for a column composing the partition key. This is why + * CassandraColumnGeneBuilder in the core module has to keep the gene of such a column non-empty: + * were it left free to generate the empty string, as any other text column is, the insertion the + * search asked for would never land. + */ + @Test + public void testEmptyPartitionKeyIsRejected() { + List insertions = CassandraDsl.cassandra() + .insertInto(KEYSPACE, KEYED_TABLE) + .d("pk", "''") + .d("ck", "'aCk'") + .d("val", "'aValue'") + .dtos(); + + CassandraInsertionResultsDto resultsDto = CassandraScriptRunner.executeInsert(connection, insertions); + + assertFalse(resultsDto.executionResults.get(0)); + assertFalse(connection.execute("SELECT * FROM " + KEYSPACE + "." + KEYED_TABLE).iterator().hasNext()); + } + + /** + * A clustering column, unlike a partition key, does accept an empty value. This is what scopes the + * constraint in CassandraColumnGeneBuilder to the partition key alone. + */ + @Test + public void testEmptyClusteringColumnIsAccepted() { + List insertions = CassandraDsl.cassandra() + .insertInto(KEYSPACE, KEYED_TABLE) + .d("pk", "'aPk'") + .d("ck", "''") + .d("val", "'aValue'") + .dtos(); + + CassandraInsertionResultsDto resultsDto = CassandraScriptRunner.executeInsert(connection, insertions); + + assertTrue(resultsDto.executionResults.get(0)); + assertEquals(1, connection.execute("SELECT * FROM " + KEYSPACE + "." + KEYED_TABLE).all().size()); + } + + /** + * A single character is enough for a partition key, which is why the minimum length imposed on the + * gene of such a column is 1 rather than some larger number. + */ + @Test + public void testOneCharacterPartitionKeyIsAccepted() { + List insertions = CassandraDsl.cassandra() + .insertInto(KEYSPACE, KEYED_TABLE) + .d("pk", "'a'") + .d("ck", "'aCk'") + .d("val", "'aValue'") + .dtos(); + + CassandraInsertionResultsDto resultsDto = CassandraScriptRunner.executeInsert(connection, insertions); + + assertTrue(resultsDto.executionResults.get(0)); + assertEquals(1, connection.execute("SELECT * FROM " + KEYSPACE + "." + KEYED_TABLE).all().size()); + } } diff --git a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt index e93b20b516..d1a7483d67 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilder.kt @@ -54,15 +54,28 @@ object CassandraColumnGeneBuilder { */ private const val ELEMENT_GENE_NAME = "element" + /** + * The shortest text length Cassandra accepts in a partition key: an empty one is rejected with + * "Key may not be empty", so a text column composing the partition key cannot be left free to + * generate the empty string that any other text column may. + */ + private const val MIN_LENGTH_OF_PARTITION_KEY_TEXT = 1 + + /** + * The single place a gene for a CQL text type is built, so that the constraint applied to a + * partition key in [constrainToNonEmpty] cannot drift from the unconstrained case. + */ + private fun textGene(name: String, minLength: Int = 0) = StringGene(name, minLength = minLength) + /** * How the gene generating the value of a column is built, for each of the CQL types handled * here, keyed by the normalized name of the type. Being the single place where such types are * enumerated, it is also what [isSupported] answers from, so that the two cannot disagree. */ private val GENE_BUILDERS: Map Gene> = mapOf( - ASCII_TYPE to { name -> StringGene(name) }, - TEXT_TYPE to { name -> StringGene(name) }, - VARCHAR_TYPE to { name -> StringGene(name) }, + ASCII_TYPE to { name -> textGene(name) }, + TEXT_TYPE to { name -> textGene(name) }, + VARCHAR_TYPE to { name -> textGene(name) }, TINYINT_TYPE to { name -> IntegerGene(name, min = Byte.MIN_VALUE.toInt(), max = Byte.MAX_VALUE.toInt()) }, SMALLINT_TYPE to { name -> IntegerGene(name, min = Short.MIN_VALUE.toInt(), max = Short.MAX_VALUE.toInt()) }, INT_TYPE to { name -> IntegerGene(name) }, @@ -98,7 +111,23 @@ object CassandraColumnGeneBuilder { * @throws IllegalArgumentException if the CQL type of [column] is not handled, as verifiable * beforehand with [isSupported] */ - fun buildGene(column: CassandraColumn): Gene = buildGene(column.name, normalize(column.cqlType)) + fun buildGene(column: CassandraColumn): Gene { + + val gene = buildGene(column.name, normalize(column.cqlType)) + + return if (column.isPartitionKey) constrainToNonEmpty(gene) else gene + } + + /** + * Constrains a gene so that it cannot generate an empty value, for a column composing the + * partition key. Only the text types can produce one among the types handled here, so a gene of + * any other type is returned untouched. + */ + private fun constrainToNonEmpty(gene: Gene): Gene = + when (gene) { + is StringGene -> textGene(gene.name, MIN_LENGTH_OF_PARTITION_KEY_TEXT) + else -> gene + } private fun isSupported(cqlType: String): Boolean { diff --git a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt index 5cf8cb31e7..7d4a0e114a 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/cassandra/CassandraColumnGeneBuilderTest.kt @@ -28,6 +28,12 @@ class CassandraColumnGeneBuilderTest { private fun buildFor(cqlType: String): Gene = CassandraColumnGeneBuilder.buildGene(CassandraColumn("aColumn", cqlType)) + private fun buildForPartitionKey(cqlType: String): Gene = + CassandraColumnGeneBuilder.buildGene(CassandraColumn("aColumn", cqlType, isPartitionKey = true)) + + private fun buildForClusteringColumn(cqlType: String): Gene = + CassandraColumnGeneBuilder.buildGene(CassandraColumn("aColumn", cqlType, isClusteringColumn = true)) + @Test fun testTextTypes() { listOf("ascii", "text", "varchar").forEach { @@ -180,4 +186,53 @@ class CassandraColumnGeneBuilderTest { assertTrue(CassandraColumnGeneBuilder.isSupported(CassandraColumn("aColumn", it)), "$it should be supported") } } + + /** + * Cassandra rejects an insertion giving an empty value for a column composing the partition key, + * as characterized by CassandraScriptRunnerTest in the client module, so the gene of such a + * column is not allowed to generate one. + */ + @Test + fun testTextPartitionKeyIsNotGeneratedEmpty() { + listOf("ascii", "text", "varchar").forEach { + val gene = buildForPartitionKey(it) as StringGene + assertEquals(1, gene.minLength, "unexpected minimum length for $it") + } + } + + /** + * The constraint belongs to the partition key alone: a clustering column does accept an empty + * value, and so does any other text column. + */ + @Test + fun testTextIsGeneratedEmptyOutsideThePartitionKey() { + listOf("ascii", "text", "varchar").forEach { + assertEquals(0, (buildFor(it) as StringGene).minLength, "unexpected minimum length for $it") + assertEquals(0, (buildForClusteringColumn(it) as StringGene).minLength, "unexpected minimum length for $it") + } + } + + /** + * No type other than the text ones can produce an empty value among the ones handled here, so + * being in the partition key does not change the gene built for them. + */ + @Test + fun testGeneOfANonTextPartitionKeyIsUnchanged() { + assertTrue(buildForPartitionKey("int") is IntegerGene) + assertTrue(buildForPartitionKey("uuid") is UUIDGene) + assertTrue(buildForPartitionKey("timestamp") is DateTimeGene) + assertTrue(buildForPartitionKey("inet") is InetGene) + assertTrue(buildForPartitionKey("duration") is CqlDurationGene) + } + + /** + * An element of a collection is not itself a partition key, so it keeps generating empty values + * even when the column holding the collection composes one. + */ + @Test + fun testElementsOfACollectionInThePartitionKeyAreUnconstrained() { + val gene = buildForPartitionKey("frozen>") as ArrayGene<*> + + assertEquals(0, (gene.template as StringGene).minLength) + } } From ec80736bf7f4877c85de56133c92c4bb85a1e469 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 29 Sep 2026 15:43:02 -0300 Subject: [PATCH 3/4] Rename params and variables --- .../cassandra/CqlBindMarkerInterpolator.java | 88 +++++++++---------- .../CqlSessionClassReplacement.java | 16 ++-- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolator.java b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolator.java index 0f2dc03106..8102a1d141 100644 --- a/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolator.java +++ b/client-java/instrumentation/src/main/java/org/evomaster/client/java/instrumentation/cassandra/CqlBindMarkerInterpolator.java @@ -122,28 +122,28 @@ public static String forStatement(Object cqlSession, Object statement, String fa * Interpolates the positional values of {@code CqlSession.execute(String, Object...)}. * * @param cqlSession a live {@code com.datastax.oss.driver.api.core.CqlSession} - * @param cql the parameterised CQL, with one {@code ?} per value - * @param values the values bound to it, in order - * @return the CQL with its markers replaced by literals, or {@code cql} unchanged on any problem + * @param parameterisedQuery the parameterised CQL, with one {@code ?} per value + * @param parameterValues the values bound to it, in order + * @return the CQL with its markers replaced by literals, or {@code parameterisedQuery} unchanged on any problem */ - public static String forPositionalValues(Object cqlSession, String cql, Object[] values) { + public static String forPositionalValues(Object cqlSession, String parameterisedQuery, Object[] parameterValues) { Objects.requireNonNull(cqlSession); - Objects.requireNonNull(cql); + Objects.requireNonNull(parameterisedQuery); - if (values == null || values.length == 0) { - return cql; + if (parameterValues == null || parameterValues.length == 0) { + return parameterisedQuery; } try { Object codecRegistry = codecRegistryOf(cqlSession); - List literals = new ArrayList<>(values.length); - for (Object value : values) { + List literals = new ArrayList<>(parameterValues.length); + for (Object value : parameterValues) { literals.add(formatValue(codecRegistry, value)); } - return interpolatePositional(cql, literals, cql); + return interpolatePositional(parameterisedQuery, literals, parameterisedQuery); } catch (Exception e) { - warnFallback(cql, e); - return cql; + warnFallback(parameterisedQuery, e); + return parameterisedQuery; } } @@ -151,28 +151,28 @@ public static String forPositionalValues(Object cqlSession, String cql, Object[] * Interpolates the named values of {@code CqlSession.execute(String, Map)}. * * @param cqlSession a live {@code com.datastax.oss.driver.api.core.CqlSession} - * @param cql the parameterised CQL, with a {@code :name} marker per entry - * @param values the values bound to it, by marker name - * @return the CQL with its markers replaced by literals, or {@code cql} unchanged on any problem + * @param parameterisedQuery the parameterised CQL, with a {@code :name} marker per entry + * @param parameterValues the values bound to it, by marker name + * @return the CQL with its markers replaced by literals, or {@code parameterisedQuery} unchanged on any problem */ - public static String forNamedValues(Object cqlSession, String cql, Map values) { + public static String forNamedValues(Object cqlSession, String parameterisedQuery, Map parameterValues) { Objects.requireNonNull(cqlSession); - Objects.requireNonNull(cql); + Objects.requireNonNull(parameterisedQuery); - if (values == null || values.isEmpty()) { - return cql; + if (parameterValues == null || parameterValues.isEmpty()) { + return parameterisedQuery; } try { Object codecRegistry = codecRegistryOf(cqlSession); Map literals = new HashMap<>(); - for (Map.Entry e : values.entrySet()) { + for (Map.Entry e : parameterValues.entrySet()) { literals.put(e.getKey(), formatValue(codecRegistry, e.getValue())); } - return interpolateNamed(cql, literals, cql); + return interpolateNamed(parameterisedQuery, literals, parameterisedQuery); } catch (Exception e) { - warnFallback(cql, e); - return cql; + warnFallback(parameterisedQuery, e); + return parameterisedQuery; } } @@ -240,13 +240,13 @@ private static Map simpleStatementNamedLiterals(Object statement } /** - * Substitutes each {@code ?} of {@code cql}, in order, with the corresponding entry of + * Substitutes each {@code ?} of {@code parameterisedQuery}, in order, with the corresponding entry of * {@code literals}. Falls back when the two counts differ, since the statement is then not the * one we think it is and a partial substitution would report a query that was never executed. */ - private static String interpolatePositional(String cql, List literals, String fallbackCql) { + private static String interpolatePositional(String parameterisedQuery, List literals, String fallbackCql) { int[] consumed = new int[1]; - String interpolated = substitute(cql, (name, positionalIndex) -> { + String interpolated = substitute(parameterisedQuery, (name, positionalIndex) -> { if (name != null || positionalIndex >= literals.size()) { return null; } @@ -261,19 +261,19 @@ private static String interpolatePositional(String cql, List literals, S } /** - * Substitutes each {@code :name} of {@code cql} with the matching entry of {@code literals}. + * Substitutes each {@code :name} of {@code parameterisedQuery} with the matching entry of {@code literals}. * Entries of {@code literals} that no marker refers to are tolerated; a marker with no entry * falls back. */ - private static String interpolateNamed(String cql, Map literals, String fallbackCql) { - String interpolated = substitute(cql, (name, positionalIndex) -> + private static String interpolateNamed(String parameterisedQuery, Map literals, String fallbackCql) { + String interpolated = substitute(parameterisedQuery, (name, positionalIndex) -> name != null ? literals.get(name) : null); return interpolated != null ? interpolated : fallbackCql; } /** - * Walks {@code cql} once and replaces every bind marker found in code position with the literal + * Walks {@code parameterisedQuery} once and replaces every bind marker found in code position with the literal * {@code resolver} gives for it. *

* A {@code ?} or {@code :name} inside a string literal, a quoted identifier, a dollar-quoted @@ -283,23 +283,23 @@ private static String interpolateNamed(String cql, Map literals, * * @return the interpolated CQL, or {@code null} if any marker could not be resolved */ - private static String substitute(String cql, MarkerResolver resolver) { - StringBuilder out = new StringBuilder(cql.length() + 32); - int n = cql.length(); + private static String substitute(String parameterisedQuery, MarkerResolver resolver) { + StringBuilder out = new StringBuilder(parameterisedQuery.length() + 32); + int n = parameterisedQuery.length(); int i = 0; int positionalIndex = 0; while (i < n) { - char c = cql.charAt(i); + char c = parameterisedQuery.charAt(i); if (c == '\'' || c == '"') { - i = appendQuoted(cql, i, c, out); - } else if (c == '$' && next(cql, i) == '$') { - i = appendUntil(cql, i, DOLLAR_QUOTE, out); - } else if ((c == '-' && next(cql, i) == '-') || (c == '/' && next(cql, i) == '/')) { - i = appendLineComment(cql, i, out); - } else if (c == '/' && next(cql, i) == '*') { - i = appendUntil(cql, i, BLOCK_COMMENT_END, out); + i = appendQuoted(parameterisedQuery, i, c, out); + } else if (c == '$' && next(parameterisedQuery, i) == '$') { + i = appendUntil(parameterisedQuery, i, DOLLAR_QUOTE, out); + } else if ((c == '-' && next(parameterisedQuery, i) == '-') || (c == '/' && next(parameterisedQuery, i) == '/')) { + i = appendLineComment(parameterisedQuery, i, out); + } else if (c == '/' && next(parameterisedQuery, i) == '*') { + i = appendUntil(parameterisedQuery, i, BLOCK_COMMENT_END, out); } else if (c == '?') { String literal = resolver.literalFor(null, positionalIndex++); if (literal == null) { @@ -307,12 +307,12 @@ private static String substitute(String cql, MarkerResolver resolver) { } out.append(literal); i++; - } else if (c == ':' && isMarkerNameStart(next(cql, i))) { + } else if (c == ':' && isMarkerNameStart(next(parameterisedQuery, i))) { int end = i + 1; - while (end < n && isMarkerNamePart(cql.charAt(end))) { + while (end < n && isMarkerNamePart(parameterisedQuery.charAt(end))) { end++; } - String literal = resolver.literalFor(cql.substring(i + 1, end), -1); + String literal = resolver.literalFor(parameterisedQuery.substring(i + 1, end), -1); if (literal == null) { return null; } 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 2045ff748a..de20187a77 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 @@ -64,21 +64,21 @@ public static Object execute(Object cqlSession, String 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) { - String tracked = CqlBindMarkerInterpolator.forPositionalValues(cqlSession, query, values); - return handleCqlExecute(CASSANDRA_EXECUTE_STRING_POSITIONAL_VALUES_SYNC, cqlSession, tracked, query, values); + public static Object execute(Object cqlSession, String parameterisedQuery, Object... values) { + String interpolatedQuery = CqlBindMarkerInterpolator.forPositionalValues(cqlSession, parameterisedQuery, values); + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_POSITIONAL_VALUES_SYNC, cqlSession, interpolatedQuery, parameterisedQuery, 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) { - String tracked = CqlBindMarkerInterpolator.forNamedValues(cqlSession, query, values); - return handleCqlExecute(CASSANDRA_EXECUTE_STRING_NAMED_VALUES_SYNC, cqlSession, tracked, query, values); + public static Object execute(Object cqlSession, String parameterisedQuery, Map values) { + String interpolatedQuery = CqlBindMarkerInterpolator.forNamedValues(cqlSession, parameterisedQuery, values); + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_NAMED_VALUES_SYNC, cqlSession, interpolatedQuery, parameterisedQuery, 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) { - String tracked = CqlBindMarkerInterpolator.forStatement(cqlSession, statement, extractQueryText(statement)); - return handleCqlExecute(CASSANDRA_EXECUTE_STATEMENT_SYNC, cqlSession, tracked, statement); + String interpolatedQuery = CqlBindMarkerInterpolator.forStatement(cqlSession, statement, extractQueryText(statement)); + return handleCqlExecute(CASSANDRA_EXECUTE_STATEMENT_SYNC, cqlSession, interpolatedQuery, statement); } private static Object handleCqlExecute(String id, Object cqlSession, String queryForTracking, Object... invokeArgs) { From 7edcc06c1ebc966613d2b0f3ef448809c5a7e798 Mon Sep 17 00:00:00 2001 From: Gonzalo Tomas Guerrero Date: Tue, 29 Sep 2026 16:59:38 -0300 Subject: [PATCH 4/4] Rename values params --- .../CqlSessionClassReplacement.java | 12 ++++++------ 1 file changed, 6 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 de20187a77..72890f2508 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 @@ -64,15 +64,15 @@ public static Object execute(Object cqlSession, String 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 parameterisedQuery, Object... values) { - String interpolatedQuery = CqlBindMarkerInterpolator.forPositionalValues(cqlSession, parameterisedQuery, values); - return handleCqlExecute(CASSANDRA_EXECUTE_STRING_POSITIONAL_VALUES_SYNC, cqlSession, interpolatedQuery, parameterisedQuery, values); + public static Object execute(Object cqlSession, String parameterisedQuery, Object... parameterValues) { + String interpolatedQuery = CqlBindMarkerInterpolator.forPositionalValues(cqlSession, parameterisedQuery, parameterValues); + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_POSITIONAL_VALUES_SYNC, cqlSession, interpolatedQuery, parameterisedQuery, parameterValues); } @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 parameterisedQuery, Map values) { - String interpolatedQuery = CqlBindMarkerInterpolator.forNamedValues(cqlSession, parameterisedQuery, values); - return handleCqlExecute(CASSANDRA_EXECUTE_STRING_NAMED_VALUES_SYNC, cqlSession, interpolatedQuery, parameterisedQuery, values); + public static Object execute(Object cqlSession, String parameterisedQuery, Map parameterValues) { + String interpolatedQuery = CqlBindMarkerInterpolator.forNamedValues(cqlSession, parameterisedQuery, parameterValues); + return handleCqlExecute(CASSANDRA_EXECUTE_STRING_NAMED_VALUES_SYNC, cqlSession, interpolatedQuery, parameterisedQuery, parameterValues); } @Replacement(type = ReplacementType.TRACKER, id = CASSANDRA_EXECUTE_STATEMENT_SYNC, usageFilter = UsageFilter.ANY, category = ReplacementCategory.CASSANDRA, castTo = RESULT_SET_CLASS)