Skip to content

Commit 27d4730

Browse files
committed
Add comments
1 parent 3d8b0ea commit 27d4730

6 files changed

Lines changed: 74 additions & 5 deletions

File tree

core-extra/solver/src/main/java/org/evomaster/solver/smtlib/SMTResultParser.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public static Z3Solution parseZ3Response(String z3Response) {
4545
String[] lines = z3Response.split("\n");
4646

4747
for (String line : lines) {
48+
// Defensive: Z3DockerExecutor already classifies 'unsat'/'unknown' before invoking this
49+
// parser, so in practice only 'sat' models reach here. This guard is kept as a safety net
50+
// in case the parser is ever called directly with an unsat response.
4851
if (line.startsWith("unsat")) {
4952
throw new RuntimeException("Unsatisfiable problem");
5053
}

core/src/main/kotlin/org/evomaster/core/EMConfig.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1988,6 +1988,16 @@ class EMConfig {
19881988
@Min(0.0)
19891989
var sqlZ3TimeoutMs = DEFAULT_SQL_Z3_TIMEOUT_MS
19901990

1991+
@Experimental
1992+
@Cfg("Number of rows the Z3 solver generates per table when solving a failed SQL query. " +
1993+
"The default of 1 is sufficient for the currently supported queries; generating a single " +
1994+
"row per table already forces the query to return a non-empty result. This will need to be " +
1995+
"increased once support for more complex JOINs (matching arbitrary row combinations, not just " +
1996+
"the diagonal pairing of row i with row i) is added. Only meaningful when generateSqlDataWithZ3=true.")
1997+
@DependsOnTrueFor("generateSqlDataWithZ3")
1998+
@Min(1.0)
1999+
var sqlZ3NumberOfRows = 1
2000+
19912001
@Cfg("Enable EvoMaster to generate SQL data with direct accesses to the database. Use a search algorithm")
19922002
@DependsOnFalseFor("blackBox")
19932003
var generateSqlDataWithSearch = true

core/src/main/kotlin/org/evomaster/core/problem/api/service/ApiWsStructureMutator.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,10 @@ abstract class ApiWsStructureMutator : StructureMutator() {
442442

443443
val newActions = mutableListOf<List<SqlAction>>()
444444
for (query in failedWhereQueries) {
445-
val newActionsForQuery = z3Solver.solve(schemaDto, query)
445+
// numberOfRows is kept at 1 by default, which is enough to force a non-empty result for the
446+
// currently supported queries. It is configurable to allow experimenting with more rows once
447+
// support for complex JOINs (arbitrary row combinations) is added.
448+
val newActionsForQuery = z3Solver.solve(schemaDto, query, config.sqlZ3NumberOfRows)
446449
newActions.addAll(mutableListOf(newActionsForQuery))
447450
ind.addInitializingDbActions(actions = newActionsForQuery)
448451
}

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

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,16 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
167167

168168
val smtlibGenStart = System.currentTimeMillis()
169169
val generator = SmtLibGenerator(schemaDto, numberOfRows)
170-
val smtLib = generator.generateSMT(queryStatement)
170+
// SMT-LIB generation can throw for unsupported column types or query shapes it cannot handle
171+
// (e.g. a cast failure on an unexpected statement structure). Degrade gracefully to an empty
172+
// result instead of letting the exception propagate into the structure mutator.
173+
val smtLib = try {
174+
generator.generateSMT(queryStatement)
175+
} catch (e: RuntimeException) {
176+
LoggingUtil.getInfoLogger().warn("SQL-Z3: failed to generate SMT-LIB for query '$sqlQuery': ${e.message}")
177+
stats?.reportSqlZ3ParseFailure()
178+
return emptyList()
179+
}
171180
val smtlibBytes = smtLib.toString().toByteArray(StandardCharsets.UTF_8).size
172181
val smtlibGenMs = System.currentTimeMillis() - smtlibGenStart
173182
stats?.reportSqlZ3SmtlibGenTime(smtlibGenMs, smtlibBytes)
@@ -325,6 +334,18 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
325334
return value.equals("True", ignoreCase = true)
326335
}
327336

337+
/**
338+
* Whether the given column's SQL type (as reported in [ColumnDto.type]) equals [expectedType],
339+
* compared case-insensitively as a raw string.
340+
*
341+
* CAVEAT: this relies on [ColumnDto.type] containing the exact spelling passed in (currently
342+
* "BOOLEAN" and "TIMESTAMP"). It is needed because the SMT sort alone cannot recover these types
343+
* (BOOLEAN is encoded as an SMT String, TIMESTAMP as an SMT Int), so gene reconstruction must
344+
* consult the original SQL type. The set of type spellings recognized here must stay consistent
345+
* with [SmtLibGenerator.TYPE_MAP]; if a backend reports a variant spelling (e.g. "BOOL" or
346+
* "TIMESTAMP WITHOUT TIME ZONE"), the special handling is silently skipped. Consolidating these
347+
* type vocabularies into a single source of truth is future work.
348+
*/
328349
private fun hasColumnType(
329350
schemaDto: DbInfoDto,
330351
table: Table,
@@ -346,13 +367,17 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
346367
}
347368

348369
/**
349-
* Extracts the table name from the key by removing the last character (index).
370+
* Extracts the table name from a row-constant key by removing the trailing row index.
371+
*
372+
* Row constants are named "${smtName}${i}" (e.g. "users1", "users2"). The trailing digits are
373+
* stripped so this works for any number of rows (e.g. "users10" -> "users"), not just single-digit
374+
* indices. Note: this still assumes table names themselves do not end in a digit.
350375
*
351376
* @param key The key containing the table name and index.
352377
* @return The extracted table name.
353378
*/
354379
private fun getTableName(key: String): String {
355-
return key.substring(0, key.length - 1)
380+
return key.replace(Regex("\\d+$"), "")
356381
}
357382

358383
/**
@@ -445,6 +470,13 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
445470
/**
446471
* Maps column types to ColumnDataType.
447472
*
473+
* FUTURE WORK: this recognizes only a small subset of SQL type spellings and falls back to
474+
* CHARACTER_VARYING for everything else. It is one of three independent type vocabularies that
475+
* interpret [ColumnDto.type] — the others being [SmtLibGenerator.TYPE_MAP] (SQL type -> SMT sort)
476+
* and [hasColumnType] (BOOLEAN/TIMESTAMP special-casing). These can silently disagree when a
477+
* backend reports a variant spelling. They should be consolidated into a single source of truth
478+
* so that generation and interpretation cannot drift; see the note on [hasColumnType].
479+
*
448480
* @param type The column type as a string.
449481
* @return The corresponding ColumnDataType.
450482
*/

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
312312
findReferencedPKSelector(smtTable.dto, referencedSmtTable.dto, foreignKey)
313313
)
314314

315+
// KNOWN LIMITATION: composite foreign keys are not fully supported. Each source column is
316+
// matched independently against a single referenced column, rather than constraining the
317+
// whole tuple of source columns to match a referenced tuple. This is correct for
318+
// single-column FKs (the common case) but under-models multi-column FKs. Fully supporting
319+
// composite FKs (as a tuple-level OR over referenced rows) is future work.
315320
for (sourceColumn in foreignKey.sourceColumns) {
316321
val nodes = assertForEqualsAny(
317322
smtTable.smtColumnName(sourceColumn), smtTable.smtName,
@@ -442,10 +447,18 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
442447
for (join in joins) {
443448
val onExpressions = join.onExpressions
444449
if (onExpressions.isNotEmpty()) {
450+
// KNOWN LIMITATION: only the first ON expression is used; a composite ON
451+
// (e.g. "a = b AND c = d") drops all but the first conjunct.
445452
val onExpression = onExpressions.elementAt(0)
446453
try {
447454
val condition = parser.parse(onExpression.toString(), toDBType(schema.databaseType))
448455
val tableFromQuery = TablesNamesFinder().getTables(sqlQuery as Statement).first()
456+
// KNOWN LIMITATION: the ON condition is translated with the SAME row index on
457+
// both sides ("diagonal pairing"): row i of one table is matched only with row i
458+
// of the other. This is sufficient at the default numberOfRows=1 to force a
459+
// non-empty JOIN, but it does not model full INNER JOIN semantics: for
460+
// numberOfRows>=2 it never explores mismatched-index pairs (e.g. users2 with
461+
// products1). Matching arbitrary row combinations is future work.
449462
for (i in 1..numberOfRows) {
450463
val constraint = parseQueryCondition(tableAliases, tableFromQuery, condition, i)
451464
smt.addNode(constraint)
@@ -591,7 +604,11 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
591604

592605
companion object {
593606

594-
// Maps database column types to SMT-LIB types
607+
// Maps database column types to SMT-LIB types.
608+
// FUTURE WORK: this is one of three independent type vocabularies interpreting ColumnDto.type
609+
// (the others are SMTLibZ3DbConstraintSolver.getColumnDataType and .hasColumnType). They can
610+
// silently disagree when a backend reports a variant spelling; consolidating them into a single
611+
// source of truth is future work (see the note on SMTLibZ3DbConstraintSolver.hasColumnType).
595612
private val TYPE_MAP = mapOf(
596613
"BIGINT" to "Int",
597614
"BIT" to "Int",
@@ -602,6 +619,9 @@ class SmtLibGenerator(private val schema: DbInfoDto, private val numberOfRows: I
602619
"INT8" to "Int",
603620
"TINYINT" to "Int",
604621
"SMALLINT" to "Int",
622+
// KNOWN LIMITATION: NUMERIC is mapped to Int, so any fractional part is truncated. This is
623+
// inconsistent with DECIMAL (mapped to Real). Mapping NUMERIC to Real (to preserve decimals)
624+
// is future work.
605625
"NUMERIC" to "Int",
606626
"SERIAL" to "Int",
607627
"SMALLSERIAL" to "Int",

docs/options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ There are 3 types of options:
347347
|`seedTestCasesPath`| __String__. File path where the seeded test cases are located. *Default value*: `postman.postman_collection.json`.|
348348
|`skipAIModelUpdateWhenResponseIs5xx`| __Boolean__. Determines whether the AI response classifier skips model updates when the response indicates a server-side error with status code 5xx. *Default value*: `false`.|
349349
|`skipAIModelUpdateWhenResponseIsNot2xxOr400`| __Boolean__. Determines whether the AI response classifier skips model updates when the response is not 2xx or 400. *Default value*: `false`.|
350+
|`sqlZ3NumberOfRows`| __Int__. Number of rows the Z3 solver generates per table when solving a failed SQL query. The default of 1 is sufficient for the currently supported queries; generating a single row per table already forces the query to return a non-empty result. This will need to be increased once support for more complex JOINs (matching arbitrary row combinations, not just the diagonal pairing of row i with row i) is added. Only meaningful when generateSqlDataWithZ3=true. *Constraints*: `min=1.0`. *Depends on*: `generateSqlDataWithZ3=true`. *Default value*: `1`.|
350351
|`sqlZ3TimeoutMs`| __Int__. Soft timeout, in milliseconds, for each Z3 solver invocation when generating SQL data. If a query exceeds it, Z3 returns 'unknown' for that query instead of running unbounded. A value of 0 disables the timeout. Only meaningful when generateSqlDataWithZ3=true. *Constraints*: `min=0.0`. *Depends on*: `generateSqlDataWithZ3=true`. *Default value*: `5000`.|
351352
|`statusOracles`| __Boolean__. Lightweight checks on HTTP status codes, e.g., a GET should not return a 201 Created. *Default value*: `false`.|
352353
|`structureMutationProFS`| __Double__. Specify a probability of applying structure mutator during the focused search. *Constraints*: `probability 0.0-1.0`. *Default value*: `0.0`.|

0 commit comments

Comments
 (0)