Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client-java/instrumentation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
<version>${springboot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-cassandra</artifactId>
<version>${springboot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ public StatementDescription(String line, String method) {

private final Set<MongoCollectionSchema> mongoCollectionSchemaData = new CopyOnWriteArraySet<>();

private final Set<CassandraTableSchema> cassandraTableSchemaData = new CopyOnWriteArraySet<>();

public Set<ExecutedSqlCommand> getSqlInfoData(){
return Collections.unmodifiableSet(executedSqlCommandData);
}
Expand Down Expand Up @@ -150,6 +152,10 @@ public Set<MongoCollectionSchema> getMongoCollectionTypeData(){
return Collections.unmodifiableSet(mongoCollectionSchemaData);
}

public Set<CassandraTableSchema> getCassandraTableTypeData(){
return Collections.unmodifiableSet(cassandraTableSchemaData);
}

public void addSqlInfo(ExecutedSqlCommand info){
executedSqlCommandData.add(info);
}
Expand Down Expand Up @@ -182,6 +188,10 @@ public void addMongoCollectionType(MongoCollectionSchema mongoCollectionSchema){
mongoCollectionSchemaData.add(mongoCollectionSchema);
}

public void addCassandraTableType(CassandraTableSchema cassandraTableSchema){
cassandraTableSchemaData.add(cassandraTableSchema);
}

public Set<String> getParsedDtoNamesView(){
return Collections.unmodifiableSet(parsedDtoNames);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.evomaster.client.java.instrumentation;

import java.io.Serializable;
import java.util.Objects;

/**
* Schema of rows in a Cassandra table.
*/
public class CassandraTableSchema implements Serializable {
private final String tableName;
private final String tableSchema;

public CassandraTableSchema(String tableName, String tableSchema) {
this.tableName = tableName;
this.tableSchema = tableSchema;
}

public String getTableName() {
return tableName;
}

public String getTableSchema() {
return tableSchema;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CassandraTableSchema that = (CassandraTableSchema) o;
return Objects.equals(tableName, that.tableName) && Objects.equals(tableSchema, that.tableSchema);
}

@Override
public int hashCode() {
return Objects.hash(tableName, tableSchema);
}

@Override
public String toString() {
return "CassandraTableSchema{" +
"tableName='" + tableName + '\'' +
", tableSchema='" + tableSchema + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static List<MethodReplacementClass> getList() {
new Base64DecoderClassReplacement(),
new BooleanClassReplacement(),
new ByteClassReplacement(),
new CassandraTemplateClassReplacement(),
new CharacterClassReplacement(),
new CollectionClassReplacement(),
new CqlSessionClassReplacement(),
Expand All @@ -54,6 +55,7 @@ public static List<MethodReplacementClass> getList() {
new LocalTimeClassReplacement(),
new LongClassReplacement(),
new MapClassReplacement(),
new MappingCassandraEntityInformationClassReplacement(),
new MatcherClassReplacement(),
new MessageBodyReaderClassReplacement(),
new MethodClassReplacement(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;

import org.evomaster.client.java.instrumentation.CassandraTableSchema;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.UsageFilter;
import org.evomaster.client.java.instrumentation.object.ClassToSchema;
import org.evomaster.client.java.instrumentation.shared.ReplacementCategory;
import org.evomaster.client.java.instrumentation.shared.ReplacementType;
import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

/**
* The intention of this replacement is the same as {@link MappingCassandraEntityInformationClassReplacement}:
* retrieve the entity-type-to-table mapping. But that constructor replacement only fires when Spring Data
* instantiates a {@code CassandraRepository}; a SUT calling {@code CassandraTemplate} directly (bypassing
* the repository layer) needs this replacement to have that mapping recorded too.
*/
public class CassandraTemplateClassReplacement extends ThirdPartyMethodReplacementClass {

private static final CassandraTemplateClassReplacement singleton = new CassandraTemplateClassReplacement();

@Override
protected String getNameOfThirdPartyTargetClass() {
return "org.springframework.data.cassandra.core.CassandraTemplate";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to monitor the schema directly from the cassandra library without having to have hooks in the spring-data library? For MongoDB this was done since the schema was lost from the spring-data library to the MongoDB library.

@gonzalotguerrero gonzalotguerrero Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! I've been doing some more research on the use of Session.getMetadata() we discussed in person.

Extracting the relevant columns directly from every intercepted query in CqlSessionClassReplacement initially sounds OK, but if there's no WHERE clause or if it does not contain all clustering columns, we'd be missing critical metadata, as all columns belonging to the table's primary key must be set during an insertion.

The TableMetadata interface inherits a getPrimaryKey() method from RelationMetadata. There, we could obtain the metadata for all columns that must be set in every insertion. Also these columns cannot be altered, so they could be cached.

Given that, I think with the use of getMetadata() we could cache the primary key of every queried table, and, for every query against a table whose primary key has already been cached, only search for the metadata regarding the non-PK columns present in the WHERE clause, if any.

What do you think of this approach?

}

private static final String INSERT_ID = "insert";
private static final String SELECT_ONE_ID = "selectOneString";
private static final String SELECT_ID = "selectString";

@Replacement(replacingStatic = false,
type = ReplacementType.TRACKER,
id = INSERT_ID,
usageFilter = UsageFilter.ANY,
category = ReplacementCategory.CASSANDRA)
public static <T> T insert(Object cassandraTemplate, T entity) {
try {
addCassandraTableType(cassandraTemplate, entity.getClass());

Method insertMethod = getOriginal(singleton, INSERT_ID, cassandraTemplate);
Object result = insertMethod.invoke(cassandraTemplate, entity);
return (T) result;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw (RuntimeException) e.getCause();
}
}

@Replacement(replacingStatic = false,
type = ReplacementType.TRACKER,
id = SELECT_ONE_ID,
usageFilter = UsageFilter.ANY,
category = ReplacementCategory.CASSANDRA)
public static <T> T selectOne(Object cassandraTemplate, String cql, Class<T> entityClass) {
try {
addCassandraTableType(cassandraTemplate, entityClass);

Method selectOneMethod = getOriginal(singleton, SELECT_ONE_ID, cassandraTemplate);
Object result = selectOneMethod.invoke(cassandraTemplate, cql, entityClass);
return (T) result;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw (RuntimeException) e.getCause();
}
}

@Replacement(replacingStatic = false,
type = ReplacementType.TRACKER,
id = SELECT_ID,
usageFilter = UsageFilter.ANY,
category = ReplacementCategory.CASSANDRA)
public static <T> List<T> select(Object cassandraTemplate, String cql, Class<T> entityClass) {
try {
addCassandraTableType(cassandraTemplate, entityClass);

Method selectMethod = getOriginal(singleton, SELECT_ID, cassandraTemplate);
Object result = selectMethod.invoke(cassandraTemplate, cql, entityClass);
return (List<T>) result;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw (RuntimeException) e.getCause();
}
}

private static void addCassandraTableType(Object cassandraTemplate, Class<?> entityClass) {
try {
Object tableNameId = cassandraTemplate.getClass().getMethod("getTableName", Class.class)
.invoke(cassandraTemplate, entityClass);
String tableName = (String) tableNameId.getClass().getMethod("asInternal").invoke(tableNameId);

String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(entityClass, true, Collections.emptyList());
ExecutionTracer.addCassandraTableType(new CassandraTableSchema(tableName, schema));
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;

import org.evomaster.client.java.instrumentation.CassandraTableSchema;
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.object.ClassToSchema;
import org.evomaster.client.java.instrumentation.shared.ReplacementCategory;
import org.evomaster.client.java.instrumentation.shared.ReplacementType;
import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;

/**
* When a Cassandra repository is created in Spring, a CqlSession is used under the hood.
* But information about the type of the repository's rows is not transferred to the session.
* That info is retained on Spring side.
* So the intention of this replacement is to retrieve that type info.
* This will allow us to create and insert rows of the correct type in the table (and the repository).
*/
public class MappingCassandraEntityInformationClassReplacement extends ThirdPartyMethodReplacementClass {
private static final MappingCassandraEntityInformationClassReplacement singleton = new MappingCassandraEntityInformationClassReplacement();
private static ThreadLocal<Object> instance = new ThreadLocal<>();

public static final String CONSTRUCTOR_ENTITY_CONVERTER_ID = "constructorEntityConverter";

@Override
protected String getNameOfThirdPartyTargetClass() {
return "org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation";
}

public static Object consumeInstance() {
Object mappingCassandraEntityInformation = instance.get();
if (mappingCassandraEntityInformation == null) {
throw new IllegalStateException("No instance to consume");
}
instance.set(null);
return mappingCassandraEntityInformation;
}

private static void addInstance(Object x) {
Object mappingCassandraEntityInformation = instance.get();
if (mappingCassandraEntityInformation != null) {
throw new IllegalStateException("Previous instance was not consumed");
}
instance.set(x);
}

@Replacement(
replacingConstructor = true,
type = ReplacementType.TRACKER,
category = ReplacementCategory.CASSANDRA,
id = CONSTRUCTOR_ENTITY_CONVERTER_ID,
castTo = "org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation"
)
public static void MappingCassandraEntityInformation(
@ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity") Object entity,
@ThirdPartyCast(actualType = "org.springframework.data.cassandra.core.convert.CassandraConverter") Object converter) {
Constructor original = getOriginalConstructor(singleton, CONSTRUCTOR_ENTITY_CONVERTER_ID);

try {
Object mappingCassandraEntityInformation = original.newInstance(entity, converter);
addInstance(mappingCassandraEntityInformation);

Object tableNameId = mappingCassandraEntityInformation.getClass().getMethod("getTableName").invoke(mappingCassandraEntityInformation);
String tableName = (String) tableNameId.getClass().getMethod("asInternal").invoke(tableNameId);

Class<?> rowType = (Class<?>) mappingCassandraEntityInformation.getClass().getMethod("getJavaType").invoke(mappingCassandraEntityInformation);
String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(rowType, true, Collections.emptyList());

ExecutionTracer.addCassandraTableType(new CassandraTableSchema(tableName, schema));
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,12 @@ public static void addMongoCollectionType(MongoCollectionSchema mongoCollectionS
}
}

public static void addCassandraTableType(CassandraTableSchema cassandraTableSchema){
if (!executingInitCassandra) {
getCurrentAdditionalInfo().addCassandraTableType(cassandraTableSchema);
}
}


public static void markLastExecutedStatement(String lastLine, String lastMethod) {
getCurrentAdditionalInfo().pushLastExecutedStatement(lastLine, lastMethod);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.foo.somedifferentpackage.examples.methodreplacement;

import org.evomaster.client.java.instrumentation.example.cassandra.MappingCassandraEntityOperations;
import org.springframework.data.cassandra.core.convert.CassandraConverter;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation;

public class MappingCassandraEntityOperationsImpl implements MappingCassandraEntityOperations {
@Override
public <T, ID> MappingCassandraEntityInformation<?, ?> callMappingCassandraEntityInformation(CassandraPersistentEntity<T> entity, CassandraConverter converter) {
return new MappingCassandraEntityInformation<>(entity, converter);
}
}
Loading
Loading