Skip to content

Z3-based SQL data generation: memoization, explicit outcomes, statistics & fixes#1586

Open
agusaldasoro wants to merge 20 commits into
masterfrom
dse/memoization
Open

Z3-based SQL data generation: memoization, explicit outcomes, statistics & fixes#1586
agusaldasoro wants to merge 20 commits into
masterfrom
dse/memoization

Conversation

@agusaldasoro

@agusaldasoro agusaldasoro commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds memoization and explicit outcome handling to EvoMaster's Z3-based SQL data generation (generateSqlDataWithZ3), plus optional statistics/correctness instrumentation and several encoding fixes.

  • Memoization: caches solver results by (sqlQuery, numberOfRows) in a bounded LRU, skipping redundant Docker/Z3 executions for repeated queries. Only deterministic outcomes (SAT/UNSAT) are cached; UNKNOWN/ERROR are not (they may be transient). The cache stores the Z3Result (not the generated SqlActions), so each individual still gets fresh, independently-mutable genes.
  • Explicit solver outcomes: introduces Z3Result (SAT / UNSAT / UNKNOWN / ERROR) and Z3Solution, replacing Optional<Map<String, SMTLibValue>>. Z3DockerExecutor classifies Z3's stdout before parsing, so unknown/unsat are no longer misparsed as an empty SAT model.
  • Configurable Z3 timeout (sqlZ3TimeoutMs, default 5000ms): passes a soft -t: timeout to Z3; on expiry Z3 returns unknown (mapped to UNKNOWN) instead of running unbounded.
  • Configurable rows (sqlZ3NumberOfRows, default 1): number of rows generated per table.
  • Optional statistics (collectSqlZ3Stats): per-run counters written to statistics.csv only when enabled — query volume & cache reuse, solver outcomes, and cost (Z3 time, SMT-LIB gen time/size).
  • Optional correctness measurement (measureSqlZ3Correctness): after a SAT result, generated rows are re-evaluated with SqlHeuristicsCalculator to measure their distance to the original SQL predicate; mismatches are logged and recorded.
  • Encoding fixes:
    • Composite primary keys now enforce tuple uniqueness (OR of per-column distinctness) instead of per-column distinctness, which over-constrained the encoding.
    • SmtLibGenerator supports DELETE and UPDATE, not just SELECT.
    • Integer columns map to LongGene instead of truncating via .toInt().
    • Improved SQL parsing in JSqlVisitor (LOWER/UPPER, timestamp literals) and added SmtLibGeneratorTest coverage.
  • Memory fix: the solver holds Statistics via a WeakReference, so its @PreDestroy registration (a GC root through Governator's predestroy-monitor thread) no longer pins Statistics → Archive → individuals, which was OOMing test suites that build many injectors.

All new options are @Experimental and default-off (or default 1 / 5000), so default runs are unaffected.

Configuration

Flag Default Purpose
generateSqlDataWithZ3 false Enable Z3-based SQL data generation
sqlZ3NumberOfRows 1 Rows generated per table
sqlZ3TimeoutMs 5000 Soft per-query Z3 timeout in ms (0 = off)
collectSqlZ3Stats false Emit solver statistics to CSV
measureSqlZ3Correctness false Post-verify generated rows against the SQL predicate

Statistics (collectSqlZ3Stats)

  • Query volume / cache: sqlZ3QueriesSeen, sqlZ3UniqueQueries, sqlZ3DuplicateQueries, sqlZ3CacheHits, sqlZ3CacheMisses — invariant: sqlZ3QueriesSeen = sqlZ3CacheHits + sqlZ3CacheMisses.
  • Outcomes: sqlZ3Sat, sqlZ3Unsat, sqlZ3Unknown, sqlZ3Errors, sqlZ3ParseFailures — sum equals sqlZ3CacheMisses.
  • Cost: sqlZ3TotalMs, sqlZ3SmtlibGenTotalMs, sqlZ3AvgSmtlibSizeBytes.

Correctness (measureSqlZ3Correctness)

sqlZ3CorrectnessChecks, sqlZ3CorrectnessZeroDistance, sqlZ3CorrectnessNonZero, sqlZ3CorrectnessAvgDist, sqlZ3CorrectnessEvalFailures.

Known limitations (documented in code as KNOWN LIMITATION / FUTURE WORK)

  • JOIN ON uses diagonal row pairing (row i ↔ row i) and only the first ON conjunct — sufficient at k=1, not full INNER JOIN semantics.
  • Composite foreign keys are matched per-column, not as a tuple.
  • NULL conditions (IS NULL / IS NOT NULL / = NULL) are dropped; all columns are treated as NOT NULL.
  • NUMERIC maps to Int (truncating decimals), inconsistent with DECIMAL → Real.
  • Three type-name vocabularies interpret ColumnDto.type independently and could drift on variant spellings.

@agusaldasoro agusaldasoro changed the title Dse/memoization Add DSE Metrics + Memoization Jun 16, 2026
@agusaldasoro agusaldasoro requested a review from jgaleotti June 16, 2026 01:45
@agusaldasoro agusaldasoro changed the title Add DSE Metrics + Memoization DSE Memoization + Statistics Jun 18, 2026
@agusaldasoro agusaldasoro marked this pull request as ready for review June 18, 2026 11:52
@jgaleotti

Copy link
Copy Markdown
Collaborator

Is this ready for review? It seems to be failing...

@agusaldasoro

agusaldasoro commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Is this ready for review? It seems to be failing...

@jgaleotti The changes are ready and working, but there seems to be an out of memory issue that I can not solve in the Core and CoreIT tests

@agusaldasoro agusaldasoro marked this pull request as draft June 25, 2026 00:37
@agusaldasoro agusaldasoro removed the request for review from jgaleotti June 25, 2026 00:37
@agusaldasoro agusaldasoro changed the title DSE Memoization + Statistics DSE Statistics & Fixes Jun 27, 2026
@agusaldasoro agusaldasoro marked this pull request as ready for review June 27, 2026 14:18
@agusaldasoro agusaldasoro requested a review from jgaleotti June 29, 2026 00:55
@agusaldasoro

Copy link
Copy Markdown
Collaborator Author

@jgaleotti Hey! It's ready for review now, thanks!

public void visit(Function function) {
// TODO This translation should be implemented
String name = function.getName().toUpperCase();
if ((name.equals("LOWER") || name.equals("UPPER"))

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.

replace "LOWER" and "UPPER" with constants

/**
* Represents the outcome of a Z3 solver invocation.
* Distinguishes between three possible states: SAT (satisfiable with a model),
* UNSAT (unsatisfiable), and ERROR (solver or parsing failure).

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 timeout? is that an outcome?

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.

also, can Z3 return UNKNOWN?

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.

Done!

/** Non-null only when status == ERROR. */
public final String errorMessage;

private Z3Result(Status status, Map<String, SMTLibValue> model, String errorMessage) {

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.

create a abstract class Z3Solution and rename model to solution. (Using model can be a bit misleading nowadays due to LLMs...)

ERROR
}

public final Status status;

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.

why not getters and private fields?

private val sqlRowsAverageCalculator = IncrementalAverage()

// DSE (Dynamic Symbolic Execution) statistics
private var dseTotalQueriesProcessed = 0

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.

To be honest, I need we need a more suitable term sonce there is no "dse/dynamic symbolic execution" involved in this work :)
What do you think of generateSqlDataWithDSE -> generateSqlDataWithZ3 ? Regarding the "dse..." it could be prefixed to "sqlZ3..." ?

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.

Done!

}
"NAME" -> {
assertTrue(gene is StringGene)
assertEquals("agus", (gene as StringGene).value)

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.

"Alice" and "Bob" please :)

@agusaldasoro agusaldasoro requested a review from jgaleotti July 8, 2026 19:17
if (config.generateSqlDataWithDSE) {
return handleDSE(ind, sampler, failedWhereQueries)
if (config.generateSqlDataWithZ3) {
return handleZ3(ind, sampler, failedWhereQueries)

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.

generateSqlDataWithZ3(...)

@@ -347,8 +347,8 @@ abstract class ApiWsStructureMutator : StructureMutator() {
return handleSearch(ind, sampler, mutatedGenes, fw)

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.

generateSqlDataWithSearch(...)

@@ -31,7 +31,7 @@ public void testRunEM() throws Throwable {
args.add("true");

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.

replace args.add with setOption(args,"generateSqlDataWithSearch","false")

// TODO This translation should be implemented
if (dateTimeLiteralExpression.getType() == DateTimeLiteralExpression.DateTime.TIMESTAMP) {
String value = dateTimeLiteralExpression.getValue();
if (value.startsWith("'") && value.endsWith("'")) {

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.

replace "'" with constant

// Treat the timestamp string as UTC to match the UTC-based decoder in
// SMTLibZ3DbConstraintSolver (LocalDateTime.ofInstant(..., UTC)).
long epochSeconds = java.time.LocalDateTime.parse(value,
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))

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.

replace "yyyy-MM-dd HH:mm:ss" with constant

String stderr = result.getStderr();
throw new RuntimeException("No result after solving file " + stderr);
String trimmed = stdout.trim();
if (trimmed.startsWith("unsat")) {

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.

replace "unsat" with constant

// "unknown" means Z3 could not decide (e.g. incomplete theory or timeout).
// It must be handled explicitly: otherwise it would fall through and be parsed
// as an empty model, silently masquerading as SAT.
if (trimmed.startsWith("unknown")) {

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.

replace "unknown" with constant

if (trimmed.startsWith("unknown")) {
return Z3Result.unknown();
}
if (!trimmed.startsWith("sat")) {

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.

replace "sat" with constant

return Z3Result.error("Unexpected Z3 output for file " + fileName + ": " + stdout);
}

Map<String, SMTLibValue> assignments = SMTResultParser.parseZ3Response(stdout);

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.

The parser should create a solution instead of a assignment map

/**
* @return the assignments of this solution, keyed by variable/constant name.
*/
public abstract Map<String, SMTLibValue> getAssignments();

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.

why is this abstract?

@agusaldasoro agusaldasoro changed the title DSE Statistics & Fixes Z3-based SQL data generation: memoization, explicit outcomes, statistics & fixes Jul 9, 2026
@agusaldasoro agusaldasoro requested a review from jgaleotti July 9, 2026 17:53
// Defensive: Z3DockerExecutor already classifies 'unsat'/'unknown' before invoking this
// parser, so in practice only 'sat' models reach here. This guard is kept as a safety net
// in case the parser is ever called directly with an unsat response.
if (line.startsWith("unsat")) {

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.

replace "unsat" with constant value

smt.addNode(constraint)
}
} catch (e: RuntimeException) {
LoggingUtil.getInfoLogger().warn("Could not translate WHERE clause to SMT-LIB, skipping: ${where}. Reason: ${e.message}")

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 this failure considered in the sqlZ3 stats?

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.

Good catch! It wasn't (the catch is inside generateSMT, so the predicate is silently dropped and Z3 still returns SAT).

Now fixed: it increments skippedQueryConstraints (here + the JOIN ON catch) and is reported as a new sqlZ3PartialTranslations stat column.

* from the average, so they do not distort the correctness metric.
*/
val distResult = computeCorrectnessDistance(sqlQuery, schemaDto, sqlActions)
if (distResult.sqlDistanceEvaluationFailure) {

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 this considered in the stats?

val distResult = computeCorrectnessDistance(sqlQuery, schemaDto, sqlActions)
if (distResult.sqlDistanceEvaluationFailure) {
LoggingUtil.getInfoLogger().warn("SQL-Z3: correctness evaluation failure for query '$sqlQuery'")
} else if (distResult.sqlDistance != 0.0) {

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 this case considered in the stats?

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.

Yes. Both branches are recorded by the reportSqlZ3CorrectnessDistance(distResult.sqlDistance, distResult.sqlDistanceEvaluationFailure) call right below this block.

@agusaldasoro agusaldasoro requested a review from jgaleotti July 9, 2026 22:56
@jgaleotti jgaleotti requested a review from arcuri82 July 9, 2026 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants