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
@@ -1,6 +1,7 @@
package org.evomaster.core.solver

import org.evomaster.client.java.controller.api.dto.database.schema.TableDto
import org.evomaster.core.utils.StringUtils.convertToAscii
import org.evomaster.dbconstraint.ast.*
import org.evomaster.solver.smtlib.AssertSMTNode
import org.evomaster.solver.smtlib.EmptySMTNode
Expand Down Expand Up @@ -31,7 +32,7 @@ class SMTConditionVisitor(
* @return The SMT-LIB column reference string.
*/
private fun getColumnReference(tableName: String, columnName: String): String {
return "(${columnName.uppercase()} ${tableName.lowercase()}$rowIndex)"
return "(${convertToAscii(columnName).uppercase()} ${convertToAscii(tableName).lowercase()}$rowIndex)"
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.

add a comment to explain why this needs to be converted to ASCII

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.evomaster.core.search.gene.sql.SqlPrimaryKeyGene
import org.evomaster.core.search.gene.string.StringGene
import org.evomaster.core.sql.SqlAction
import org.evomaster.core.sql.schema.*
import org.evomaster.core.utils.StringUtils.convertToAscii
import org.evomaster.solver.Z3DockerExecutor
import org.evomaster.solver.smtlib.SMTLib
import org.evomaster.solver.smtlib.value.*
Expand Down Expand Up @@ -152,37 +153,42 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {

// Create the list of genes with the values
val genes = mutableListOf<Gene>()
for (columnName in columns.fields) {
var gene: Gene = IntegerGene(columnName, 0)
when (val columnValue = columns.getField(columnName)) {
// smtColumn is the Ascii version from SmtLib; resolve back to original DB column name
for (smtColumn in columns.fields) {
val dbColumn = table.columns.firstOrNull {
convertToAscii(it.name).equals(smtColumn, ignoreCase = true)
}
val dbColumnName = dbColumn?.name ?: smtColumn

var gene: Gene = IntegerGene(dbColumnName, 0)
when (val columnValue = columns.getField(smtColumn)) {
is StringValue -> {
gene = if (hasColumnType(schemaDto, table, columnName, "BOOLEAN")) {
BooleanGene(columnName, toBoolean(columnValue.value))
gene = if (hasColumnType(schemaDto, table, dbColumnName, "BOOLEAN")) {
BooleanGene(dbColumnName, toBoolean(columnValue.value))
} else {
StringGene(columnName, columnValue.value)
StringGene(dbColumnName, columnValue.value)
}
}
is LongValue -> {
gene = if (hasColumnType(schemaDto, table, columnName, "TIMESTAMP")) {
gene = if (hasColumnType(schemaDto, table, dbColumnName, "TIMESTAMP")) {
val epochSeconds = columnValue.value.toLong()
val localDateTime = LocalDateTime.ofInstant(
Instant.ofEpochSecond(epochSeconds), ZoneOffset.UTC
)
val formatted = localDateTime.format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
)
ImmutableDataHolderGene(columnName, formatted, inQuotes = true)
ImmutableDataHolderGene(dbColumnName, formatted, inQuotes = true)
} else {
IntegerGene(columnName, columnValue.value.toInt())
IntegerGene(dbColumnName, columnValue.value.toInt())
}
}
is RealValue -> {
gene = DoubleGene(columnName, columnValue.value)
gene = DoubleGene(dbColumnName, columnValue.value)
}
}
val currentColumn = table.columns.firstOrNull(){ it.name.equals(columnName, ignoreCase = true) }
if (currentColumn != null && currentColumn.primaryKey) {
gene = SqlPrimaryKeyGene(columnName, table.id, gene, actionId)
if (dbColumn != null && dbColumn.primaryKey) {
gene = SqlPrimaryKeyGene(dbColumnName, table.id, gene, actionId)
}
gene.markAllAsInitialized()
genes.add(gene)
Expand Down
61 changes: 34 additions & 27 deletions core/src/main/kotlin/org/evomaster/core/solver/SmtLibGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import org.evomaster.core.utils.StringUtils
import org.evomaster.dbconstraint.ConstraintDatabaseType
import org.evomaster.dbconstraint.ast.SqlCondition
import net.sf.jsqlparser.JSQLParserException
import org.evomaster.core.utils.StringUtils.convertToAscii
import org.evomaster.dbconstraint.parser.SqlConditionParserException
import org.evomaster.dbconstraint.parser.jsql.JSqlConditionParser
import org.evomaster.solver.smtlib.*
import org.evomaster.solver.smtlib.assertion.*
import java.util.*

/**
* Generates SMT-LIB constraints from SQL queries and schema definitions.
Expand Down Expand Up @@ -58,7 +58,8 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
*/
private fun appendTableDefinitions(smt: SMTLib) {
for (table in schema.tables) {
val dataTypeName = "${StringUtils.capitalization(table.id.name)}Row"
val smtTableName = convertToAscii(table.id.name)
val dataTypeName = "${StringUtils.capitalization(smtTableName)}Row"

// Declare datatype for the table
smt.addNode(
Expand All @@ -67,7 +68,7 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I

// Declare constants for each row
for (i in 1..numberOfRows) {
smt.addNode(DeclareConstSMTNode("${table.id.name.lowercase()}$i", dataTypeName))
smt.addNode(DeclareConstSMTNode("${smtTableName.lowercase()}$i", dataTypeName))
}
}
}
Expand All @@ -91,10 +92,10 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
* @param table The table for which unique constraints are added.
*/
private fun appendUniqueConstraints(smt: SMTLib, table: TableDto) {
val tableName = table.id.name.lowercase()
val smtTableName = convertToAscii(table.id.name).lowercase()
for (column in table.columns) {
if (column.unique) {
val nodes = assertForDistinctField(column.name, tableName)
val nodes = assertForDistinctField(convertToAscii(column.name), smtTableName)
smt.addNodes(nodes)
}
}
Expand Down Expand Up @@ -131,7 +132,7 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
* @return The corresponding SMT node.
*/
private fun parseCheckExpression(table: TableDto, condition: SqlCondition, index: Int): SMTNode {
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.

maybe not for this PR, but something to discuss as well with @jgaleotti , why is most of the code here relaying on TableDto and not org.evomaster.core.sql.schema.Table. DTOs are only meant as simple POJOs for transfer of data between processes, and not for business logic computation

val visitor = SMTConditionVisitor(table.id.name.lowercase(), emptyMap(), schema.tables, index)
val visitor = SMTConditionVisitor(convertToAscii(table.id.name).lowercase(), emptyMap(), schema.tables, index)
return condition.accept(visitor, null) as SMTNode
}

Expand Down Expand Up @@ -167,21 +168,21 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I

private fun appendBooleanConstraints(smt: SMTLib) {
for (table in schema.tables) {
val tableName = table.id.name.lowercase()
val smtTableName = convertToAscii(table.id.name).lowercase()
for (column in table.columns) {
if (column.type.equals("BOOLEAN", ignoreCase = true)) {
val columnName = column.name.uppercase()
val columnName = convertToAscii(column.name).uppercase()
for (i in 1..numberOfRows) {
smt.addNode(
AssertSMTNode(
OrAssertion(
listOf(
EqualsAssertion(listOf("($columnName $tableName$i)", "\"true\"")),
EqualsAssertion(listOf("($columnName $tableName$i)", "\"True\"")),
EqualsAssertion(listOf("($columnName $tableName$i)", "\"TRUE\"")),
EqualsAssertion(listOf("($columnName $tableName$i)", "\"false\"")),
EqualsAssertion(listOf("($columnName $tableName$i)", "\"False\"")),
EqualsAssertion(listOf("($columnName $tableName$i)", "\"FALSE\""))
EqualsAssertion(listOf("($columnName $smtTableName$i)", "\"true\"")),
EqualsAssertion(listOf("($columnName $smtTableName$i)", "\"True\"")),
EqualsAssertion(listOf("($columnName $smtTableName$i)", "\"TRUE\"")),
EqualsAssertion(listOf("($columnName $smtTableName$i)", "\"false\"")),
EqualsAssertion(listOf("($columnName $smtTableName$i)", "\"False\"")),
EqualsAssertion(listOf("($columnName $smtTableName$i)", "\"FALSE\""))
)
)
)
Expand All @@ -194,26 +195,26 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I

private fun appendTimestampConstraints(smt: SMTLib) {
for (table in schema.tables) {
val tableName = table.id.name.lowercase()
val smtTableName = convertToAscii(table.id.name).lowercase()
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.

there are so many calls to this convertToAscii, which makes it more difficult to read, and possibly easy to make mistake in future if code needs changing and missing a needed call. maybe create a new SmtTable class with that that you need, and pass that around the code instead of doing the conversions each time?

for (column in table.columns) {
if (column.type.equals("TIMESTAMP", ignoreCase = true)) {
val columnName = column.name.uppercase()
val columnName = convertToAscii(column.name).uppercase()
val lowerBound = 0 // Example for Unix epoch start
val upperBound = 32503680000 // Example for year 3000 in seconds

for (i in 1..numberOfRows) {
smt.addNode(
AssertSMTNode(
GreaterThanOrEqualsAssertion(
"($columnName $tableName$i)",
"($columnName $smtTableName$i)",
lowerBound.toString()
)
)
)
smt.addNode(
AssertSMTNode(
LessThanOrEqualsAssertion(
"($columnName $tableName$i)",
"($columnName $smtTableName$i)",
upperBound.toString()
)
)
Expand All @@ -232,11 +233,11 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
* @param table The table for which primary key constraints are added.
*/
private fun appendPrimaryKeyConstraints(smt: SMTLib, table: TableDto) {
val tableName = table.id.name.lowercase()
val smtTableName = convertToAscii(table.id.name).lowercase()
val primaryKeys = table.columns.filter { it.primaryKey }

for (primaryKey in primaryKeys) {
val nodes = assertForDistinctField(primaryKey.name, tableName)
val nodes = assertForDistinctField(convertToAscii(primaryKey.name), smtTableName)
smt.addNodes(nodes)
}
}
Expand Down Expand Up @@ -274,16 +275,16 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
* @param table The table for which foreign key constraints are added.
*/
private fun appendForeignKeyConstraints(smt: SMTLib, table: TableDto) {
val sourceTableName = table.id.name.lowercase()
val sourceTableName = convertToAscii(table.id.name).lowercase()

for (foreignKey in table.foreignKeys) {
val referencedTable = findReferencedTable(foreignKey)
val referencedTableName = referencedTable.id.name.lowercase()
val referencedColumnSelector = findReferencedPKSelector(table, referencedTable, foreignKey)
val referencedTableName = convertToAscii(referencedTable.id.name).lowercase()
val referencedColumnSelector = convertToAscii(findReferencedPKSelector(table, referencedTable, foreignKey))

for (sourceColumn in foreignKey.sourceColumns) {
val nodes = assertForEqualsAny(
sourceColumn, sourceTableName,
convertToAscii(sourceColumn), sourceTableName,
referencedColumnSelector, referencedTableName
)
smt.addNodes(nodes)
Expand Down Expand Up @@ -434,7 +435,7 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
* @return The corresponding SMT node.
*/
private fun parseQueryCondition(tableAliases: Map<String, String>, defaultTableName: String, condition: SqlCondition, index: Int): SMTNode {
val visitor = SMTConditionVisitor(defaultTableName, tableAliases, schema.tables, index)
val visitor = SMTConditionVisitor(convertToAscii(defaultTableName), tableAliases, schema.tables, index)
return condition.accept(visitor, null) as SMTNode
}

Expand Down Expand Up @@ -519,9 +520,10 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
// Only add GetValueSMTNode for the mentioned tables
for (table in schema.tables) {
val tableNameLower = table.id.name.lowercase()
val smtColumnName = convertToAscii(tableNameLower)
if (tablesMentioned.contains(tableNameLower)) {
for (i in 1..numberOfRows) {
smt.addNode(GetValueSMTNode("$tableNameLower$i"))
smt.addNode(GetValueSMTNode("$smtColumnName$i"))
}
}
}
Expand All @@ -537,22 +539,27 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
return table.columns.map { c ->
val smtType = TYPE_MAP[c.type.uppercase()]
?: throw RuntimeException("Unsupported column type: ${c.type}")
DeclareConstSMTNode(c.name, smtType)
DeclareConstSMTNode(convertToAscii(c.name), smtType)
}
}

companion object {

// Maps database column types to SMT-LIB types
private val TYPE_MAP = mapOf(
"BIGINT" to "Int",
"BIT" to "Int",
"INTEGER" to "Int",
"INT" to "Int",
"INT2" to "Int",
"INT4" to "Int",
"INT8" to "Int",
"TINYINT" to "Int",
"SMALLINT" to "Int",
"NUMERIC" to "Int",
"SERIAL" to "Int",
"SMALLSERIAL" to "Int",
"BIGSERIAL" to "Int",
"TIMESTAMP" to "Int",
"DATE" to "Int",
"FLOAT" to "Real",
Expand Down
21 changes: 19 additions & 2 deletions core/src/main/kotlin/org/evomaster/core/utils/StringUtils.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.evomaster.core.utils

import java.util.*

object StringUtils {

/**
Expand Down Expand Up @@ -82,4 +80,23 @@ object StringUtils {
}
return lines
}

/**
* Replaces non-ASCII characters in a name to make it a valid SMT-LIB identifier.
* SMT-LIB unquoted symbols are restricted to ASCII, so characters like Æ, Ø, Å must be transliterated.
*
* This is needed because our test suite includes Norwegian APIs whose database schemas
* contain column and table names with Norwegian characters (Æ, Ø, Å).
*
* Characters that do not decompose under NFD (Ø, Æ) are replaced explicitly.
* Characters that decompose under NFD (Å→A, and other accented letters like é, ü, ñ)
* are handled by normalizing to NFD form and stripping the remaining non-ASCII combining marks.
*/
fun convertToAscii(name: String): String {
val replaced = name
.replace('Ø', 'O').replace('ø', 'o')
.replace("Æ", "AE").replace("æ", "ae")
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.

what about Norwegian å and Å? or Swedish characters like Ä, ä, Ö and ö ? or other special characters in all other languages?
we cannot have ad-hoc solutions like this, only specific to our test data. Need a general solution.

return java.text.Normalizer.normalize(replaced, java.text.Normalizer.Form.NFD)
.replace(Regex("[^\\x00-\\x7F]"), "")
}
}
Loading