Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public static void initClass() {
connection.execute("CREATE KEYSPACE IF NOT EXISTS " + KEYSPACE +
" WITH replication = {'class':'SimpleStrategy','replication_factor':1}");
connection.execute("CREATE TABLE IF NOT EXISTS " + KEYSPACE + "." + TABLE +
" (id int PRIMARY KEY, name text, elapsed duration)");
" (id int PRIMARY KEY, name text, elapsed duration, ip inet," +
" tags set<text>, scores list<int>, favs map<text, int>)");
}

@AfterAll
Expand Down Expand Up @@ -97,6 +98,49 @@ public void testInsertDuration() {
assertEquals(2, connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).all().size());
}

/**
* An IP address is written as a quoted literal, whereas a collection is written as a delimited
* sequence of the literals of what it holds, with a list between square brackets and a set and
* a map between braces. This is what CassandraLiteralRenderer in the core module relies on when
* rendering a CqlCollectionGene and an InetGene, so it is checked here against a real Cassandra.
*/
@Test
public void testInsertInetAndCollections() {
List<CassandraInsertionDto> insertions = CassandraDsl.cassandra()
.insertInto(KEYSPACE, TABLE)
.d("id", "1")
.d("ip", "'127.0.0.1'")
.d("tags", "{'pet', 'cute'}")
.d("scores", "[17, 4, 2]")
.d("favs", "{'fruit': 3}")
.dtos();

CassandraInsertionResultsDto resultsDto = CassandraScriptRunner.executeInsert(connection, insertions);

assertTrue(resultsDto.executionResults.get(0));
assertEquals(1, connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).all().size());
}

/**
* A collection gene can be randomized into an empty one, so the literal it renders has to be
* accepted as well. Note that Cassandra stores an empty collection as null.
*/
@Test
public void testInsertEmptyCollections() {
List<CassandraInsertionDto> insertions = CassandraDsl.cassandra()
.insertInto(KEYSPACE, TABLE)
.d("id", "1")
.d("tags", "{}")
.d("scores", "[]")
.d("favs", "{}")
.dtos();

CassandraInsertionResultsDto resultsDto = CassandraScriptRunner.executeInsert(connection, insertions);

assertTrue(resultsDto.executionResults.get(0));
assertEquals(1, connection.execute("SELECT * FROM " + KEYSPACE + "." + TABLE).all().size());
}

@Test
public void testInsertionFailureDoesNotStopFollowingInsertions() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,40 @@ package org.evomaster.core.database.cassandra
import org.evomaster.core.search.gene.BooleanGene
import org.evomaster.core.search.gene.Gene
import org.evomaster.core.search.gene.UUIDGene
import org.evomaster.core.search.gene.cassandra.CqlCollectionGene
import org.evomaster.core.search.gene.cassandra.CqlCollectionKind
import org.evomaster.core.search.gene.cassandra.CqlDurationGene
import org.evomaster.core.search.gene.collection.ArrayGene
import org.evomaster.core.search.gene.collection.FixedMapGene
import org.evomaster.core.search.gene.datetime.DateGene
import org.evomaster.core.search.gene.datetime.DateTimeGene
import org.evomaster.core.search.gene.datetime.TimeGene
import org.evomaster.core.search.gene.network.InetGene
import org.evomaster.core.search.gene.numeric.*
import org.evomaster.core.search.gene.string.StringGene

/**
* Builds the gene used to generate the value of a Cassandra column, based on its CQL type.
*
* The collection types are handled by recursing on the types parameterizing them, so a column is
* only supported when all of the types composing it are.
*
* Two different reasons keep a CQL type out of the ones handled here:
* - the value of a column of that type cannot be generated at all, ie a counter, which is only
* writable with an UPDATE, and a timeuuid, which requires a version 1 UUID, whereas [UUIDGene]
* generates a random one;
* - no gene generating a value of that type has been written yet, ie blob, inet, the collections
* - no gene generating a value of that type has been written yet, ie blob, the tuples, the vectors
* and the user defined types.
*/
object CassandraColumnGeneBuilder {

/**
* The name given to the genes generating what a collection holds. Such genes are not bound to a
* column of their own, and the elements of a collection are written with no name in a CQL
* literal, so the name is only there to identify them while debugging.
*/
private const val ELEMENT_GENE_NAME = "element"

/**
* 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
Expand All @@ -41,6 +56,11 @@ object CassandraColumnGeneBuilder {
"double" to { name -> DoubleGene(name) },
"boolean" to { name -> BooleanGene(name) },
"uuid" to { name -> UUIDGene(name) },
/*
Only IPv4 addresses are generated for now, although the CQL type also accepts IPv6 ones, as
that is what InetGene builds. The same restriction already applies to the SQL types.
*/
"inet" to { name -> InetGene(name) },
/*
Only valid values are generated, as these genes are used to set up the state of the
database, and Cassandra would just reject an insertion carrying an invalid one.
Expand All @@ -52,23 +72,55 @@ object CassandraColumnGeneBuilder {
)

/**
* @return whether a gene can be built for [column], ie whether its CQL type is one of the
* scalar types handled here
* @return whether a gene can be built for [column], ie whether its CQL type is one of the ones
* handled here, or a collection of such types
*/
fun isSupported(column: CassandraColumn) = normalize(column.cqlType) in GENE_BUILDERS
fun isSupported(column: CassandraColumn) = isSupported(normalize(column.cqlType))

/**
* @throws IllegalArgumentException if the CQL type of [column] is not handled, as verifiable
* beforehand with [isSupported]
*/
fun buildGene(column: CassandraColumn): Gene {
fun buildGene(column: CassandraColumn): Gene = buildGene(column.name, normalize(column.cqlType))

private fun isSupported(cqlType: String): Boolean {

val builder = GENE_BUILDERS[normalize(column.cqlType)]
?: throw IllegalArgumentException("Cannot handle the CQL type of column $column")
val collection = CqlCollectionTypeParser.parse(cqlType) ?: return cqlType in GENE_BUILDERS

return builder(column.name)
return collection.parameters.all { isSupported(normalize(it)) }
}

/**
* @param cqlType a normalized CQL type
*/
private fun buildGene(name: String, cqlType: String): Gene {

CqlCollectionTypeParser.parse(cqlType)?.let { return buildCollectionGene(name, it) }

val builder = GENE_BUILDERS[cqlType]
?: throw IllegalArgumentException("Cannot handle the CQL type $cqlType of column $name")

return builder(name)
}

private fun buildCollectionGene(name: String, type: CqlCollectionType): Gene {

val content = when (type.kind) {
CqlCollectionKind.LIST -> ArrayGene(name, template = elementGene(type, 0))
/*
Cassandra collapses the repeated elements of a set literal into a single one, so
generating them would just be wasted search effort.
*/
CqlCollectionKind.SET -> ArrayGene(name, template = elementGene(type, 0), uniqueElements = true)
CqlCollectionKind.MAP -> FixedMapGene(name, key = elementGene(type, 0), value = elementGene(type, 1))
}

return CqlCollectionGene(name, type.kind, content)
}

private fun elementGene(type: CqlCollectionType, index: Int) =
buildGene(ELEMENT_GENE_NAME, normalize(type.parameters[index]))

private fun normalize(cqlType: String) = cqlType.trim().lowercase()

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ package org.evomaster.core.database.cassandra
import org.evomaster.core.search.gene.BooleanGene
import org.evomaster.core.search.gene.Gene
import org.evomaster.core.search.gene.UUIDGene
import org.evomaster.core.search.gene.cassandra.CqlCollectionGene
import org.evomaster.core.search.gene.cassandra.CqlDurationGene
import org.evomaster.core.search.gene.collection.FixedMapGene
import org.evomaster.core.search.gene.collection.PairGene
import org.evomaster.core.search.gene.datetime.DateGene
import org.evomaster.core.search.gene.datetime.DateTimeGene
import org.evomaster.core.search.gene.datetime.TimeGene
import org.evomaster.core.search.gene.network.InetGene
import org.evomaster.core.search.gene.numeric.NumberGene
import org.evomaster.core.search.gene.string.StringGene

/**
* Renders the value of a gene as a CQL literal, ie as it would be written inside a CQL statement.
*
* This is needed because such literals are inserted verbatim into the INSERT command built on the
* client side, and how a value has to be written depends on its type: text and the temporal types
* are enclosed in single quotes, whereas numbers, booleans, uuids and durations are not.
* client side, and how a value has to be written depends on its type: text, the temporal types and
* the IP addresses are enclosed in single quotes, whereas numbers, booleans, uuids and durations are
* not, and the collections are written as a delimited sequence of the literals of what they hold.
*/
object CassandraLiteralRenderer {

Expand All @@ -26,21 +31,44 @@ object CassandraLiteralRenderer {
*/
private const val ESCAPED_SINGLE_QUOTE = "''"

private const val ELEMENT_SEPARATOR = ", "

private const val KEY_VALUE_SEPARATOR = ": "

/**
* @throws IllegalArgumentException if there is no known CQL representation for [gene], which
* should not happen for the genes built by [CassandraColumnGeneBuilder]
*/
fun toCqlLiteral(gene: Gene): String {

val value = gene.getValueAsRawString()

return when (gene) {
is StringGene, is DateGene, is TimeGene, is DateTimeGene -> quote(value)
is BooleanGene, is UUIDGene, is NumberGene<*>, is CqlDurationGene -> value
is StringGene, is DateGene, is TimeGene, is DateTimeGene, is InetGene -> quote(gene.getValueAsRawString())
is BooleanGene, is UUIDGene, is NumberGene<*>, is CqlDurationGene -> gene.getValueAsRawString()
is CqlCollectionGene -> renderCollection(gene)
else -> throw IllegalArgumentException("Cannot render a CQL literal for a gene of type ${gene.javaClass.simpleName}")
}
}

/**
* The elements are rendered by recursing, rather than by asking the collection gene to print
* itself, as each of them has to be written the way a CQL literal of its own type is, eg with a
* text enclosed in single quotes rather than in the double quotes a gene prints itself with.
*/
private fun renderCollection(gene: CqlCollectionGene): String {

val content = gene.content

val entries = when (content) {
is FixedMapGene<*, *> -> content.getViewOfChildren().map { renderEntry(it as PairGene<*, *>) }
else -> content.getViewOfChildren().map { toCqlLiteral(it) }
}

return entries.joinToString(ELEMENT_SEPARATOR, gene.kind.opening, gene.kind.closing)
}

private fun renderEntry(entry: PairGene<*, *>) =
toCqlLiteral(entry.first) + KEY_VALUE_SEPARATOR + toCqlLiteral(entry.second)

private fun quote(value: String) =
SINGLE_QUOTE + value.replace(SINGLE_QUOTE, ESCAPED_SINGLE_QUOTE) + SINGLE_QUOTE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ object CassandraTableSchemaParser {

private const val CLUSTERING_COLUMN_SUFFIX = " CLUSTERING"

private const val TYPE_PARAMETERS_START = '<'

private const val TYPE_PARAMETERS_END = '>'

/**
* @param tableSchema the description of all the columns of a table, as reported by the SUT driver
* @return the columns described in [tableSchema], in the same order
Expand All @@ -40,43 +36,8 @@ object CassandraTableSchemaParser {
* @throws IllegalArgumentException if the type parameter lists are not balanced, as then there
* is no telling which of the separators are the ones between columns
*/
private fun splitColumns(tableSchema: String): List<String> {

val columns = mutableListOf<String>()
val current = StringBuilder()
var depth = 0

for (c in tableSchema) {
when {
c == TYPE_PARAMETERS_START -> {
depth++
current.append(c)
}
c == TYPE_PARAMETERS_END -> {
if (depth == 0) {
throw IllegalArgumentException("Unbalanced type parameters in the description" +
" of the columns of a Cassandra table: $tableSchema")
}
depth--
current.append(c)
}
c == COLUMN_SEPARATOR && depth == 0 -> {
columns.add(current.toString())
current.clear()
}
else -> current.append(c)
}
}

if (depth != 0) {
throw IllegalArgumentException("Unbalanced type parameters in the description" +
" of the columns of a Cassandra table: $tableSchema")
}

columns.add(current.toString())

return columns
}
private fun splitColumns(tableSchema: String) =
CqlTypeParameters.splitAtTopLevel(tableSchema, COLUMN_SEPARATOR)

private fun parseColumn(description: String): CassandraColumn {

Expand Down
Loading
Loading