Skip to content

Commit 802cb6d

Browse files
authored
Solver: avoid failing when nullable columns & Fix column name in join (#1476)
* Fix solver generation when comparision to null * Add support for not equal * Not fail when no support conditions happen * Apply suggestions from code review Co-authored-by: Agustina Aldasoro <agusaldasoro@users.noreply.github.com> * Add more supported types and fix join column name
1 parent 7235a54 commit 802cb6d

5 files changed

Lines changed: 116 additions & 24 deletions

File tree

core-extra/dbconstraint/src/main/java/org/evomaster/dbconstraint/ast/SqlComparisonOperator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
public enum SqlComparisonOperator {
44
EQUALS_TO("="),
5+
NOT_EQUALS_TO("<>"),
56
GREATER_THAN(">"),
67
GREATER_THAN_OR_EQUAL(">="),
78
LESS_THAN("<"),

core-extra/dbconstraint/src/main/java/org/evomaster/dbconstraint/parser/jsql/JSqlVisitor.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,11 @@ public void visit(MinorThanEquals minorThanEquals) {
310310

311311
@Override
312312
public void visit(NotEqualsTo notEqualsTo) {
313-
// TODO This translation should be implemented
314-
throw new RuntimeException("Extraction of condition not yet implemented");
313+
notEqualsTo.getLeftExpression().accept(this);
314+
SqlCondition left = stack.pop();
315+
notEqualsTo.getRightExpression().accept(this);
316+
SqlCondition right = stack.pop();
317+
stack.push(new SqlComparisonCondition(left, SqlComparisonOperator.NOT_EQUALS_TO, right));
315318
}
316319

317320
@Override

core/src/main/kotlin/org/evomaster/core/solver/SMTConditionVisitor.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ class SMTConditionVisitor(
4242
* @return The corresponding SMT node.
4343
*/
4444
override fun visit(condition: SqlAndCondition, parameter: Void?): SMTNode {
45-
val left = condition.leftExpr.accept(this, parameter) as AssertSMTNode
46-
val right = condition.rightExpr.accept(this, parameter) as AssertSMTNode
47-
return AssertSMTNode(AndAssertion(listOf(left.assertion, right.assertion)))
45+
val left = condition.leftExpr.accept(this, parameter)
46+
val right = condition.rightExpr.accept(this, parameter)
47+
if (left is EmptySMTNode && right is EmptySMTNode) return EmptySMTNode()
48+
if (left is EmptySMTNode) return right
49+
if (right is EmptySMTNode) return left
50+
return AssertSMTNode(AndAssertion(listOf((left as AssertSMTNode).assertion, (right as AssertSMTNode).assertion)))
4851
}
4952

5053
/**
@@ -55,8 +58,11 @@ class SMTConditionVisitor(
5558
* @return The corresponding SMT node.
5659
*/
5760
override fun visit(condition: SqlOrCondition, parameter: Void?): SMTNode {
58-
val conditions = condition.orConditions.map { it.accept(this, parameter) as AssertSMTNode }
59-
return AssertSMTNode(OrAssertion(conditions.map { it.assertion }))
61+
val conditions = condition.orConditions.map { it.accept(this, parameter) }
62+
val nonEmpty = conditions.filterIsInstance<AssertSMTNode>()
63+
if (nonEmpty.isEmpty()) return EmptySMTNode()
64+
if (nonEmpty.size == 1) return nonEmpty[0]
65+
return AssertSMTNode(OrAssertion(nonEmpty.map { it.assertion }))
6066
}
6167

6268
/**
@@ -67,6 +73,9 @@ class SMTConditionVisitor(
6773
* @return The corresponding SMT node.
6874
*/
6975
override fun visit(condition: SqlComparisonCondition, parameter: Void?): SMTNode {
76+
if (condition.leftOperand is SqlNullLiteralValue || condition.rightOperand is SqlNullLiteralValue) {
77+
return EmptySMTNode() // TODO: Change this when we add support for nullable columns in the db schema
78+
}
7079
val left = getVariableAndLiteral(condition.leftOperand)
7180
val right = getVariableAndLiteral(condition.rightOperand)
7281

core/src/main/kotlin/org/evomaster/core/solver/SmtLibGenerator.kt

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.evomaster.core.logging.LoggingUtil
1414
import org.evomaster.core.utils.StringUtils
1515
import org.evomaster.dbconstraint.ConstraintDatabaseType
1616
import org.evomaster.dbconstraint.ast.SqlCondition
17+
import net.sf.jsqlparser.JSQLParserException
1718
import org.evomaster.dbconstraint.parser.SqlConditionParserException
1819
import org.evomaster.dbconstraint.parser.jsql.JSqlConditionParser
1920
import org.evomaster.solver.smtlib.*
@@ -114,7 +115,9 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
114115
smt.addNode(constraint)
115116
}
116117
} catch (e: SqlConditionParserException) {
117-
throw RuntimeException("Error parsing check expression: " + check.sqlCheckExpression, e)
118+
LoggingUtil.getInfoLogger().warn("Could not translate CHECK constraint to SMT-LIB, skipping: ${check.sqlCheckExpression}. Reason: ${e.message}")
119+
} catch (e: JSQLParserException) {
120+
LoggingUtil.getInfoLogger().warn("Could not translate CHECK constraint to SMT-LIB, skipping: ${check.sqlCheckExpression}. Reason: ${e.message}")
118121
}
119122
}
120123
}
@@ -276,7 +279,7 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
276279
for (foreignKey in table.foreignKeys) {
277280
val referencedTable = findReferencedTable(foreignKey)
278281
val referencedTableName = referencedTable.id.name.lowercase()
279-
val referencedColumnSelector = findReferencedPKSelector(referencedTable, foreignKey)
282+
val referencedColumnSelector = findReferencedPKSelector(table, referencedTable, foreignKey)
280283

281284
for (sourceColumn in foreignKey.sourceColumns) {
282285
val nodes = assertForEqualsAny(
@@ -328,13 +331,24 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
328331
* @param foreignKey The foreign key constraint.
329332
* @return The primary key column name in the referenced table.
330333
*/
331-
private fun findReferencedPKSelector(referencedTable: TableDto, foreignKey: ForeignKeyDto): String {
334+
private fun findReferencedPKSelector(sourceTable: TableDto, referencedTable: TableDto, foreignKey: ForeignKeyDto): String {
332335
val referencedPrimaryKeys = referencedTable.columns.filter { it.primaryKey }
333-
if (referencedPrimaryKeys.isEmpty()) {
334-
throw RuntimeException("Referenced table has no primary key: ${foreignKey.targetTable}")
336+
val sourceColumnName = foreignKey.sourceColumns.firstOrNull()
337+
val sourceSmtType = sourceColumnName?.let { scn ->
338+
sourceTable.columns.firstOrNull { it.name.equals(scn, ignoreCase = true) }
339+
?.let { TYPE_MAP[it.type.uppercase()] }
335340
}
336-
// Assuming single-column primary keys
337-
return referencedPrimaryKeys[0].name
341+
if (referencedPrimaryKeys.isNotEmpty() &&
342+
(sourceSmtType == null || TYPE_MAP[referencedPrimaryKeys[0].type.uppercase()] == sourceSmtType)) {
343+
return referencedPrimaryKeys[0].name
344+
}
345+
// No PK or type mismatch: find a type-compatible column
346+
if (sourceSmtType != null) {
347+
referencedTable.columns.firstOrNull { TYPE_MAP[it.type.uppercase()] == sourceSmtType }
348+
?.let { return it.name }
349+
}
350+
return referencedTable.columns.firstOrNull()?.name
351+
?: throw RuntimeException("Referenced table has no columns: ${foreignKey.targetTable}")
338352
}
339353

340354
/**
@@ -364,11 +378,15 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
364378
val where = plainSelect.where
365379

366380
if (where != null) {
367-
val condition = parser.parse(where.toString(), toDBType(schema.databaseType))
368-
val tableFromQuery = TablesNamesFinder().getTables(sqlQuery as Statement).first()
369-
for (i in 1..numberOfRows) {
370-
val constraint = parseQueryCondition(tableAliases, tableFromQuery, condition, i)
371-
smt.addNode(constraint)
381+
try {
382+
val condition = parser.parse(where.toString(), toDBType(schema.databaseType))
383+
val tableFromQuery = TablesNamesFinder().getTables(sqlQuery as Statement).first()
384+
for (i in 1..numberOfRows) {
385+
val constraint = parseQueryCondition(tableAliases, tableFromQuery, condition, i)
386+
smt.addNode(constraint)
387+
}
388+
} catch (e: RuntimeException) {
389+
LoggingUtil.getInfoLogger().warn("Could not translate WHERE clause to SMT-LIB, skipping: ${where}. Reason: ${e.message}")
372390
}
373391
}
374392
}
@@ -390,11 +408,15 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
390408
val onExpressions = join.onExpressions
391409
if (onExpressions.isNotEmpty()) {
392410
val onExpression = onExpressions.elementAt(0)
393-
val condition = parser.parse(onExpression.toString(), toDBType(schema.databaseType))
394-
val tableFromQuery = TablesNamesFinder().getTables(sqlQuery as Statement).first()
395-
for (i in 1..numberOfRows) {
396-
val constraint = parseQueryCondition(tableAliases, tableFromQuery, condition, i)
397-
smt.addNode(constraint)
411+
try {
412+
val condition = parser.parse(onExpression.toString(), toDBType(schema.databaseType))
413+
val tableFromQuery = TablesNamesFinder().getTables(sqlQuery as Statement).first()
414+
for (i in 1..numberOfRows) {
415+
val constraint = parseQueryCondition(tableAliases, tableFromQuery, condition, i)
416+
smt.addNode(constraint)
417+
}
418+
} catch (e: RuntimeException) {
419+
LoggingUtil.getInfoLogger().warn("Could not translate JOIN ON clause to SMT-LIB, skipping: ${onExpression}. Reason: ${e.message}")
398420
}
399421
}
400422
}
@@ -525,8 +547,12 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
525547
"BIGINT" to "Int",
526548
"BIT" to "Int",
527549
"INTEGER" to "Int",
550+
"INT2" to "Int",
551+
"INT4" to "Int",
552+
"INT8" to "Int",
528553
"TINYINT" to "Int",
529554
"SMALLINT" to "Int",
555+
"NUMERIC" to "Int",
530556
"TIMESTAMP" to "Int",
531557
"DATE" to "Int",
532558
"FLOAT" to "Real",
@@ -539,6 +565,10 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
539565
"TEXT" to "String",
540566
"CHARACTER LARGE OBJECT" to "String",
541567
"BOOLEAN" to "String",
568+
"BOOL" to "String",
569+
"UUID" to "String",
570+
"JSONB" to "String",
571+
"BYTEA" to "String",
542572
)
543573
}
544574
}

core/src/test/kotlin/org/evomaster/core/solver/SmtLibGeneratorTest.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,27 @@ class SmtLibGeneratorTest {
209209
}
210210
}
211211

212+
/**
213+
* Test that a <> (not-equals) condition in the WHERE clause is correctly translated
214+
* to a distinct SMT-LIB assertion.
215+
*/
216+
@Test
217+
@Throws(JSQLParserException::class)
218+
fun selectFromUsersWithNotEquals() {
219+
val selectStatement: Statement = CCJSqlParserUtil.parse("SELECT * FROM Users WHERE age <> 30;")
220+
221+
val response: SMTLib = generator.generateSMT(selectStatement)
222+
223+
val expected = tableConstraints
224+
expected.addNode(AssertSMTNode(DistinctAssertion(listOf("(AGE users1)", "30"))))
225+
expected.addNode(AssertSMTNode(DistinctAssertion(listOf("(AGE users2)", "30"))))
226+
expected.addNode(CheckSatSMTNode())
227+
expected.addNode(GetValueSMTNode("users1"))
228+
expected.addNode(GetValueSMTNode("users2"))
229+
230+
assertEquals(expected, response)
231+
}
232+
212233
/**
213234
* Test the generation of SMT from a simple select statement and a database schema
214235
* @throws JSQLParserException if the statement is not valid
@@ -383,6 +404,34 @@ class SmtLibGeneratorTest {
383404
assertEquals(expected, response)
384405
}
385406

407+
/**
408+
* Test that NULL comparisons in the WHERE clause are skipped (not emitted as invalid SMT-LIB),
409+
* and the remaining non-null constraints are still applied.
410+
*/
411+
@Test
412+
@Throws(JSQLParserException::class)
413+
fun selectFromUsersWithNullComparison() {
414+
val selectStatement: Statement = CCJSqlParserUtil.parse("SELECT * FROM Users WHERE name = NULL AND age > 30;")
415+
416+
val response: SMTLib = generator.generateSMT(selectStatement)
417+
418+
val expected = tableConstraints
419+
// The NULL comparison is skipped; only the non-null constraint is emitted
420+
expected.addNode(AssertSMTNode(GreaterThanAssertion("(AGE users1)", "30")))
421+
expected.addNode(AssertSMTNode(GreaterThanAssertion("(AGE users2)", "30")))
422+
423+
val satConstraints = arrayOf(
424+
CheckSatSMTNode(),
425+
GetValueSMTNode("users1"),
426+
GetValueSMTNode("users2")
427+
)
428+
for (constraint in satConstraints) {
429+
expected.addNode(constraint)
430+
}
431+
432+
assertEquals(expected, response)
433+
}
434+
386435
/**
387436
* Test that a BIT column type is translated to Int in the SMT-LIB representation.
388437
*/

0 commit comments

Comments
 (0)