@@ -5,20 +5,13 @@ import com.google.inject.Inject
55import net.sf.jsqlparser.JSQLParserException
66import net.sf.jsqlparser.parser.CCJSqlParserUtil
77import net.sf.jsqlparser.statement.Statement
8- import net.sf.jsqlparser.statement.insert.Insert
98import org.apache.commons.io.FileUtils
109import org.evomaster.client.java.controller.api.dto.database.schema.ColumnDto
1110import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType
1211import org.evomaster.client.java.controller.api.dto.database.schema.DbInfoDto
1312import org.evomaster.client.java.controller.api.dto.database.schema.TableDto
1413import org.evomaster.core.EMConfig
1514import org.evomaster.core.logging.LoggingUtil
16- import org.evomaster.client.java.sql.DataRow
17- import org.evomaster.client.java.sql.QueryResult
18- import org.evomaster.client.java.sql.QueryResultSet
19- import org.evomaster.client.java.sql.heuristic.SqlHeuristicsCalculator
20- import org.evomaster.client.java.sql.heuristic.TableColumnResolver
21- import org.evomaster.client.java.sql.internal.SqlDistanceWithMetrics
2215import org.evomaster.core.search.gene.BooleanGene
2316import org.evomaster.core.search.gene.Gene
2417import org.evomaster.core.search.gene.numeric.DoubleGene
@@ -71,11 +64,12 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
7164 // Schema is assumed stable within a single run, so only query + row count form the key.
7265 // Null until Z3 SQL generation is enabled in postConstruct — avoids allocating the map in runs where Z3 SQL generation is off.
7366 //
74- // THREAD-SAFETY: this is an access-ordered LinkedHashMap (LRU), whose get() mutates internal order,
75- // so it is NOT thread-safe. It relies on solve() being called from a single thread, which holds today
76- // because the MIO search loop (and thus fitness evaluation / structure mutation) is single-threaded.
77- // If fitness evaluation is ever parallelized, this cache must be synchronized or replaced with a
78- // concurrent LRU, otherwise concurrent access would corrupt it (lost entries / ConcurrentModificationException).
67+ // THREAD-SAFETY: the backing map is an access-ordered LinkedHashMap (a bounded LRU, whose get() mutates
68+ // internal order), wrapped in Collections.synchronizedMap so every operation is guarded by the map's
69+ // intrinsic lock. This is the standard idiom for a thread-safe bounded LRU (a plain ConcurrentHashMap
70+ // cannot do access-order eviction). Compound get-then-put in solve() is intentionally not atomic: at
71+ // worst two threads recompute the same query concurrently, which is harmless (idempotent), and never
72+ // corrupts the map. This makes the cache safe for the parallelized fitness evaluation that is planned.
7973 private var z3ResultCache: MutableMap <Pair <String , Int >, Z3Result > ? = null
8074
8175 companion object {
@@ -111,10 +105,11 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
111105 private fun postConstruct () {
112106 if (config.generateSqlDataWithZ3) {
113107 initializeExecutor()
114- z3ResultCache = object : LinkedHashMap <Pair <String , Int >, Z3Result > (16 , 0.75f , true ) {
108+ val lru = object : LinkedHashMap <Pair <String , Int >, Z3Result > (16 , 0.75f , true ) {
115109 override fun removeEldestEntry (eldest : MutableMap .MutableEntry <Pair <String , Int >, Z3Result >? ) =
116110 size > MAX_CACHE_SIZE
117111 }
112+ z3ResultCache = Collections .synchronizedMap(lru)
118113 }
119114 }
120115
@@ -210,33 +205,7 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
210205 Z3Result .Status .SAT -> {
211206 stats?.reportSqlZ3Sat(z3TimeMs)
212207 z3ResultCache?.set(cacheKey, z3Result)
213- val sqlActions = toSqlActionList(schemaDto, z3Result.solution)
214- if (::config.isInitialized && config.measureSqlZ3Correctness && queryStatement !is Insert ) {
215- /*
216- * INSERT statements have no WHERE clause, so SqlHeuristicsCalculator has no
217- * predicate to evaluate distance against and will always report a failure.
218- * Correctness measurement only makes sense for queries that filter rows
219- * (SELECT, DELETE, UPDATE). In the future this could be extended to verify
220- * that the generated rows satisfy insertion preconditions such as FK constraints
221- * or NOT NULL columns that Z3 SQL generation currently leaves unconstrained.
222- *
223- * Note: SqlHeuristicsCalculator is SELECT-oriented. For DELETE/UPDATE the distance
224- * computation may fail and be reported as an evaluation failure (sqlDistanceEvaluationFailure)
225- * rather than a real distance; such failures are counted separately and are excluded
226- * from the average, so they do not distort the correctness metric.
227- */
228- val distResult = computeCorrectnessDistance(sqlQuery, schemaDto, sqlActions)
229- if (distResult.sqlDistanceEvaluationFailure) {
230- LoggingUtil .getInfoLogger().warn(" SQL-Z3: correctness evaluation failure for query '$sqlQuery '" )
231- } else if (distResult.sqlDistance != 0.0 ) {
232- LoggingUtil .getInfoLogger().warn(" SQL-Z3: non-zero correctness distance (${distResult.sqlDistance} ) for query '$sqlQuery '" )
233- }
234- statisticsRef?.get()?.reportSqlZ3CorrectnessDistance(
235- distResult.sqlDistance,
236- distResult.sqlDistanceEvaluationFailure
237- )
238- }
239- sqlActions
208+ toSqlActionList(schemaDto, z3Result.solution)
240209 }
241210 Z3Result .Status .UNSAT -> {
242211 stats?.reportSqlZ3Unsat(z3TimeMs)
@@ -387,15 +356,15 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
387356 /* *
388357 * Extracts the table name from a row-constant key by removing the trailing row index.
389358 *
390- * Row constants are named "${smtName}${i}" (e.g. "users1 ", "users2"). The trailing digits are
391- * stripped so this works for any number of rows (e.g. "users10" -> "users"), not just single-digit
392- * indices. Note: this still assumes table names themselves do not end in a digit .
359+ * Row constants are named "${smtName}${SEP}${ i}" (e.g. "users__1 ", "users__2"; see
360+ * [SmtLibGenerator.rowConstantName]). Splitting on the last separator recovers the table name
361+ * unambiguously even when the table name itself ends in digits (e.g. "inventory2026__1" -> "inventory2026") .
393362 *
394363 * @param key The key containing the table name and index.
395364 * @return The extracted table name.
396365 */
397366 private fun getTableName (key : String ): String {
398- return key.replace( Regex ( " \\ d+$ " ), " " )
367+ return key.substringBeforeLast( SmtLibGenerator . ROW_INDEX_SEPARATOR )
399368 }
400369
401370 /* *
@@ -488,7 +457,7 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
488457 /* *
489458 * Maps column types to ColumnDataType.
490459 *
491- * FUTURE WORK : this recognizes only a small subset of SQL type spellings and falls back to
460+ * TODO : this recognizes only a small subset of SQL type spellings and falls back to
492461 * CHARACTER_VARYING for everything else. It is one of three independent type vocabularies that
493462 * interpret [ColumnDto.type] — the others being [SmtLibGenerator.TYPE_MAP] (SQL type -> SMT sort)
494463 * and [hasColumnType] (BOOLEAN/TIMESTAMP special-casing). These can silently disagree when a
@@ -550,60 +519,4 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver {
550519 }
551520
552521 private fun leadingBarResourcesFolder () = if (resourcesFolder.endsWith(" /" )) resourcesFolder else " $resourcesFolder /"
553-
554- private fun computeCorrectnessDistance (
555- sqlQuery : String ,
556- schemaDto : DbInfoDto ,
557- sqlActions : List <SqlAction >
558- ): SqlDistanceWithMetrics {
559- val queryResultSet = toQueryResultSet(schemaDto, sqlActions)
560- val calculator = SqlHeuristicsCalculator .SqlHeuristicsCalculatorBuilder ()
561- .withTableColumnResolver(TableColumnResolver (schemaDto))
562- .withSourceQueryResultSet(queryResultSet)
563- .build()
564- return calculator.computeDistance(sqlQuery)
565- }
566-
567- private fun toQueryResultSet (schemaDto : DbInfoDto , sqlActions : List <SqlAction >): QueryResultSet {
568- val queryResultSet = QueryResultSet ()
569- val byTable = sqlActions.groupBy { it.table.id.name }
570- for ((tableName, actions) in byTable) {
571- val columnNames = actions.first().seeTopGenes().map { it.name }
572- val queryResult = QueryResult (columnNames, tableName)
573- for (action in actions) {
574- val values: List <Any ?> = action.seeTopGenes().map { gene -> extractGeneValue(gene) }
575- queryResult.addRow(DataRow (tableName, columnNames, values))
576- }
577- queryResultSet.addQueryResult(queryResult)
578- }
579- // Tables not present in Z3's SAT model (e.g. the optional side of a LEFT OUTER JOIN)
580- // still need an (empty) QueryResult, otherwise SqlHeuristicsCalculator NPEs when it
581- // looks them up unconditionally while walking the FROM/JOIN clause.
582- for (table in schemaDto.tables) {
583- if (table.id.name !in byTable.keys) {
584- val columnNames = table.columns.map { it.name }
585- queryResultSet.addQueryResult(QueryResult (columnNames, table.id.name))
586- }
587- }
588- return queryResultSet
589- }
590-
591- private fun extractGeneValue (gene : Gene ): Any? {
592- val inner = if (gene is SqlPrimaryKeyGene ) gene.gene else gene
593- return when (inner) {
594- is IntegerGene -> inner.value
595- is LongGene -> inner.value
596- is StringGene -> inner.value
597- is DoubleGene -> inner.value
598- is BooleanGene -> inner.value
599- is ImmutableDataHolderGene -> inner.value
600- else -> {
601- LoggingUtil .getInfoLogger().warn(
602- " SQL-Z3: extractGeneValue() fallback to raw string for unhandled gene type " +
603- " ${inner.javaClass.name} (outer: ${gene.javaClass.name} , name: ${gene.name} )"
604- )
605- inner.getValueAsRawString()
606- }
607- }
608- }
609522}
0 commit comments