From 39127b55f36113920f44837c4773b10b011ec448 Mon Sep 17 00:00:00 2001 From: Agustina Aldasoro Date: Thu, 8 Oct 2026 19:38:10 +0200 Subject: [PATCH] Map BLOB column types and write their values in hex on H2 --- .../database/sql/solver/SmtLibGenerator.kt | 9 +- .../service/SMTLibZ3DbConstraintSolver.kt | 19 +++- .../solver/service/BlobColumnSolvingTest.kt | 91 +++++++++++++++++++ .../service/UntranslatableQueryCacheTest.kt | 4 +- 4 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 core/src/test/kotlin/org/evomaster/core/database/sql/solver/service/BlobColumnSolvingTest.kt diff --git a/core/src/main/kotlin/org/evomaster/core/database/sql/solver/SmtLibGenerator.kt b/core/src/main/kotlin/org/evomaster/core/database/sql/solver/SmtLibGenerator.kt index 6aadfd7dbf..bd358fba0c 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/sql/solver/SmtLibGenerator.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/sql/solver/SmtLibGenerator.kt @@ -89,6 +89,13 @@ class SmtLibGenerator( */ val BOOLEAN_TYPES = setOf(BOOLEAN_TYPE, "BOOL") + /** + * Spellings of a binary large object: H2 2.x reports BINARY LARGE OBJECT, H2 1.4 BLOB, and + * MySQL BLOB and its size variants. Encoded as SMT Strings; SMTLibZ3DbConstraintSolver writes + * the value in a form the database accepts. + */ + val BLOB_TYPES = setOf("BLOB", "BINARY LARGE OBJECT", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB") + /** * The canonical string values a BOOLEAN column may take (BOOLEAN is encoded as an SMT String). * These are generation constraints, so Z3 is forced to pick one of them; only the two canonical @@ -153,7 +160,7 @@ class SmtLibGenerator( "UUID" to SMT_STRING, "JSONB" to SMT_STRING, "BYTEA" to SMT_STRING, - ) + ) + BLOB_TYPES.associateWith { SMT_STRING } } /** diff --git a/core/src/main/kotlin/org/evomaster/core/database/sql/solver/service/SMTLibZ3DbConstraintSolver.kt b/core/src/main/kotlin/org/evomaster/core/database/sql/solver/service/SMTLibZ3DbConstraintSolver.kt index da2fe34947..36e61beef4 100644 --- a/core/src/main/kotlin/org/evomaster/core/database/sql/solver/service/SMTLibZ3DbConstraintSolver.kt +++ b/core/src/main/kotlin/org/evomaster/core/database/sql/solver/service/SMTLibZ3DbConstraintSolver.kt @@ -487,7 +487,7 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver { if (hasColumnType(schemaDto, table, dbColumnName, SmtLibGenerator.BOOLEAN_TYPE)) { BooleanGene(dbColumnName, toBoolean(columnValue.value)) } else { - StringGene(dbColumnName, columnValue.value) + StringGene(dbColumnName, binaryValue(schemaDto, table, dbColumnName, columnValue.value)) } } is LongValue -> { @@ -590,6 +590,23 @@ class SMTLibZ3DbConstraintSolver() : DbConstraintSolver { return typeMatches(col.type, expectedType) } + /** + * Writes the value Z3 assigned to a BLOB column in a form H2 accepts. + * + * H2 1.4 reads a string literal given for a binary column as hexadecimal, so a value such as "abc" + * made it reject the INSERT of the whole row. The value is written as the hex of its UTF-8 bytes, + * which H2 2.x accepts too. The mapping is injective, so the equalities and distinctions the + * formula imposes (unique and primary keys) still hold. MySQL takes any string as is. + */ + private fun binaryValue(schemaDto: DbInfoDto, table: Table, columnName: String, value: String): String { + if (schemaDto.databaseType != DatabaseType.H2) return value + val type = schemaDto.tables.firstOrNull { it.id.name.equals(table.id.name, ignoreCase = true) } + ?.columns?.firstOrNull { it.name.equals(columnName, ignoreCase = true) } + ?.type?.uppercase() + if (type !in SmtLibGenerator.BLOB_TYPES) return value + return value.toByteArray(StandardCharsets.UTF_8).joinToString("") { "%02x".format(it) } + } + /** * Whether a column's declared type is the expected one, accounting for spellings that * [SmtLibGenerator.TYPE_MAP] already treats as equivalent. diff --git a/core/src/test/kotlin/org/evomaster/core/database/sql/solver/service/BlobColumnSolvingTest.kt b/core/src/test/kotlin/org/evomaster/core/database/sql/solver/service/BlobColumnSolvingTest.kt new file mode 100644 index 0000000000..ed472b8878 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/database/sql/solver/service/BlobColumnSolvingTest.kt @@ -0,0 +1,91 @@ +package org.evomaster.core.database.sql.solver.service + +import org.evomaster.client.java.controller.api.dto.database.schema.DbInfoDto +import org.evomaster.client.java.sql.DbInfoExtractor +import org.evomaster.client.java.sql.SqlScriptRunner +import org.evomaster.core.database.sql.SqlActionTransformer +import org.evomaster.core.search.gene.string.StringGene +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.sql.Connection +import java.sql.DriverManager + +/** + * A BLOB column had no entry in the type map. Since every table of the schema is declared for every + * query, a single BLOB column made the generation fail for all the queries of the schema. + * + * Its value must also be one the database accepts: H2 1.4 reads a string literal given for a binary + * column as hexadecimal, and rejected the INSERT of a value such as "abc". + */ +class BlobColumnSolvingTest { + + companion object { + private lateinit var solver: SMTLibZ3DbConstraintSolver + private lateinit var connection: Connection + private lateinit var schemaDto: DbInfoDto + + @JvmStatic + @BeforeAll + fun setup() { + connection = DriverManager.getConnection("jdbc:h2:mem:blob_column_test", "sa", "") + SqlScriptRunner.execCommand( + connection, + "CREATE TABLE file(id bigint primary key, name varchar(20), data blob);\n" + ) + schemaDto = DbInfoExtractor.extract(connection) + solver = SMTLibZ3DbConstraintSolver() + solver.initializeExecutor() + } + + @JvmStatic + @AfterAll + fun tearDown() { + connection.close() + if (this::solver.isInitialized) { + solver.close() + } + } + + private fun dataColumn() = + schemaDto.tables.single().columns.single { it.name.equals("DATA", ignoreCase = true) } + } + + @BeforeEach + fun clean() { + SqlScriptRunner.execCommand(connection, "DELETE FROM file;\n") + } + + @Test + fun `a query on a table with a BLOB column is solved`() { + assertEquals("BINARY LARGE OBJECT", dataColumn().type.uppercase()) // as H2 2.x reports it + val query = "SELECT * FROM file WHERE name = 'report'" + + val actions = solver.solve(schemaDto, query, 1) + assertEquals(1, actions.size) + + val results = SqlScriptRunner.execInsert(connection, SqlActionTransformer.transform(actions).insertions) + assertEquals(listOf(true), results.executionResults) + assertEquals(1, SqlScriptRunner.execCommand(connection, query).seeRows().size) + } + + @Test + fun `the value of a BLOB column is written in hexadecimal`() { + val reported = dataColumn().type + dataColumn().type = "BLOB" // as H2 1.4 reports it + try { + val actions = solver.solve(schemaDto, "SELECT * FROM file WHERE data = 'abc'", 1) + assertEquals(1, actions.size) + + val data = actions.single().seeTopGenes().first { it.name.equals("DATA", ignoreCase = true) } + assertEquals("616263", (data as StringGene).value) + + val results = SqlScriptRunner.execInsert(connection, SqlActionTransformer.transform(actions).insertions) + assertEquals(listOf(true), results.executionResults) + } finally { + dataColumn().type = reported + } + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/database/sql/solver/service/UntranslatableQueryCacheTest.kt b/core/src/test/kotlin/org/evomaster/core/database/sql/solver/service/UntranslatableQueryCacheTest.kt index 4607b20340..14bd6f7c50 100644 --- a/core/src/test/kotlin/org/evomaster/core/database/sql/solver/service/UntranslatableQueryCacheTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/database/sql/solver/service/UntranslatableQueryCacheTest.kt @@ -55,7 +55,7 @@ class UntranslatableQueryCacheTest { private const val CACHE_BOUND = 128 /** Valid SQL, run against [schemaWithUnsupportedType] so it fails while generating SMT-LIB. */ - private const val UNSUPPORTED_TYPE = "SELECT * FROM blobs WHERE id = 1" + private const val UNSUPPORTED_TYPE = "SELECT * FROM periods WHERE id = 1" @JvmStatic @BeforeAll @@ -71,7 +71,7 @@ class UntranslatableQueryCacheTest { DriverManager.getConnection("jdbc:h2:mem:untranslatable_unsupported", "sa", "") SqlScriptRunner.execCommand( unsupportedConnection, - "CREATE TABLE blobs(id bigint primary key, payload blob);\n" + "CREATE TABLE periods(id bigint primary key, length interval year);\n" ) schemaWithUnsupportedType = DbInfoExtractor.extract(unsupportedConnection) // No injector here, so the caches have to be allocated explicitly. The Z3 executor is