diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/h2/H2SchemaExtractorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/h2/H2SchemaExtractorTest.java index 831c325732..2c041a8e5e 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/h2/H2SchemaExtractorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/h2/H2SchemaExtractorTest.java @@ -146,6 +146,9 @@ public void testBasicForeignKey() throws Exception { assertEquals(1, foreignKey.sourceColumns.size()); assertTrue(foreignKey.sourceColumns.stream().anyMatch(c -> c.equalsIgnoreCase("barId"))); assertTrue(foreignKey.targetTable.equalsIgnoreCase("Bar")); + + assertEquals(1, foreignKey.targetColumns.size()); + assertTrue(foreignKey.targetColumns.stream().anyMatch(c -> c.equalsIgnoreCase("id"))); } @Test diff --git a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/mysql/MySQLSchemaExtractorTest.java b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/mysql/MySQLSchemaExtractorTest.java index a0f05e9253..ce74c4dfb0 100644 --- a/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/mysql/MySQLSchemaExtractorTest.java +++ b/client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/mysql/MySQLSchemaExtractorTest.java @@ -1,6 +1,7 @@ package org.evomaster.client.java.controller.internal.db.sql.mysql; import org.evomaster.client.java.controller.DatabaseTestTemplate; +import org.evomaster.client.java.controller.api.dto.database.schema.ForeignKeyDto; import org.evomaster.client.java.controller.api.dto.database.schema.DbInfoDto; import org.evomaster.client.java.controller.api.dto.database.schema.TableDto; import org.evomaster.client.java.sql.SqlScriptRunner; diff --git a/client-java/sql-dto/src/main/java/org/evomaster/client/java/controller/api/dto/database/schema/ForeignKeyDto.java b/client-java/sql-dto/src/main/java/org/evomaster/client/java/controller/api/dto/database/schema/ForeignKeyDto.java index 6252678589..9d493ac543 100644 --- a/client-java/sql-dto/src/main/java/org/evomaster/client/java/controller/api/dto/database/schema/ForeignKeyDto.java +++ b/client-java/sql-dto/src/main/java/org/evomaster/client/java/controller/api/dto/database/schema/ForeignKeyDto.java @@ -3,11 +3,52 @@ import java.util.ArrayList; import java.util.List; +/** + * Represents a foreign key relationship in a database schema. + * + * A foreign key establishes a connection between two database tables, + * where one table (source) references the primary key or a unique column + * in another table (target). + * + * This class captures the metadata for the foreign key, including the columns + * involved in the relationship and the target table being referenced. + */ public class ForeignKeyDto { + /** + * A list of column names in the source table of the foreign key relationship. + * + * These column names correspond to the columns in the source table + * that are used in the foreign key constraint. They establish the + * connection to the target table by referencing its primary key + * or unique columns. + * + * The order of the columns in this list corresponds to the order + * in which they are defined in the foreign key constraint. + */ public List sourceColumns = new ArrayList<>(); + /** + * The name of the target table in a foreign key relationship. + * + * This variable specifies the table being referenced by the foreign key. + * The value corresponds to the physical name of the target table in the database. + * The foreign key relationship indicates that a column or set of columns in + * the source table references a column or set of columns (usually the primary key) + * in the target table. + */ public String targetTable; - //TODO likely ll need to handle targetColumns if we have multi-columns + /** + * A list of column names in the target table of the foreign key relationship. + * + * These column names represent the columns in the target table + * that are referenced by the foreign key constraint. They typically + * refer to primary key columns or unique columns in the target table. + * + * The order of the columns in this list corresponds to the order + * defined in the foreign key relationship, ensuring a one-to-one + * mapping with the source columns. + */ + public List targetColumns = new ArrayList<>(); } diff --git a/client-java/sql/src/main/java/org/evomaster/client/java/sql/DbCleaner.java b/client-java/sql/src/main/java/org/evomaster/client/java/sql/DbCleaner.java index 3f5435ff74..f423416a84 100644 --- a/client-java/sql/src/main/java/org/evomaster/client/java/sql/DbCleaner.java +++ b/client-java/sql/src/main/java/org/evomaster/client/java/sql/DbCleaner.java @@ -282,7 +282,7 @@ private static List cleanDataInTables(List tableToSkip, if (doDropTable) { dropTableIfExists(statement, ts); } else { - truncateTable(statement, ts, restartIdentityWhenTruncating); + truncateTable(statement, ts, restartIdentityWhenTruncating, type); } } else { //note: if one at a time, need to make sure to first disable FK checks @@ -300,7 +300,7 @@ private static List cleanDataInTables(List tableToSkip, if (type == DatabaseType.MS_SQL_SERVER) deleteTables(statement, t, schema, tablesHaveIdentifies); else - truncateTable(statement, t, restartIdentityWhenTruncating); + truncateTable(statement, t, restartIdentityWhenTruncating, type); } } } @@ -327,12 +327,15 @@ private static void deleteTables(Statement statement, String table, String schem statement.executeUpdate("DBCC CHECKIDENT ('" + tableWithSchema + "', RESEED, 0)"); } - private static void truncateTable(Statement statement, String table, boolean restartIdentityWhenTruncating) throws SQLException { + private static void truncateTable(Statement statement, String table, boolean restartIdentityWhenTruncating, DatabaseType type) throws SQLException { + String sql = "TRUNCATE TABLE " + table; if (restartIdentityWhenTruncating) { - statement.executeUpdate("TRUNCATE TABLE " + table + " RESTART IDENTITY"); - } else { - statement.executeUpdate("TRUNCATE TABLE " + table); + sql += " RESTART IDENTITY"; + } + if (type == DatabaseType.POSTGRES) { + sql += " CASCADE"; } + statement.executeUpdate(sql); } private static void resetSequences(Statement s, DatabaseType type, String schemaName, List sequenceToClean) throws SQLException { diff --git a/client-java/sql/src/main/java/org/evomaster/client/java/sql/DbInfoExtractor.java b/client-java/sql/src/main/java/org/evomaster/client/java/sql/DbInfoExtractor.java index b6d8208c66..926cd8d004 100644 --- a/client-java/sql/src/main/java/org/evomaster/client/java/sql/DbInfoExtractor.java +++ b/client-java/sql/src/main/java/org/evomaster/client/java/sql/DbInfoExtractor.java @@ -654,14 +654,22 @@ private static void handleTableEntry(Connection connection, DbInfoDto schemaDto, ResultSet fks = md.getImportedKeys(tableDto.id.catalog, tableDto.id.schema, tableDto.id.name); + Map foreignKeysByName = new HashMap<>(); while (fks.next()) { - //TODO need to see how to handle case of multi-columns - - ForeignKeyDto fkDto = new ForeignKeyDto(); - fkDto.sourceColumns.add(fks.getString("FKCOLUMN_NAME")); - fkDto.targetTable = fks.getString("PKTABLE_NAME"); - - tableDto.foreignKeys.add(fkDto); + String fkName = fks.getString("FK_NAME"); + String sourceColumn = fks.getString("FKCOLUMN_NAME"); + String targetTable = fks.getString("PKTABLE_NAME"); + String targetColumn = fks.getString("PKCOLUMN_NAME"); + + ForeignKeyDto fkDto = foreignKeysByName.get(fkName); + if (fkDto == null) { + fkDto = new ForeignKeyDto(); + fkDto.targetTable = targetTable; + foreignKeysByName.put(fkName, fkDto); + tableDto.foreignKeys.add(fkDto); + } + fkDto.sourceColumns.add(sourceColumn); + fkDto.targetColumns.add(targetColumn); } fks.close(); } diff --git a/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorH2Test.java b/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorH2Test.java new file mode 100644 index 0000000000..b7c4b927de --- /dev/null +++ b/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorH2Test.java @@ -0,0 +1,42 @@ +package org.evomaster.client.java.sql; + +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +import java.sql.Connection; +import java.sql.DriverManager; + + +public class DbInfoExtractorH2Test extends DbInfoExtractorTestBase { + + private static Connection connection; + + @BeforeAll + public static void initClass() throws Exception { + connection = DriverManager.getConnection("jdbc:h2:mem:db_test", "sa", ""); + } + + @AfterAll + public static void afterClass() throws Exception { + connection.close(); + } + + @BeforeEach + public void initTest() throws Exception { + //custom H2 command + SqlScriptRunner.execCommand(connection, "DROP ALL OBJECTS;"); + } + + + @Override + protected DatabaseType getDbType() { + return DatabaseType.H2; + } + + @Override + protected Connection getConnection() { + return connection; + } +} diff --git a/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorMySQLTest.java b/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorMySQLTest.java new file mode 100644 index 0000000000..5859d6d092 --- /dev/null +++ b/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorMySQLTest.java @@ -0,0 +1,75 @@ +package org.evomaster.client.java.sql; + +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DbInfoExtractorMySQLTest extends DbInfoExtractorTestBase { + + private static final String DB_NAME = "test"; + + private static final int PORT = 3306; + + private static final String MYSQL_VERSION = "8.0.27"; + + public static final GenericContainer mysql = new GenericContainer("mysql:" + MYSQL_VERSION) + .withEnv(new HashMap(){{ + put("MYSQL_ROOT_PASSWORD", "root"); + put("MYSQL_DATABASE", DB_NAME); + put("MYSQL_USER", "test"); + put("MYSQL_PASSWORD", "test"); + }}) + .withExposedPorts(PORT); + + private static Connection connection; + + @BeforeAll + public static void initClass() throws Exception { + + mysql.start(); + + String host = mysql.getContainerIpAddress(); + int port = mysql.getMappedPort(PORT); + String url = "jdbc:mysql://"+host+":"+port+"/"+DB_NAME; + + connection = DriverManager.getConnection(url, "test", "test"); + + } + + @AfterAll + public static void afterClass() throws Exception { + connection.close(); + mysql.stop(); + } + + @AfterEach + public void afterTest() throws SQLException { + SqlScriptRunner.execCommand(connection, "DROP TABLE IF EXISTS example_table;"); + } + + + @Override + protected DatabaseType getDbType() { + return DatabaseType.MYSQL; + } + + @Override + protected Connection getConnection() { + return connection; + } + + +} diff --git a/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorPostgresTest.java b/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorPostgresTest.java new file mode 100644 index 0000000000..efd9683a1e --- /dev/null +++ b/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorPostgresTest.java @@ -0,0 +1,63 @@ +package org.evomaster.client.java.sql; + +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.testcontainers.containers.GenericContainer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.Collections; + +public class DbInfoExtractorPostgresTest extends DbInfoExtractorTestBase { + + private static final String POSTGRES_VERSION = "14"; + + private static final GenericContainer postgres = new GenericContainer("postgres:" + POSTGRES_VERSION) + .withExposedPorts(5432) + .withTmpFs(Collections.singletonMap("/var/lib/postgresql/data", "rw")) + .withEnv("POSTGRES_HOST_AUTH_METHOD","trust"); + + private static Connection connection; + + @BeforeAll + public static void initClass() throws Exception{ + postgres.start(); + String host = postgres.getHost(); + int port = postgres.getMappedPort(5432); + String url = "jdbc:postgresql://"+host+":"+port+"/postgres"; + + connection = DriverManager.getConnection(url, "postgres", ""); + } + + @AfterAll + public static void afterClass() throws Exception{ + connection.close(); + postgres.stop(); + } + + @BeforeEach + public void initTest() throws Exception { + /* + see: + https://stackoverflow.com/questions/3327312/how-can-i-drop-all-the-tables-in-a-postgresql-database + */ + SqlScriptRunner.execCommand(connection, "DROP SCHEMA public CASCADE;"); + SqlScriptRunner.execCommand(connection, "CREATE SCHEMA public;"); + SqlScriptRunner.execCommand(connection, "GRANT ALL ON SCHEMA public TO postgres;"); + SqlScriptRunner.execCommand(connection, "GRANT ALL ON SCHEMA public TO public;"); + } + + @Override + protected Connection getConnection(){ + return connection; + } + + @Override + protected DatabaseType getDbType() { + return DatabaseType.POSTGRES; + } + + +} diff --git a/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorTestBase.java b/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorTestBase.java new file mode 100644 index 0000000000..e8740a2dba --- /dev/null +++ b/client-java/sql/src/test/java/org/evomaster/client/java/sql/DbInfoExtractorTestBase.java @@ -0,0 +1,149 @@ +package org.evomaster.client.java.sql; + +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType; +import org.evomaster.client.java.controller.api.dto.database.schema.DbInfoDto; +import org.evomaster.client.java.controller.api.dto.database.schema.ForeignKeyDto; +import org.evomaster.client.java.controller.api.dto.database.schema.TableDto; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.Time; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +public abstract class DbInfoExtractorTestBase { + + protected abstract DatabaseType getDbType(); + protected abstract Connection getConnection(); + + @Test + public void testCompositeForeignKey() throws Exception { + + SqlScriptRunner.execCommand(getConnection(), "CREATE TABLE Parent(" + + "id1 bigint, " + + "id2 bigint, " + + "primary key (id1, id2)" + + ")"); + SqlScriptRunner.execCommand(getConnection(), "CREATE TABLE Child(" + + "id bigint primary key, " + + "pid1 bigint not null, " + + "pid2 bigint not null " + + ")"); + SqlScriptRunner.execCommand(getConnection(), "ALTER TABLE Child add constraint compositeKey foreign key (pid1, pid2) references Parent(id1, id2)"); + + DbInfoDto schema = DbInfoExtractor.extract(getConnection()); + TableDto parent = schema.tables.stream().filter(t -> t.id.name.equalsIgnoreCase("Parent")).findAny().get(); + TableDto child = schema.tables.stream().filter(t -> t.id.name.equalsIgnoreCase("Child")).findAny().get(); + + assertEquals(0, parent.foreignKeys.size()); + assertEquals(1, child.foreignKeys.size()); + + ForeignKeyDto foreignKey = child.foreignKeys.get(0); + + assertEquals(2, foreignKey.sourceColumns.size()); + assertTrue(foreignKey.sourceColumns.get(0).equalsIgnoreCase("pid1")); + assertTrue(foreignKey.sourceColumns.get(1).equalsIgnoreCase("pid2")); + assertTrue(foreignKey.targetTable.equalsIgnoreCase("Parent")); + + assertEquals(2, foreignKey.targetColumns.size()); + assertTrue(foreignKey.targetColumns.get(0).equalsIgnoreCase("id1")); + assertTrue(foreignKey.targetColumns.get(1).equalsIgnoreCase("id2")); + } + + @Test + public void testOneImplicitCompositeForeignKey() throws Exception { + /** + * Implicity foreign keys are not supported by MySQL, so we skip this test for MySQL databases. + */ + Assumptions.assumeTrue(this.getDbType() != DatabaseType.MYSQL); + + SqlScriptRunner.execCommand(getConnection(), "CREATE TABLE Parent(" + + "id1 bigint, " + + "id2 bigint, " + + "primary key (id1, id2)" + + ")"); + SqlScriptRunner.execCommand(getConnection(), "CREATE TABLE Child(" + + "id bigint primary key, " + + "pid1 bigint not null, " + + "pid2 bigint not null, " + + "foreign key (pid1, pid2) references Parent" + + ")"); + + DbInfoDto schema = DbInfoExtractor.extract(getConnection()); + TableDto child = schema.tables.stream().filter(t -> t.id.name.equalsIgnoreCase("Child")).findAny().get(); + + assertEquals(1, child.foreignKeys.size()); + + ForeignKeyDto foreignKey = child.foreignKeys.get(0); + + assertEquals(2, foreignKey.sourceColumns.size()); + assertTrue(foreignKey.sourceColumns.get(0).equalsIgnoreCase("pid1")); + assertTrue(foreignKey.sourceColumns.get(1).equalsIgnoreCase("pid2")); + assertTrue(foreignKey.targetTable.equalsIgnoreCase("Parent")); + + assertEquals(2, foreignKey.targetColumns.size()); + assertTrue(foreignKey.targetColumns.get(0).equalsIgnoreCase("id1")); + assertTrue(foreignKey.targetColumns.get(1).equalsIgnoreCase("id2")); + } + + @Test + public void testTwoCompositeExplicitForeignKeys() throws Exception { + + SqlScriptRunner.execCommand(getConnection(), "CREATE TABLE Parent1(" + + "id1 bigint, " + + "id2 bigint, " + + "primary key (id1, id2)" + + ")"); + SqlScriptRunner.execCommand(getConnection(), "CREATE TABLE Parent2(" + + "id1 bigint, " + + "id2 bigint, " + + "primary key (id1, id2)" + + ")"); + SqlScriptRunner.execCommand(getConnection(), "CREATE TABLE ChildTwoImplicit(" + + "id bigint primary key, " + + "p1_id1 bigint not null, " + + "p1_id2 bigint not null, " + + "p2_id1 bigint not null, " + + "p2_id2 bigint not null, " + + "foreign key (p1_id1, p1_id2) references Parent1(id1, id2), " + + "foreign key (p2_id1, p2_id2) references Parent2(id1, id2) " + + ")"); + + DbInfoDto schema = DbInfoExtractor.extract(getConnection()); + TableDto child = schema.tables.stream().filter(t -> t.id.name.equalsIgnoreCase("ChildTwoImplicit")).findAny().get(); + + assertEquals(2, child.foreignKeys.size()); + + ForeignKeyDto fk1 = child.foreignKeys.stream() + .filter(fk -> fk.targetTable.equalsIgnoreCase("Parent1")) + .findFirst().get(); + + assertEquals(2, fk1.sourceColumns.size()); + assertTrue(fk1.sourceColumns.get(0).equalsIgnoreCase("p1_id1")); + assertTrue(fk1.sourceColumns.get(1).equalsIgnoreCase("p1_id2")); + assertEquals(2, fk1.targetColumns.size()); + assertTrue(fk1.targetColumns.get(0).equalsIgnoreCase("id1")); + assertTrue(fk1.targetColumns.get(1).equalsIgnoreCase("id2")); + + ForeignKeyDto fk2 = child.foreignKeys.stream() + .filter(fk -> fk.targetTable.equalsIgnoreCase("Parent2")) + .findFirst().get(); + + assertEquals(2, fk2.sourceColumns.size()); + assertTrue(fk2.sourceColumns.get(0).equalsIgnoreCase("p2_id1")); + assertTrue(fk2.sourceColumns.get(1).equalsIgnoreCase("p2_id2")); + assertEquals(2, fk2.targetColumns.size()); + assertTrue(fk2.targetColumns.get(0).equalsIgnoreCase("id1")); + assertTrue(fk2.targetColumns.get(1).equalsIgnoreCase("id2")); + } + +} diff --git a/client-java/sql/src/test/java/org/evomaster/client/java/sql/cleaner/DbCleanerTestBase.java b/client-java/sql/src/test/java/org/evomaster/client/java/sql/cleaner/DbCleanerTestBase.java index b819fb32e6..61aa6ce07c 100644 --- a/client-java/sql/src/test/java/org/evomaster/client/java/sql/cleaner/DbCleanerTestBase.java +++ b/client-java/sql/src/test/java/org/evomaster/client/java/sql/cleaner/DbCleanerTestBase.java @@ -8,6 +8,7 @@ import java.math.BigInteger; import java.sql.Connection; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -117,6 +118,10 @@ public void testFKs() throws Exception{ assertEquals(0, res.seeRows().size()); res = SqlScriptRunner.execCommand(getConnection(), "SELECT * FROM Bar;"); assertEquals(0, res.seeRows().size()); + + SqlScriptRunner.execCommand(getConnection(), "INSERT INTO Foo (x) VALUES (42)"); + clearDatabase(Collections.singletonList("foo")); + } @Test @@ -182,4 +187,4 @@ public void testAvoidViews() throws Exception{ //this should work without throwing any exception clearDatabase(null); } -} \ No newline at end of file +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/compositepk/PostgresCompositePKApplication.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/compositepk/PostgresCompositePKApplication.kt new file mode 100644 index 0000000000..771e85dbc6 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/compositepk/PostgresCompositePKApplication.kt @@ -0,0 +1,37 @@ +package com.foo.spring.rest.postgres.compositepk + +import com.foo.spring.rest.postgres.SwaggerConfiguration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import springfox.documentation.swagger2.annotations.EnableSwagger2 +import javax.persistence.EntityManager + +@EnableSwagger2 +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/postgres/compositepk"]) +open class PostgresCompositePKApplication : SwaggerConfiguration() { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(PostgresCompositePKApplication::class.java, *args) + } + } + + @Autowired + private lateinit var em: EntityManager + + @GetMapping(path = ["/testPK"]) + open fun testPK(): ResponseEntity { + val query = em.createNativeQuery("select 1 from CompositePK where id1 > 0 and id2 > 0") + val res = query.resultList + + val status = if (res.isNotEmpty()) 200 else 400 + return ResponseEntity.status(status).build() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKApplication.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKApplication.kt new file mode 100644 index 0000000000..f2ae3af4da --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/kotlin/com/foo/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKApplication.kt @@ -0,0 +1,37 @@ +package com.foo.spring.rest.postgres.multicolumnfk + +import com.foo.spring.rest.postgres.SwaggerConfiguration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import springfox.documentation.swagger2.annotations.EnableSwagger2 +import javax.persistence.EntityManager + +@EnableSwagger2 +@SpringBootApplication(exclude = [SecurityAutoConfiguration::class]) +@RequestMapping(path = ["/api/postgres/multicolumnfk"]) +open class PostgresMultiColumnFKApplication : SwaggerConfiguration() { + + companion object { + @JvmStatic + fun main(args: Array) { + SpringApplication.run(PostgresMultiColumnFKApplication::class.java, *args) + } + } + + @Autowired + private lateinit var em: EntityManager + + @GetMapping(path = ["/testFK"]) + open fun testFK(): ResponseEntity { + val query = em.createNativeQuery("select * from Parent p JOIN Child c ON p.id1 = c.parent_id1 AND p.id2 = c.parent_id2 WHERE c.parent_id1>0 AND c.parent_id2>0") + val res = query.resultList + + val status = if (res.isNotEmpty()) 200 else 400 + return ResponseEntity.status(status).build() + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/compositepk/V1.0__createDB.sql b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/compositepk/V1.0__createDB.sql new file mode 100644 index 0000000000..a8ba7b8285 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/compositepk/V1.0__createDB.sql @@ -0,0 +1,6 @@ +CREATE TABLE CompositePK ( + id1 integer NOT NULL, + id2 integer NOT NULL, + name varchar(255), + PRIMARY KEY (id1, id2) +); diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/multicolumnfk/V1.0__createDB.sql b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/multicolumnfk/V1.0__createDB.sql new file mode 100644 index 0000000000..73699f5aa8 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/main/resources/schema/multicolumnfk/V1.0__createDB.sql @@ -0,0 +1,14 @@ +CREATE TABLE Parent ( + id1 integer NOT NULL, + id2 integer NOT NULL, + name varchar(255), + PRIMARY KEY (id1, id2) +); + +CREATE TABLE Child ( + child_id integer PRIMARY KEY, + parent_id1 integer NOT NULL, + parent_id2 integer NOT NULL, + description varchar(255), + FOREIGN KEY (parent_id1, parent_id2) REFERENCES Parent(id1, id2) +); diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/compositepk/PostgresCompositePKController.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/compositepk/PostgresCompositePKController.kt new file mode 100644 index 0000000000..690d3f4b21 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/compositepk/PostgresCompositePKController.kt @@ -0,0 +1,8 @@ +package com.foo.spring.rest.postgres.compositepk + +import com.foo.spring.rest.postgres.SpringRestPostgresController + +class PostgresCompositePKController : SpringRestPostgresController(PostgresCompositePKApplication::class.java) { + + override fun pathToFlywayFiles() = "classpath:/schema/compositepk" +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKController.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKController.kt new file mode 100644 index 0000000000..2ad4668c95 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/com/foo/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKController.kt @@ -0,0 +1,8 @@ +package com.foo.spring.rest.postgres.multicolumnfk + +import com.foo.spring.rest.postgres.SpringRestPostgresController + +class PostgresMultiColumnFKController : SpringRestPostgresController(PostgresMultiColumnFKApplication::class.java) { + + override fun pathToFlywayFiles() = "classpath:/schema/multicolumnfk" +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/compositepk/PostgresCompositePKEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/compositepk/PostgresCompositePKEMTest.kt new file mode 100644 index 0000000000..8a854ebcee --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/compositepk/PostgresCompositePKEMTest.kt @@ -0,0 +1,37 @@ +package org.evomaster.e2etests.spring.rest.postgres.compositepk + +import com.foo.spring.rest.postgres.compositepk.PostgresCompositePKController +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.rest.postgres.SpringRestPostgresTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class PostgresCompositePKEMTest : SpringRestPostgresTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun initClass() { + initKlass(PostgresCompositePKController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "PostgresCompositePKEM", + "com.foo.spring.rest.postgres.compositepk.PostgresCompositePKEM", + 1_000 + ) { args -> + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + assertHasAtLeastOne(solution, HttpVerb.GET, 400, "/api/postgres/compositepk/testPK", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/postgres/compositepk/testPK", null) + } + } +} diff --git a/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKEMTest.kt b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKEMTest.kt new file mode 100644 index 0000000000..1bf810ed46 --- /dev/null +++ b/core-tests/e2e-tests/spring/spring-rest-postgres/src/test/kotlin/org/evomaster/e2etests/spring/rest/postgres/multicolumnfk/PostgresMultiColumnFKEMTest.kt @@ -0,0 +1,38 @@ +package org.evomaster.e2etests.spring.rest.postgres.multicolumnfk + +import com.foo.spring.rest.postgres.multicolumnfk.PostgresMultiColumnFKController +import org.evomaster.core.problem.rest.data.HttpVerb +import org.evomaster.e2etests.spring.rest.postgres.SpringRestPostgresTestBase +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +class PostgresMultiColumnFKEMTest : SpringRestPostgresTestBase() { + + companion object { + @BeforeAll + @JvmStatic + fun initClass() { + initKlass(PostgresMultiColumnFKController()) + } + } + + @Test + fun testRunEM() { + + runTestHandlingFlakyAndCompilation( + "PostgresMultiColumnFKEM", + "com.foo.spring.rest.postgres.multicolumnfk.PostgresMultiColumnFKEM", + 1_000 + ) { args -> + + val solution = initAndRun(args) + + assertTrue(solution.individuals.size >= 1) + + assertHasAtLeastOne(solution, HttpVerb.GET, 400, "/api/postgres/multicolumnfk/testFK", null) + assertHasAtLeastOne(solution, HttpVerb.GET, 200, "/api/postgres/multicolumnfk/testFK", null) + } + } +} diff --git a/core/src/main/kotlin/org/evomaster/core/output/SqlWriter.kt b/core/src/main/kotlin/org/evomaster/core/output/SqlWriter.kt index 627396e580..dfba32b000 100644 --- a/core/src/main/kotlin/org/evomaster/core/output/SqlWriter.kt +++ b/core/src/main/kotlin/org/evomaster/core/output/SqlWriter.kt @@ -139,14 +139,11 @@ object SqlWriter { val uniqueIdOfPrimaryKey = fkg.uniqueIdOfPrimaryKey - /* - TODO: the code here is not handling multi-column PKs/FKs - */ val pkExisting = allActions .filter { it.representExistingData } .flatMap { it.seeTopGenes() } .filterIsInstance() - .find { it.uniqueId == uniqueIdOfPrimaryKey } + .find { it.uniqueId == uniqueIdOfPrimaryKey && it.name == fkg.targetColumn } /* This FK might point to a PK of data already existing in the database. @@ -175,7 +172,7 @@ object SqlWriter { val pkg = allActions .flatMap { it.seeTopGenes() } .filterIsInstance() - .find { it.uniqueId == uniqueIdOfPrimaryKey }!! + .find { it.uniqueId == uniqueIdOfPrimaryKey && it.name == fkg.targetColumn }!! val pk = getPrintableValue(format, pkg) return ".d(\"$variableName\", \"$pk\")" diff --git a/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlForeignKeyGene.kt b/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlForeignKeyGene.kt index 571d84c39a..462ce8a5b8 100644 --- a/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlForeignKeyGene.kt +++ b/core/src/main/kotlin/org/evomaster/core/search/gene/sql/SqlForeignKeyGene.kt @@ -25,26 +25,41 @@ import org.evomaster.core.sql.schema.TableId * known beforehand, as primary keys could be dynamically generated by the database. */ class SqlForeignKeyGene( + /** + * The name of this column in the table. + */ sourceColumn: String, + /** + * The position in the list of SQL insertion actions + * where the FK is being inserted. + */ uniqueId: Long, /** * The id of the table this FK points to */ val targetTable: TableId, + /** + * The name of the column in the target table this FK points to. + */ + val targetColumn: String, + /** + * If true, this FK can be NULL. + */ val nullable: Boolean, /** * A negative value means this FK is not bound yet. * Otherwise, it should be equal to the uniqueId of * a previous SqlPrimaryKey */ - var uniqueIdOfPrimaryKey: Long = -1 + var uniqueIdOfPrimaryKey: Long = -1, + /** + * names of columns that form a composite foreign key + * together with this one. + */ + val otherSourceColumnsInCompositeFK: List = emptyList() ) : SqlWrapperGene, SimpleGene(sourceColumn) { - @Deprecated("Rather use the one forcing TableId") - constructor(sourceColumn: String, uniqueId: Long, targetTable: String, nullable: Boolean, uniqueIdOfPrimaryKey: Long =-1) - : this(sourceColumn, uniqueId, TableId(targetTable), nullable, uniqueIdOfPrimaryKey) - init { if (uniqueId < 0) { throw IllegalArgumentException("Negative unique id") @@ -72,7 +87,7 @@ class SqlForeignKeyGene( return this } - override fun copyContent() = SqlForeignKeyGene(name, uniqueId, targetTable, nullable, uniqueIdOfPrimaryKey) + override fun copyContent() = SqlForeignKeyGene(name, uniqueId, targetTable, targetColumn, nullable, uniqueIdOfPrimaryKey, otherSourceColumnsInCompositeFK) override fun setValueWithRawString(value: String) { throw IllegalStateException("cannot set value with string ($value) for ${this.javaClass.simpleName}") @@ -110,6 +125,22 @@ class SqlForeignKeyGene( .map { it.uniqueId } .toSet() + // For composite FKs, we need to make sure all the genes point to the same PK. + // We find other FK genes in the same action that point to the same target table AND are part of the same composite FK. + val otherFksInSameAction = allGenes.asSequence() + .flatMap { it.flatView().asSequence() } + .filterIsInstance() + .filter { it.uniqueId == uniqueId && it !== this && it.targetTable == targetTable } + .filter { otherSourceColumnsInCompositeFK.contains(it.name) } + .toList() + + val alreadyBoundId = otherFksInSameAction.find { it.isBound() }?.uniqueIdOfPrimaryKey + + if (alreadyBoundId != null) { + uniqueIdOfPrimaryKey = alreadyBoundId + return + } + if (pks.isEmpty()) { /* FIXME: we cannot crash here. @@ -180,8 +211,11 @@ class SqlForeignKeyGene( } } - val pk = previousGenes.find { it is SqlPrimaryKeyGene && it.uniqueId == uniqueIdOfPrimaryKey } - ?: throw IllegalArgumentException("Input genes do not contain primary key with id $uniqueIdOfPrimaryKey") + val pk = previousGenes.find { + it is SqlPrimaryKeyGene && + it.uniqueId == uniqueIdOfPrimaryKey && + it.name == targetColumn + } ?: throw IllegalArgumentException("Input genes do not contain primary key with id $uniqueIdOfPrimaryKey and column $targetColumn") if (!pk.isPrintable()) { //this can happen if the PK is autoincrement @@ -197,8 +231,8 @@ class SqlForeignKeyGene( } val pk = previousGenes.filterIsInstance() - .find { it.uniqueId == uniqueIdOfPrimaryKey } - ?: throw IllegalArgumentException("Input genes do not contain primary key with id $uniqueIdOfPrimaryKey") + .find { it.uniqueId == uniqueIdOfPrimaryKey && it.name == targetColumn } + ?: throw IllegalArgumentException("Input genes do not contain primary key with id $uniqueIdOfPrimaryKey and column $targetColumn") if (!pk.isPrintable()) { diff --git a/core/src/main/kotlin/org/evomaster/core/sql/SqlActionGeneBuilder.kt b/core/src/main/kotlin/org/evomaster/core/sql/SqlActionGeneBuilder.kt index 84bf2ea31b..f0f88e675c 100644 --- a/core/src/main/kotlin/org/evomaster/core/sql/SqlActionGeneBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/sql/SqlActionGeneBuilder.kt @@ -10,7 +10,6 @@ import org.evomaster.core.sql.schema.Table import org.evomaster.core.parser.RegexHandler import org.evomaster.core.parser.RegexHandler.createGeneForPostgresLike import org.evomaster.core.parser.RegexHandler.createGeneForPostgresSimilarTo -import org.evomaster.core.parser.RegexType import org.evomaster.core.search.gene.* import org.evomaster.core.search.gene.collection.EnumGene import org.evomaster.core.search.gene.datetime.DateGene @@ -54,8 +53,9 @@ class SqlActionGeneBuilder { //TODO handle all constraints and cases column.autoIncrement -> SqlAutoIncrementGene(column.name) - fk != null -> - SqlForeignKeyGene(column.name, id, fk.targetTableId, column.nullable) + fk != null -> { + handleForeignKeyColumn(fk, column, id) + } else -> when (column.type) { // Man: TODO need to check @@ -406,6 +406,27 @@ class SqlActionGeneBuilder { return gene } + private fun handleForeignKeyColumn( + fk: ForeignKey, + column: Column, + id: Long, + ): SqlForeignKeyGene { + val indexOfSourceColumn = fk.sourceColumns.indexOf(column) + if (indexOfSourceColumn == -1) { + throw IllegalArgumentException("Column $column is not part of foreign key $fk") + } + val targetColumn = fk.targetColumns[indexOfSourceColumn] + val otherSourceColumnsInCompositeFK = fk.sourceColumns.filter { it != column }.map { it.name } + return SqlForeignKeyGene( + sourceColumn = column.name, + uniqueId = id, + targetTable = fk.targetTableId, + targetColumn = targetColumn.name, + nullable = column.nullable, + otherSourceColumnsInCompositeFK = otherSourceColumnsInCompositeFK + ) + } + private fun handleSqlGeometry(column: Column): ChoiceGene<*> { return ChoiceGene(name = column.name, listOf(buildSqlPointGene(column), diff --git a/core/src/main/kotlin/org/evomaster/core/sql/SqlActionUtils.kt b/core/src/main/kotlin/org/evomaster/core/sql/SqlActionUtils.kt index a7c26bf327..2d02db8e18 100644 --- a/core/src/main/kotlin/org/evomaster/core/sql/SqlActionUtils.kt +++ b/core/src/main/kotlin/org/evomaster/core/sql/SqlActionUtils.kt @@ -6,6 +6,7 @@ import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.sql.SqlForeignKeyGene import org.evomaster.core.search.gene.sql.SqlPrimaryKeyGene import org.evomaster.core.search.service.Randomness +import org.evomaster.core.sql.schema.ForeignKey import org.slf4j.Logger import org.slf4j.LoggerFactory import org.evomaster.core.sql.schema.Table @@ -140,49 +141,104 @@ object SqlActionUtils { } + /** + * Validates the foreign key constraints in the provided list of SQL actions. + * + * @param actions A list of SQL actions to validate their foreign key constraints. + * @param errors An optional mutable list to store error messages if any invalidity is found. + * If null, errors will not be collected. + * + * @return A boolean value indicating whether all foreign key constraints are valid. + * Returns true if valid, otherwise false. + */ fun isValidForeignKeys(actions: List, errors: MutableList? = null): Boolean { for (i in 0 until actions.size) { - - val fks = actions[i].seeTopGenes() + val currentAction = actions[i] + val currentFkGenes = currentAction.seeTopGenes() .flatMap { it.flatView() } .filterIsInstance() - fks.find { !it.nullable && !it.isBound() } + currentFkGenes.find { !it.nullable && !it.isBound() } ?.let { errors?.add("FK ${it.name} is not nullable and not bound: this is invalid") return false } if (i == 0) { - if(fks.isEmpty()) { + if (currentFkGenes.isEmpty()) { continue - } - else { + } else { errors?.add("First SQL action has FKs") return false } } - /* - note: a row could have FK to itself... weird, but possible. - but not sure if we should allow it - */ - val previous = actions.subList(0, i) - - fks.filter { it.isBound() } - .map { it.uniqueIdOfPrimaryKey } - .forEach { id -> - val match = previous.asSequence() - .flatMap { it.seeTopGenes().asSequence() } - .filterIsInstance() - .any { it.uniqueId == id } - - if (!match) { - errors?.add("FK is pointing to $id, but such action could not be found in previous insertions.") - return false - } + // Collect all unique Ids and their associated table IDs + val previousSqlActions = actions.subList(0, i) + val previousPkMap = mutableMapOf() + previousSqlActions.forEach { action -> + action.seeTopGenes() + .flatMap { it.flatView() } + .filterIsInstance() + .forEach { previousPkMap[it.uniqueId] = it.tableName } + } + + // Check each foreign key constraint defined in the table + for (fkConstraint in currentAction.table.foreignKeys) { + if (!isValidForeignKey(fkConstraint, currentFkGenes, previousPkMap, errors)) { + return false } + } + } + + return true + } + + /** + * Validates if the provided foreign key constraint is satisfied given the specified context. + * + * @param fkConstraint the foreign key constraint to validate + * @param currentFkGenes a list of `SqlForeignKeyGene` representing the current foreign key elements + * @param previousPkUniqueIds a set of unique IDs of primary keys from previous operations + * @param errors a mutable list to which validation error messages will be added, if any + * @return `true` if the foreign key constraint is valid, `false` otherwise + */ + private fun isValidForeignKey( + fkConstraint: ForeignKey, + currentFkGenes: List, + previousPkMap: Map, + errors: MutableList? + ): Boolean { + val sourceColumnNames = fkConstraint.sourceColumns.map { it.name } + val genesInFk = currentFkGenes.filter { sourceColumnNames.contains(it.name) } + + if (genesInFk.isEmpty()) { + if (fkConstraint.sourceColumns.all { it.nullable }) { + return true + } + errors?.add("FK constraint $fkConstraint is not satisfied: no genes found for source columns ${sourceColumnNames.joinToString(",")}") + return false + } + + val boundIds = genesInFk.filter { it.isBound() }.map { it.uniqueIdOfPrimaryKey }.distinct() + if (boundIds.size > 1) { + errors?.add("Composite FK $fkConstraint points to multiple different primary keys: $boundIds") + return false + } + + if (boundIds.isNotEmpty()) { + val referencedPkUniqueId = boundIds.first() + if (!previousPkMap.containsKey(referencedPkUniqueId)) { + errors?.add("FK is pointing to $referencedPkUniqueId, but such action could not be found in previous insertions.") + return false + } + + val actualTableId = previousPkMap[referencedPkUniqueId] + if (actualTableId != fkConstraint.targetTableId) { + errors?.add("FK is pointing to $referencedPkUniqueId from table $actualTableId, but the constraint expects table ${fkConstraint.targetTableId}") + return false + } } return true diff --git a/core/src/main/kotlin/org/evomaster/core/sql/SqlInsertBuilder.kt b/core/src/main/kotlin/org/evomaster/core/sql/SqlInsertBuilder.kt index 900137ed93..a4c3e0d686 100644 --- a/core/src/main/kotlin/org/evomaster/core/sql/SqlInsertBuilder.kt +++ b/core/src/main/kotlin/org/evomaster/core/sql/SqlInsertBuilder.kt @@ -1,6 +1,5 @@ package org.evomaster.core.sql -import org.evomaster.client.java.controller.api.dto.SqlDtoUtils import org.evomaster.client.java.controller.api.dto.database.operations.DataRowDto import org.evomaster.client.java.controller.api.dto.database.operations.DatabaseCommandDto import org.evomaster.client.java.controller.api.dto.database.operations.QueryResultDto @@ -179,32 +178,61 @@ class SqlInsertBuilder( tableToColumns: MutableMap> ): MutableSet { val fks = mutableSetOf() - + val tableId = TableId.fromDto(databaseType, tableDto.id) for (fk in tableDto.foreignKeys) { - val tableKey = SqlActionUtils.getTableKey(tableToColumns.keys, fk.targetTable) + val targetTableId = SqlActionUtils.getTableKey(tableToColumns.keys, fk.targetTable) - if(tableKey == null || tableToColumns[tableKey] == null) { + if(targetTableId == null || tableToColumns[targetTableId] == null) { throw IllegalArgumentException("Foreign key for non-existent table ${fk.targetTable}") } - val sourceColumns = mutableSetOf() + val sourceColumns = getSourceColumnsOfForeignKey(fk, tableToColumns, tableId) + val targetColumns = getTargetColumnsOfForeignKey(fk, tableToColumns, targetTableId) + if (sourceColumns.size!=targetColumns.size) { + throw IllegalArgumentException("Foreign key for table ${fk.targetTable} has ${sourceColumns.size} source columns but ${targetColumns.size} target columns") + } + val foreignKey = ForeignKey(sourceColumns, targetTableId, targetColumns) + fks.add(foreignKey) + } + return fks + } - for (cname in fk.sourceColumns) { - // TODO: wrong check, as should be based on targetColumns, when we ll introduce them - // val c = targetTable.find { it.name.equals(cname, ignoreCase = true) } - // ?: throw IllegalArgumentException("Issue in foreign key: table ${f.targetTable} does not have a column called $cname") + private fun getSourceColumnsOfForeignKey( + fk: ForeignKeyDto, + tableToColumns: MutableMap>, + tableId: TableId + ): MutableList { + val sourceColumns = mutableListOf() + for (sourceColumnName in fk.sourceColumns) { - val id = TableId.fromDto(databaseType, tableDto.id) + val c = tableToColumns[tableId]!!.find { it.name.equals(sourceColumnName, ignoreCase = true) } + ?: throw IllegalArgumentException("Issue in foreign key: table ${tableId.name} does not have a column called $sourceColumnName") - val c = tableToColumns[id]!!.find { it.name.equals(cname, ignoreCase = true) } - ?: throw IllegalArgumentException("Issue in foreign key: table ${tableDto.id.name} does not have a column called $cname") + sourceColumns.add(c) + } + return sourceColumns + } - sourceColumns.add(c) + private fun getTargetColumnsOfForeignKey( + foreignKeyDto: ForeignKeyDto, + tableToColumns: MutableMap>, + targetTableId: TableId, + ): MutableList { + val targetColumns = mutableListOf() + if (foreignKeyDto.targetColumns.isEmpty()) { + val targetTablePrimaryKeys = tableToColumns[targetTableId]!!.filter { it.primaryKey } + if (targetTablePrimaryKeys.isEmpty()) { + throw IllegalArgumentException("Foreign key for table ${foreignKeyDto.targetTable} has no specified target columns and no primary key found") + } + targetColumns.addAll(targetTablePrimaryKeys) + } else { + for (targetColumnName in foreignKeyDto.targetColumns) { + val targetColumn = tableToColumns[targetTableId]!!.find { it.name.equals(targetColumnName, ignoreCase = true) } + ?: throw IllegalArgumentException("Issue in foreign key: table ${foreignKeyDto.targetTable} does not have a column called $targetColumnName") + targetColumns.add(targetColumn) } - - fks.add(ForeignKey(sourceColumns, tableKey)) } - return fks + return targetColumns } private fun generateColumnsFrom( diff --git a/core/src/main/kotlin/org/evomaster/core/sql/TableConstraintEvaluator.kt b/core/src/main/kotlin/org/evomaster/core/sql/TableConstraintEvaluator.kt index 9ed5dd05dd..368c30dff9 100644 --- a/core/src/main/kotlin/org/evomaster/core/sql/TableConstraintEvaluator.kt +++ b/core/src/main/kotlin/org/evomaster/core/sql/TableConstraintEvaluator.kt @@ -176,6 +176,15 @@ class TableConstraintEvaluator(val previousActions: List = listOf()) continue } val tuple = getTuple(constraint.uniqueColumnNames, previousAction.seeTopGenes()) + if (tuple.values.any { it == null }) { + /** + * In SQL, NULL represents an unknown value, and comparisons involving NULL do not + * evaluate to true (even NULL = NULL is not true). Because of this, + * UNIQUE constraints typically treat rows containing NULL values as not equal to each + * other, allowing multiple rows with NULL in the constrained column(s). + */ + continue + } if (tuple in tuples) { // if the tuple was already observed, return false return false @@ -198,6 +207,8 @@ class TableConstraintEvaluator(val previousActions: List = listOf()) // for that column. Momentarly we will use NULL as default value for all // columns not listed. tuple[uniqueColumnName] = null + } else if (gene is NullableGene && !gene.isActive) { + tuple[uniqueColumnName] = null } else { val rawString = gene.getValueAsRawString() tuple[uniqueColumnName] = rawString diff --git a/core/src/main/kotlin/org/evomaster/core/sql/schema/ForeignKey.kt b/core/src/main/kotlin/org/evomaster/core/sql/schema/ForeignKey.kt index 6a94623786..0535870c76 100644 --- a/core/src/main/kotlin/org/evomaster/core/sql/schema/ForeignKey.kt +++ b/core/src/main/kotlin/org/evomaster/core/sql/schema/ForeignKey.kt @@ -7,7 +7,19 @@ package org.evomaster.core.sql.schema data class ForeignKey( - val sourceColumns: Set, + /** + * The columns in the source table that are referenced by this foreign key + */ + val sourceColumns: List, - val targetTableId: TableId -) \ No newline at end of file + /** + * The target table that is referenced by this foreign key + */ + val targetTableId: TableId, + + /** + * The columns in the target table that are referenced by this foreign key. + * The order should match the order of the source columns. + */ + val targetColumns: List +) diff --git a/core/src/test/kotlin/org/evomaster/core/TestUtils.kt b/core/src/test/kotlin/org/evomaster/core/TestUtils.kt index b1889039b3..ee975dd14f 100644 --- a/core/src/test/kotlin/org/evomaster/core/TestUtils.kt +++ b/core/src/test/kotlin/org/evomaster/core/TestUtils.kt @@ -77,7 +77,7 @@ object TestUtils { val fkColumName = "fkId" val fkId = Column(fkColumName, ColumnDataType.INTEGER, 10, primaryKey = false, databaseType = DatabaseType.H2) - val foreignKeyGene = SqlForeignKeyGene(fkColumName, bId, TableId(aTable), false, uniqueIdOfPrimaryKey = aUniqueId) + val foreignKeyGene = SqlForeignKeyGene(fkColumName, bId, TableId(aTable), "id", false, uniqueIdOfPrimaryKey = aUniqueId) val barInsertion = generateFakeDbAction(bId, bUniqueId, bTable, bValue, fkId, foreignKeyGene) @@ -109,4 +109,4 @@ object TestUtils { ) } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/org/evomaster/core/output/SqlWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/SqlWriterTest.kt new file mode 100644 index 0000000000..4b17163de1 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/output/SqlWriterTest.kt @@ -0,0 +1,67 @@ +package org.evomaster.core.output + +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType +import org.evomaster.core.search.action.EvaluatedDbAction +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.sql.SqlForeignKeyGene +import org.evomaster.core.search.gene.sql.SqlPrimaryKeyGene +import org.evomaster.core.sql.SqlAction +import org.evomaster.core.sql.SqlActionResult +import org.evomaster.core.sql.schema.Column +import org.evomaster.core.sql.schema.ColumnDataType +import org.evomaster.core.sql.schema.ForeignKey +import org.evomaster.core.sql.schema.Table +import org.evomaster.core.sql.schema.TableId +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class SqlWriterTest { + + @Test + fun testHandleFKWithMultiColumnForeignKey() { + val targetTableId = TableId("target") + val pkCol1 = Column("pk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val pkCol2 = Column("pk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val targetTable = Table(targetTableId, setOf(pkCol1, pkCol2), emptySet()) + + val sourceTableId = TableId("source") + val fkCol1 = Column("fk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fkCol2 = Column("fk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val foreignKey = ForeignKey(listOf(fkCol1, fkCol2), targetTableId, listOf(pkCol1, pkCol2)) + val sourceTable = Table(sourceTableId, setOf(fkCol1, fkCol2), setOf(foreignKey)) + + val pkUniqueId = 1L + val pkGene1 = SqlPrimaryKeyGene("pk1", targetTableId, IntegerGene("pk1", 10), pkUniqueId) + val pkGene2 = SqlPrimaryKeyGene("pk2", targetTableId, IntegerGene("pk2", 20), pkUniqueId) + val targetAction = SqlAction(targetTable, setOf(pkCol1, pkCol2), 100L, listOf(pkGene1, pkGene2)) + targetAction.setLocalId("0") + + val fkUniqueId = 2L + val fkGene1 = SqlForeignKeyGene("fk1", fkUniqueId, targetTableId, "pk1", false, + otherSourceColumnsInCompositeFK = listOf("fk2"), uniqueIdOfPrimaryKey = pkUniqueId) + val fkGene2 = SqlForeignKeyGene("fk2", fkUniqueId, targetTableId, "pk2", false, + otherSourceColumnsInCompositeFK = listOf("fk1"), uniqueIdOfPrimaryKey = pkUniqueId) + val sourceAction = SqlAction(sourceTable, setOf(fkCol1, fkCol2), 101L, listOf(fkGene1, fkGene2)) + sourceAction.setLocalId("1") + + val format = OutputFormat.JAVA_JUNIT_5 + val lines = Lines(format) + val dbInitialization = listOf( + EvaluatedDbAction(targetAction, SqlActionResult("0").apply { setInsertExecutionResult(true) }), + EvaluatedDbAction(sourceAction, SqlActionResult("1").apply { setInsertExecutionResult(true) }) + ) + + SqlWriter.handleDbInitialization( + format = format, + dbInitialization = dbInitialization, + lines = lines, + insertionVars = mutableListOf(), + skipFailure = false + ) + + val output = lines.toString() + assertTrue(output.contains(".insertInto(\"source\", 101L)")) + assertTrue(output.contains(".d(\"fk1\", \"10\")")) + assertTrue(output.contains(".d(\"fk2\", \"20\")")) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt b/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt index 6fabb3f1fc..d2454da456 100644 --- a/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/output/TestCaseWriterTest.kt @@ -328,7 +328,7 @@ class TestCaseWriterTest : WriterTestBase(){ val firstInsertionId = 1001L val insertIntoTable0 = SqlAction(table0, setOf(idColumn), firstInsertionId, listOf(primaryKeyTable0Gene)) val secondInsertionId = 1002L - val foreignKeyGene = SqlForeignKeyGene(fkColumn.name, secondInsertionId, "Table0", false, uniqueIdOfPrimaryKey = pkGeneUniqueId) + val foreignKeyGene = SqlForeignKeyGene(fkColumn.name, secondInsertionId, TableId("Table0"), idColumn.name, false, uniqueIdOfPrimaryKey = pkGeneUniqueId) val insertIntoTable1 = SqlAction(table1, setOf(idColumn, fkColumn), secondInsertionId, listOf(primaryKeyTable1Gene, foreignKeyGene)) @@ -366,6 +366,84 @@ class TestCaseWriterTest : WriterTestBase(){ } + @Test + fun testMultipleForeignKeyColumns() { + val idColumn = Column("Id", INTEGER, 10, primaryKey = true, databaseType = DatabaseType.H2) + val table0 = Table("Table0", setOf(idColumn), HashSet()) + val table1 = Table("Table1", setOf(idColumn), HashSet()) + + val fk0Column = Column("fkId0", INTEGER, 10, primaryKey = false, databaseType = DatabaseType.H2) + val fk1Column = Column("fkId1", INTEGER, 10, primaryKey = false, databaseType = DatabaseType.H2) + val table2 = Table("Table2", setOf(idColumn, fk0Column, fk1Column), HashSet()) + + + val pk0UniqueId = 12345L + val pk1UniqueId = 54321L + + val integerGene0 = IntegerGene(idColumn.name, 42, 0, 100) + val primaryKeyTable0Gene = SqlPrimaryKeyGene(idColumn.name, TableId("Table0"), integerGene0, pk0UniqueId) + + val integerGene1 = IntegerGene(idColumn.name, 84, 0, 100) + val primaryKeyTable1Gene = SqlPrimaryKeyGene(idColumn.name, TableId("Table1"), integerGene1, pk1UniqueId) + + val integerGene2 = IntegerGene(idColumn.name, 10, 0, 100) + val primaryKeyTable2Gene = SqlPrimaryKeyGene(idColumn.name, TableId("Table2"), integerGene2, 20) + + + val firstInsertionId = 1001L + val insertIntoTable0 = SqlAction(table0, setOf(idColumn), firstInsertionId, listOf(primaryKeyTable0Gene)) + + val secondInsertionId = 1002L + val insertIntoTable1 = SqlAction(table1, setOf(idColumn), secondInsertionId, listOf(primaryKeyTable1Gene)) + + val thirdInsertionId = 1003L + val foreignKey0Gene = SqlForeignKeyGene(fk0Column.name, thirdInsertionId, TableId("Table0"), idColumn.name, false, uniqueIdOfPrimaryKey = pk0UniqueId) + val foreignKey1Gene = SqlForeignKeyGene(fk1Column.name, thirdInsertionId, TableId("Table1"), idColumn.name, false, uniqueIdOfPrimaryKey = pk1UniqueId) + + val insertIntoTable2 = SqlAction(table2, setOf(idColumn, fk0Column, fk1Column), thirdInsertionId, listOf(primaryKeyTable2Gene, foreignKey0Gene, foreignKey1Gene)) + + val (format, baseUrlOfSut, ei) = buildEvaluatedIndividual(mutableListOf( + insertIntoTable0.copy() as SqlAction, + insertIntoTable1.copy() as SqlAction, + insertIntoTable2.copy() as SqlAction)) + val config = getConfig(format) + + val test = TestCase(test = ei, name = "test") + + val writer = RestTestCaseWriter(config, PartialOracles()) + + val lines = writer.convertToCompilableTestCode(test, baseUrlOfSut) + + val expectedLines = Lines(format).apply { + add("@Test") + add("public void test() throws Exception {") + indent() + add("List insertions = sql().insertInto(\"Table0\", 1001L)") + indent() + indent() + add(".d(\"Id\", \"42\")") + deindent() + add(".and().insertInto(\"Table1\", 1002L)") + indent() + add(".d(\"Id\", \"84\")") + deindent() + add(".and().insertInto(\"Table2\", 1003L)") + indent() + add(".d(\"Id\", \"10\")") + add(".d(\"fkId0\", \"42\")") + add(".d(\"fkId1\", \"84\")") + deindent() + add(".dtos();") + deindent() + add("InsertionResultsDto insertionsresult = controller.execInsertionsIntoDatabase(insertions);") + deindent() + add("}") + } + + assertEquals(expectedLines.toString(), lines.toString()) + } + + @Test fun testBooleanColumn() { val aColumn = Column("aColumn", BOOLEAN, 10, databaseType = DatabaseType.H2) @@ -427,7 +505,7 @@ class TestCaseWriterTest : WriterTestBase(){ val firstInsertionId = 1001L val insertIntoTable0 = SqlAction(table0, setOf(idColumn), firstInsertionId, listOf(primaryKeyTable0Gene)) val secondInsertionId = 1002L - val foreignKeyGene = SqlForeignKeyGene(fkColumn.name, secondInsertionId, "Table0", true, -1L) + val foreignKeyGene = SqlForeignKeyGene(fkColumn.name, secondInsertionId, TableId("Table0"), idColumn.name, true, -1L) val insertIntoTable1 = SqlAction(table1, setOf(idColumn, fkColumn), secondInsertionId, listOf(primaryKeyTable1Gene, foreignKeyGene)) @@ -585,7 +663,7 @@ class TestCaseWriterTest : WriterTestBase(){ val firstInsertionId = 1001L val insertIntoTable0 = SqlAction(table0, setOf(idColumn), firstInsertionId, listOf(primaryKeyTable0Gene)) val secondInsertionId = 1002L - val foreignKeyGene = SqlForeignKeyGene(fkColumn.name, secondInsertionId, "Table0", false, pkGeneUniqueId) + val foreignKeyGene = SqlForeignKeyGene(fkColumn.name, secondInsertionId, TableId("Table0"), idColumn.name, false, pkGeneUniqueId) val insertIntoTable1 = SqlAction(table1, setOf(idColumn, fkColumn), secondInsertionId, listOf(primaryKeyTable1Gene, foreignKeyGene)) @@ -640,13 +718,13 @@ class TestCaseWriterTest : WriterTestBase(){ val insertId1 = 1002L - val fkGene0 = SqlForeignKeyGene(table1_Id.name, insertId1, "Table0", false, insertId0) + val fkGene0 = SqlForeignKeyGene(table1_Id.name, insertId1, TableId("Table0"), table0_Id.name, false, insertId0) val pkGene1 = SqlPrimaryKeyGene(table1_Id.name, "Table1", fkGene0, insertId1) val insert1 = SqlAction(table1, setOf(table1_Id), insertId1, listOf(pkGene1)) val insertId2 = 1003L - val fkGene1 = SqlForeignKeyGene(table2_Id.name, insertId2, "Table1", false, insertId1) + val fkGene1 = SqlForeignKeyGene(table2_Id.name, insertId2, TableId("Table1"), table1_Id.name, false, insertId1) val pkGene2 = SqlPrimaryKeyGene(table2_Id.name, "Table2", fkGene1, insertId2) val insert2 = SqlAction(table2, setOf(table2_Id), insertId2, listOf(pkGene2)) @@ -1027,7 +1105,7 @@ class TestCaseWriterTest : WriterTestBase(){ val fooInsertionId = 1001L val fooInsertion = SqlAction(foo, setOf(fooId), fooInsertionId, listOf(pkFoo)) val barInsertionId = 1002L - val foreignKeyGene = SqlForeignKeyGene(fkId.name, barInsertionId, "Foo", false, uniqueIdOfPrimaryKey = pkGeneUniqueId) + val foreignKeyGene = SqlForeignKeyGene(fkId.name, barInsertionId, TableId("Foo"), fooId.name, false, uniqueIdOfPrimaryKey = pkGeneUniqueId) val barInsertion = SqlAction(bar, setOf(fooId, fkId), barInsertionId, listOf(pkBar, foreignKeyGene)) val fooAction = RestCallAction("1", HttpVerb.GET, RestPath("/foo"), mutableListOf()) @@ -1099,7 +1177,7 @@ public void test() throws Exception { val fooInsertionId = 1001L val fooInsertion = SqlAction(foo, setOf(fooId), fooInsertionId, listOf(pkFoo)) val barInsertionId = 1002L - val foreignKeyGene = SqlForeignKeyGene(fkId.name, barInsertionId, "Foo", false, uniqueIdOfPrimaryKey = pkGeneUniqueId) + val foreignKeyGene = SqlForeignKeyGene(fkId.name, barInsertionId, TableId("Foo"), fooId.name, false, uniqueIdOfPrimaryKey = pkGeneUniqueId) val barInsertion = SqlAction(bar, setOf(fooId, fkId), barInsertionId, listOf(pkBar, foreignKeyGene)) val fooAction = RestCallAction("1", HttpVerb.GET, RestPath("/foo"), mutableListOf()) diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt index 9b253caea2..3c446a4df3 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/GeneSamplerForTests.kt @@ -8,6 +8,7 @@ import org.evomaster.core.search.gene.interfaces.ComparableGene import org.evomaster.core.search.gene.mongo.ObjectIdGene import org.evomaster.core.search.gene.regex.* import org.evomaster.core.search.gene.sql.* +import org.evomaster.core.sql.schema.TableId import org.evomaster.core.search.gene.sql.geometric.* import org.evomaster.core.search.gene.network.CidrGene import org.evomaster.core.search.gene.network.InetGene @@ -376,7 +377,8 @@ object GeneSamplerForTests { private fun sampleSqlForeignKeyGene(rand: Randomness): SqlForeignKeyGene { return SqlForeignKeyGene(sourceColumn = "rand source column", uniqueId = rand.nextLong(min = 0L, max = Long.MAX_VALUE), - targetTable = "rand target table", + targetTable = TableId("rand target table"), + targetColumn = "rand target column", nullable = rand.nextBoolean(), uniqueIdOfPrimaryKey = rand.nextLong()) } diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/sql/SqlForeignKeyGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/sql/SqlForeignKeyGeneTest.kt new file mode 100644 index 0000000000..cc4e06824f --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/sql/SqlForeignKeyGeneTest.kt @@ -0,0 +1,382 @@ +package org.evomaster.core.search.gene.sql + +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType +import org.evomaster.core.problem.enterprise.SampleType +import org.evomaster.core.problem.rest.data.RestIndividual +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.service.Randomness +import org.evomaster.core.sql.SqlAction +import org.evomaster.core.sql.SqlActionUtils +import org.evomaster.core.sql.schema.Column +import org.evomaster.core.sql.schema.ColumnDataType +import org.evomaster.core.sql.schema.ForeignKey +import org.evomaster.core.sql.schema.Table +import org.evomaster.core.sql.schema.TableId +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class SqlForeignKeyGeneTest { + + @Test + fun testRandomizeSingleColumnForeignKey() { + val targetTableId = TableId("targetTable") + val pkColumn = Column("col_pk", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val targetTable = Table(targetTableId, setOf(pkColumn), emptySet()) + + val uniqueId0 = 1L + val pkGene1 = SqlPrimaryKeyGene("col_pk", targetTableId, IntegerGene("col_pk", 1), uniqueId0) + val action1 = SqlAction(targetTable, setOf(pkColumn), uniqueId0, listOf(pkGene1)) + + val uniqueId1 = 2L + val pkGene2 = SqlPrimaryKeyGene("col_pk", targetTableId, IntegerGene("col_pk", 2), uniqueId1) + val action2 = SqlAction(targetTable, setOf(pkColumn), uniqueId1, listOf(pkGene2)) + + val sourceTableId = TableId("sourceTable") + val fkColumn = Column("col_fk", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val foreignKey = ForeignKey(listOf(fkColumn), targetTableId, listOf(pkColumn)) + val sourceTable = Table(sourceTableId, setOf(fkColumn), setOf(foreignKey)) + val uniqueId2 = 3L + val fkGene = SqlForeignKeyGene("col_fk", uniqueId2, targetTableId, "col_pk", nullable = false, otherSourceColumnsInCompositeFK = emptyList()) + val action3 = SqlAction(sourceTable, setOf(fkColumn), uniqueId2, listOf(fkGene)) + + RestIndividual( + resourceCalls = mutableListOf(), + sampleType = SampleType.RANDOM, + dbInitialization = mutableListOf(action1, action2, action3) + ) + + val randomness = Randomness() + randomness.updateSeed(42) + + fkGene.randomize(randomness, tryToForceNewValue = false) + assertTrue(fkGene.isBound()) + assertTrue(fkGene.uniqueIdOfPrimaryKey == uniqueId0 || fkGene.uniqueIdOfPrimaryKey == uniqueId1) + assertTrue(SqlActionUtils.isValidActions(listOf(action1, action2, action3))) + + } + + @Test + fun testRandomizeMultiColumnForeignKey() { + val targetTableId = TableId("targetTable") + val pkColumn1 = Column("col_pk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val pkColumn2 = Column("col_pk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val targetTable = Table(targetTableId, setOf(pkColumn1, pkColumn2), emptySet()) + + // First row in target table + val uniqueId0 = 1L + val pkGene1_1 = SqlPrimaryKeyGene("col_pk1", targetTableId, IntegerGene("col_pk1", 1), uniqueId0) + val pkGene1_2 = SqlPrimaryKeyGene("col_pk2", targetTableId, IntegerGene("col_pk2", 10), uniqueId0) + val action1 = SqlAction(targetTable, setOf(pkColumn1, pkColumn2), uniqueId0, listOf(pkGene1_1, pkGene1_2)) + + // Second row in target table + val uniqueId1 = 2L + val pkGene2_1 = SqlPrimaryKeyGene("col_pk1", targetTableId, IntegerGene("col_pk1", 2), uniqueId1) + val pkGene2_2 = SqlPrimaryKeyGene("col_pk2", targetTableId, IntegerGene("col_pk2", 20), uniqueId1) + val action2 = SqlAction(targetTable, setOf(pkColumn1, pkColumn2), uniqueId1, listOf(pkGene2_1, pkGene2_2)) + + val sourceTableId = TableId("sourceTable") + val fkColumn1 = Column("col_fk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fkColumn2 = Column("col_fk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey(listOf(fkColumn1, fkColumn2), targetTableId, listOf(pkColumn1, pkColumn2)) + val sourceTable = Table(sourceTableId, setOf(fkColumn1, fkColumn2), setOf(foreignKey)) + + // First row in source table + val uniqueId2 = 3L + val fkGene1 = SqlForeignKeyGene("col_fk1", uniqueId2, targetTableId, "col_pk1", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk2")) + val fkGene2 = SqlForeignKeyGene("col_fk2", uniqueId2, targetTableId, "col_pk2", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk1")) + val action3 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), uniqueId2, listOf(fkGene1, fkGene2)) + + RestIndividual( + resourceCalls = mutableListOf(), + sampleType = SampleType.RANDOM, + dbInitialization = mutableListOf(action1, action2, action3) + ) + + // Randomize FK genes + val randomness = Randomness() + randomness.updateSeed(42) + fkGene1.randomize(randomness, tryToForceNewValue = false) + fkGene2.randomize(randomness, tryToForceNewValue = false) + + assertTrue(SqlActionUtils.isValidActions(listOf(action1, action2, action3))) + } + + + + @Test + fun testRandomizeMultiColumnForeignKeyWithDifferentInsertions() { + val targetTableId = TableId("targetTable") + val pkColumn1 = Column("col_pk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val pkColumn2 = Column("col_pk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val targetTable = Table(targetTableId, setOf(pkColumn1, pkColumn2), emptySet()) + + // First row in target table + val uniqueId0 = 1L + val pkGene1_1 = SqlPrimaryKeyGene("col_pk1", targetTableId, IntegerGene("col_pk1", 1), uniqueId0) + val pkGene1_2 = SqlPrimaryKeyGene("col_pk2", targetTableId, IntegerGene("col_pk2", 10), uniqueId0) + val action1 = SqlAction(targetTable, setOf(pkColumn1, pkColumn2), uniqueId0, listOf(pkGene1_1, pkGene1_2)) + + // Second row in target table + val uniqueId1 = 2L + val pkGene2_1 = SqlPrimaryKeyGene("col_pk1", targetTableId, IntegerGene("col_pk1", 2), uniqueId1) + val pkGene2_2 = SqlPrimaryKeyGene("col_pk2", targetTableId, IntegerGene("col_pk2", 20), uniqueId1) + val action2 = SqlAction(targetTable, setOf(pkColumn1, pkColumn2), uniqueId1, listOf(pkGene2_1, pkGene2_2)) + + val sourceTableId = TableId("sourceTable") + val fkColumn1 = Column("col_fk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fkColumn2 = Column("col_fk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey(listOf(fkColumn1, fkColumn2), targetTableId, listOf(pkColumn1, pkColumn2)) + val sourceTable = Table(sourceTableId, setOf(fkColumn1, fkColumn2), setOf(foreignKey)) + + // First row in source table + val uniqueId2_multi = 3L + val fkGene1 = SqlForeignKeyGene("col_fk1", uniqueId2_multi, targetTableId, "col_pk1", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk2")) + val fkGene2 = SqlForeignKeyGene("col_fk2", uniqueId2_multi, targetTableId, "col_pk2", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk1")) + val action3 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), uniqueId2_multi, listOf(fkGene1, fkGene2)) + + // Second row in source table + val uniqueId3_multi = 4L + val fkGene3 = SqlForeignKeyGene("col_fk1", uniqueId3_multi, targetTableId, "col_pk1", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk2")) + val fkGene4 = SqlForeignKeyGene("col_fk2", uniqueId3_multi, targetTableId, "col_pk2", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk1")) + val action4 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), uniqueId3_multi, listOf(fkGene3, fkGene4)) + + RestIndividual( + resourceCalls = mutableListOf(), + sampleType = SampleType.RANDOM, + dbInitialization = mutableListOf(action1, action2, action3, action4) + ) + + // Randomize FK genes + val randomness = Randomness() + randomness.updateSeed(42) + fkGene1.randomize(randomness, tryToForceNewValue = false) + fkGene2.randomize(randomness, tryToForceNewValue = false) + fkGene3.randomize(randomness, tryToForceNewValue = false) + fkGene4.randomize(randomness, tryToForceNewValue = false) + + assertTrue(SqlActionUtils.isValidActions(listOf(action1, action2, action3, action4))) + } + + @Test + fun testTwoMultiColumnForeignKeysToSameTable() { + val targetTableId = TableId("targetTable") + val pkColumn1 = Column("col_pk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val pkColumn2 = Column("col_pk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val targetTable = Table(targetTableId, setOf(pkColumn1, pkColumn2), emptySet()) + + // Row 1 in target table + val uniqueIdTarget1 = 1L + val pkGene1_1 = SqlPrimaryKeyGene("col_pk1", targetTableId, IntegerGene("col_pk1", 1), uniqueIdTarget1) + val pkGene1_2 = SqlPrimaryKeyGene("col_pk2", targetTableId, IntegerGene("col_pk2", 10), uniqueIdTarget1) + val actionTarget1 = SqlAction(targetTable, setOf(pkColumn1, pkColumn2), uniqueIdTarget1, listOf(pkGene1_1, pkGene1_2)) + + // Row 2 in target table + val uniqueIdTarget2 = 2L + val pkGene2_1 = SqlPrimaryKeyGene("col_pk1", targetTableId, IntegerGene("col_pk1", 2), uniqueIdTarget2) + val pkGene2_2 = SqlPrimaryKeyGene("col_pk2", targetTableId, IntegerGene("col_pk2", 20), uniqueIdTarget2) + val actionTarget2 = SqlAction(targetTable, setOf(pkColumn1, pkColumn2), uniqueIdTarget2, listOf(pkGene2_1, pkGene2_2)) + + val sourceTableId = TableId("sourceTable") + val fk1_1 = Column("fk1_1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fk1_2 = Column("fk1_2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fk2_1 = Column("fk2_1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fk2_2 = Column("fk2_2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + + val foreignKey1 = ForeignKey(listOf(fk1_1, fk1_2), targetTableId, listOf(pkColumn1, pkColumn2)) + val foreignKey2 = ForeignKey(listOf(fk2_1, fk2_2), targetTableId, listOf(pkColumn1, pkColumn2)) + + val sourceTable = Table(sourceTableId, setOf(fk1_1, fk1_2, fk2_1, fk2_2), setOf(foreignKey1, foreignKey2)) + + // A single row in source table that points to TWO different rows in target table + val uniqueIdSource = 3L + val fkGene1_1 = SqlForeignKeyGene("fk1_1", uniqueIdSource, targetTableId, "col_pk1", nullable = false, otherSourceColumnsInCompositeFK = listOf("fk1_2")) + val fkGene1_2 = SqlForeignKeyGene("fk1_2", uniqueIdSource, targetTableId, "col_pk2", nullable = false, otherSourceColumnsInCompositeFK = listOf("fk1_1")) + val fkGene2_1 = SqlForeignKeyGene("fk2_1", uniqueIdSource, targetTableId, "col_pk1", nullable = false, otherSourceColumnsInCompositeFK = listOf("fk2_2")) + val fkGene2_2 = SqlForeignKeyGene("fk2_2", uniqueIdSource, targetTableId, "col_pk2", nullable = false, otherSourceColumnsInCompositeFK = listOf("fk2_1")) + + val actionSource = SqlAction(sourceTable, setOf(fk1_1, fk1_2, fk2_1, fk2_2), uniqueIdSource, + listOf(fkGene1_1, fkGene1_2, fkGene2_1, fkGene2_2)) + + RestIndividual( + resourceCalls = mutableListOf(), + sampleType = SampleType.RANDOM, + dbInitialization = mutableListOf(actionTarget1, actionTarget2, actionSource) + ) + + // For a multi-column FK, both genes in the same FK must point to the same row in the target table. + // But the two different FKs (FK1 and FK2) can point to different rows. + + // We manually bind them to different rows to test the specific scenario + fkGene1_1.uniqueIdOfPrimaryKey = uniqueIdTarget1 + fkGene1_2.uniqueIdOfPrimaryKey = uniqueIdTarget1 + + fkGene2_1.uniqueIdOfPrimaryKey = uniqueIdTarget2 + fkGene2_2.uniqueIdOfPrimaryKey = uniqueIdTarget2 + + assertTrue(SqlActionUtils.isValidActions(listOf(actionTarget1, actionTarget2, actionSource))) + + // Now test that randomization also works and keeps it valid + val randomness = Randomness() + randomness.updateSeed(42) + fkGene1_1.randomize(randomness, tryToForceNewValue = false) + fkGene1_2.randomize(randomness, tryToForceNewValue = false) + fkGene2_1.randomize(randomness, tryToForceNewValue = false) + fkGene2_2.randomize(randomness, tryToForceNewValue = false) + + assertTrue(SqlActionUtils.isValidActions(listOf(actionTarget1, actionTarget2, actionSource))) + } + + @Test + fun testTwoDifferentForeignKeysToSameTableInSameAction() { + val targetTableId = TableId("targetTable") + val pkColumn = Column("col_pk", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val targetTable = Table(targetTableId, setOf(pkColumn), emptySet()) + + // Row 1 in target table + val uniqueIdTarget1 = 1L + val pkGene1 = SqlPrimaryKeyGene("col_pk", targetTableId, IntegerGene("col_pk", 1), uniqueIdTarget1) + val actionTarget1 = SqlAction(targetTable, setOf(pkColumn), uniqueIdTarget1, listOf(pkGene1)) + + // Row 2 in target table + val uniqueIdTarget2 = 2L + val pkGene2 = SqlPrimaryKeyGene("col_pk", targetTableId, IntegerGene("col_pk", 2), uniqueIdTarget2) + val actionTarget2 = SqlAction(targetTable, setOf(pkColumn), uniqueIdTarget2, listOf(pkGene2)) + + val sourceTableId = TableId("sourceTable") + val fk1 = Column("fk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fk2 = Column("fk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + + val foreignKey1 = ForeignKey(listOf(fk1), targetTableId, listOf(pkColumn)) + val foreignKey2 = ForeignKey(listOf(fk2), targetTableId, listOf(pkColumn)) + + val sourceTable = Table(sourceTableId, setOf(fk1, fk2), setOf(foreignKey1, foreignKey2)) + + val uniqueIdSource = 3L + val fkGene1 = SqlForeignKeyGene("fk1", uniqueIdSource, targetTableId, "col_pk", nullable = false, otherSourceColumnsInCompositeFK = emptyList()) + val fkGene2 = SqlForeignKeyGene("fk2", uniqueIdSource, targetTableId, "col_pk", nullable = false, otherSourceColumnsInCompositeFK = emptyList()) + + val actionSource = SqlAction(sourceTable, setOf(fk1, fk2), uniqueIdSource, listOf(fkGene1, fkGene2)) + + RestIndividual( + resourceCalls = mutableListOf(), + sampleType = SampleType.RANDOM, + dbInitialization = mutableListOf(actionTarget1, actionTarget2, actionSource) + ) + + // Bind them to DIFFERENT rows + fkGene1.uniqueIdOfPrimaryKey = uniqueIdTarget1 + fkGene2.uniqueIdOfPrimaryKey = uniqueIdTarget2 + + // Verify initial state is valid + assertTrue(SqlActionUtils.isValidActions(listOf(actionTarget1, actionTarget2, actionSource))) + + val randomness = Randomness() + randomness.updateSeed(42) + + // Randomize fkGene1. It should NOT force fkGene2 to change (if fkGene2 was already bound) + // OR more importantly, if we randomize fkGene2, it should NOT be forced to point to whatever fkGene1 points to. + + // Let's randomize fkGene2. Because it's "bound" (to uniqueIdTarget2), and fkGene1 is also bound (to uniqueIdTarget1). + // In the current BUGGY implementation, fkGene2.randomize() will find fkGene1 in 'otherFksInSameAction', + // see it's bound to uniqueIdTarget1, and FORCE fkGene2 to also point to uniqueIdTarget1. + + fkGene2.randomize(randomness, tryToForceNewValue = false) + + // If the bug exists, fkGene2.uniqueIdOfPrimaryKey will now be equal to fkGene1.uniqueIdOfPrimaryKey (uniqueIdTarget1) + // But they are independent FKs, so they SHOULD be allowed to point to different PKs. + // Actually, randomize() might choose a new value if it wasn't forced, but here it's forced by 'alreadyBoundId' + + assertNotEquals(fkGene1.uniqueIdOfPrimaryKey, fkGene2.uniqueIdOfPrimaryKey, + "Independent FKs in the same action should NOT be forced to point to the same PK") + } + + @Test + fun testIsReferenceToNonPrintableMultiColumnForeignKey() { + val targetTableId = TableId("targetTable") + // Column 1 is printable, Column 2 is non-printable (AutoIncrement) + val pkColumn1 = Column("col_pk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val pkColumn2 = Column("col_pk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val targetTable = Table(targetTableId, setOf(pkColumn1, pkColumn2), emptySet()) + + // First row in target table + val uniqueId0 = 1L + val pkGene1_1 = SqlPrimaryKeyGene("col_pk1", targetTableId, IntegerGene("col_pk1", 1), uniqueId0) + val pkGene1_2 = SqlPrimaryKeyGene("col_pk2", targetTableId, SqlAutoIncrementGene("col_pk2"), uniqueId0) + val action1 = SqlAction(targetTable, setOf(pkColumn1, pkColumn2), uniqueId0, listOf(pkGene1_1, pkGene1_2)) + + val sourceTableId = TableId("sourceTable") + val fkColumn1 = Column("col_fk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fkColumn2 = Column("col_fk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey(listOf(fkColumn1, fkColumn2), targetTableId, listOf(pkColumn1, pkColumn2)) + val sourceTable = Table(sourceTableId, setOf(fkColumn1, fkColumn2), setOf(foreignKey)) + + // First row in source table + val uniqueId2 = 3L + val fkGene1 = SqlForeignKeyGene("col_fk1", uniqueId2, targetTableId, "col_pk1", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk2")) + val fkGene2 = SqlForeignKeyGene("col_fk2", uniqueId2, targetTableId, "col_pk2", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk1")) + val action3 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), uniqueId2, listOf(fkGene1, fkGene2)) + + val previousGenes = listOf(pkGene1_1, pkGene1_2, fkGene1, fkGene2) + + // Bind FK genes to PK genes with uniqueId0 + fkGene1.uniqueIdOfPrimaryKey = uniqueId0 + fkGene2.uniqueIdOfPrimaryKey = uniqueId0 + + // fkGene1 points to pkGene1_1 which is printable (IntegerGene) + assertFalse(fkGene1.isReferenceToNonPrintable(previousGenes)) + // fkGene2 points to pkGene1_2 which is non-printable (SqlAutoIncrementGene) + assertTrue(fkGene2.isReferenceToNonPrintable(previousGenes)) + + // Unbind them + fkGene1.uniqueIdOfPrimaryKey = -1 + fkGene2.uniqueIdOfPrimaryKey = -1 + assertFalse(fkGene1.isReferenceToNonPrintable(previousGenes)) + assertFalse(fkGene2.isReferenceToNonPrintable(previousGenes)) + } + + @Test + fun testGetValueAsPrintableStringMultiColumnForeignKey() { + val targetTableId = TableId("targetTable") + val pkColumn1 = Column("col_pk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val pkColumn2 = Column("col_pk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, primaryKey = true) + val targetTable = Table(targetTableId, setOf(pkColumn1, pkColumn2), emptySet()) + + // First row in target table + val uniqueId0 = 1L + val pkGene1_1 = SqlPrimaryKeyGene("col_pk1", targetTableId, IntegerGene("col_pk1", 1), uniqueId0) + val pkGene1_2 = SqlPrimaryKeyGene("col_pk2", targetTableId, IntegerGene("col_pk2", 10), uniqueId0) + val action1 = SqlAction(targetTable, setOf(pkColumn1, pkColumn2), uniqueId0, listOf(pkGene1_1, pkGene1_2)) + + val sourceTableId = TableId("sourceTable") + val fkColumn1 = Column("col_fk1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val fkColumn2 = Column("col_fk2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey(listOf(fkColumn1, fkColumn2), targetTableId, listOf(pkColumn1, pkColumn2)) + val sourceTable = Table(sourceTableId, setOf(fkColumn1, fkColumn2), setOf(foreignKey)) + + // First row in source table + val uniqueId2 = 3L + val fkGene1 = SqlForeignKeyGene("col_fk1", uniqueId2, targetTableId, "col_pk1", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk2")) + val fkGene2 = SqlForeignKeyGene("col_fk2", uniqueId2, targetTableId, "col_pk2", nullable = false, otherSourceColumnsInCompositeFK = listOf("col_fk1")) + val action3 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), uniqueId2, listOf(fkGene1, fkGene2)) + + val ind = RestIndividual( + resourceCalls = mutableListOf(), + sampleType = SampleType.RANDOM, + dbInitialization = mutableListOf(action1, action3) + ) + + // Bind FK genes to PK genes with uniqueId0 + fkGene1.uniqueIdOfPrimaryKey = uniqueId0 + fkGene2.uniqueIdOfPrimaryKey = uniqueId0 + + val previousGenes = listOf(pkGene1_1, pkGene1_2, fkGene1, fkGene2) + + assertEquals("1", fkGene1.getValueAsPrintableString(previousGenes, null, null)) + assertEquals("10", fkGene2.getValueAsPrintableString(previousGenes, null, null)) + } + +} diff --git a/core/src/test/kotlin/org/evomaster/core/search/gene/sql/SqlPrimaryKeyGeneTest.kt b/core/src/test/kotlin/org/evomaster/core/search/gene/sql/SqlPrimaryKeyGeneTest.kt new file mode 100644 index 0000000000..fa2729de14 --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/search/gene/sql/SqlPrimaryKeyGeneTest.kt @@ -0,0 +1,146 @@ +package org.evomaster.core.search.gene.sql + +import org.evomaster.core.problem.enterprise.EnterpriseChildTypeVerifier +import org.evomaster.core.problem.enterprise.EnterpriseIndividual +import org.evomaster.core.problem.enterprise.SampleType +import org.evomaster.core.search.action.ActionComponent +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.sql.SqlAction +import org.evomaster.core.sql.schema.Table +import org.evomaster.core.sql.schema.TableId +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class SqlPrimaryKeyGeneTest { + + @Test + fun testConstructorAndProperties() { + val tableId = TableId("my_table") + val innerGene = IntegerGene("foo", 42) + val pk = SqlPrimaryKeyGene("pk", tableId, innerGene, 1L) + + assertEquals("pk", pk.name) + assertEquals(tableId, pk.tableName) + assertEquals(innerGene, pk.gene) + assertEquals(1L, pk.uniqueId) + } + + @Test + fun testNegativeUniqueIdThrowsException() { + val tableId = TableId("my_table") + val innerGene = IntegerGene("foo", 42) + assertThrows(IllegalArgumentException::class.java) { + SqlPrimaryKeyGene("pk", tableId, innerGene, -1L) + } + } + + @Test + fun testCopy() { + val tableId = TableId("my_table") + val innerGene = IntegerGene("foo", 42) + val pk = SqlPrimaryKeyGene("pk", tableId, innerGene, 1L) + + val copy = pk.copy() as SqlPrimaryKeyGene + + assertNotSame(pk, copy) + assertNotSame(pk.gene, copy.gene) + assertEquals(pk.name, copy.name) + assertEquals(pk.tableName, copy.tableName) + assertEquals(pk.uniqueId, copy.uniqueId) + assertEquals((pk.gene as IntegerGene).value, (copy.gene as IntegerGene).value) + } + + @Test + fun testContainsSameValueAs() { + val tableId = TableId("table") + val pk1 = SqlPrimaryKeyGene("pk", tableId, IntegerGene("foo", 1), 1L) + val pk2 = SqlPrimaryKeyGene("pk", tableId, IntegerGene("foo", 1), 2L) // different uniqueId doesn't matter for value + val pk3 = SqlPrimaryKeyGene("pk", tableId, IntegerGene("foo", 2), 1L) + + assertTrue(pk1.containsSameValueAs(pk2)) + assertFalse(pk1.containsSameValueAs(pk3)) + + assertThrows(IllegalArgumentException::class.java) { + pk1.containsSameValueAs(IntegerGene("foo", 1)) + } + } + + @Test + fun testUnsafeCopyValueFrom() { + val tableId = TableId("table") + val pk1 = SqlPrimaryKeyGene("pk", tableId, IntegerGene("foo", 1), 1L) + val pk2 = SqlPrimaryKeyGene("pk", tableId, IntegerGene("foo", 2), 2L) + + pk1.unsafeCopyValueFrom(pk2) + assertEquals(2, (pk1.gene as IntegerGene).value) + + assertFalse(pk1.unsafeCopyValueFrom(IntegerGene("foo", 3))) + } + + @Test + fun testPrintableAndRawStrings() { + val tableId = TableId("table") + val innerGene = IntegerGene("foo", 123) + val pk = SqlPrimaryKeyGene("pk", tableId, innerGene, 1L) + + assertEquals("123", pk.getValueAsPrintableString()) + assertEquals("123", pk.getValueAsRawString()) + assertEquals(innerGene.getVariableName(), pk.getVariableName()) + } + + @Test + fun testGetWrappedAndLeafGene() { + val tableId = TableId("table") + val innerGene = IntegerGene("foo", 1) + val pk = SqlPrimaryKeyGene("pk", tableId, innerGene, 1L) + + assertEquals(pk, pk.getWrappedGene(SqlPrimaryKeyGene::class.java)) + assertEquals(innerGene, pk.getWrappedGene(IntegerGene::class.java)) + assertEquals(innerGene, pk.getLeafGene()) + } + + @Test + fun testCheckForGloballyValid() { + val table = Table(TableId("t"), emptySet(), emptySet()) + val pk = SqlPrimaryKeyGene("pk", table.id, IntegerGene("foo", 1), 1L) + + // Not mounted + + assertFalse(pk.isGloballyValid()) + + val action = SqlAction(table, emptySet(), 1L, listOf(pk)) + + // Now it is mounted and insertionId matches uniqueId + assertTrue(pk.isGloballyValid()) + } + + class TestIndividual(children: MutableList) : EnterpriseIndividual( + sampleTypeField = SampleType.RANDOM, + children = children, + childTypeVerifier = EnterpriseChildTypeVerifier(SqlAction::class.java), + groups = getEnterpriseTopGroups(children, 0, children.size, 0, 0, 0, 0) + ) + + @Test + fun testShiftIdByUpdatesFKs() { + val tableId = TableId("table") + val pk = SqlPrimaryKeyGene("pk", tableId, IntegerGene("id", 1), 10L) + + val fk1 = SqlForeignKeyGene("fk", 100L, tableId, "id", false, 10L) // bound to pk + val fk2 = SqlForeignKeyGene("fk2", 101L, tableId, "id", false, 20L) // bound to something else + val fk3 = SqlForeignKeyGene("fk3", 102L, tableId, "id", false, -1L) // unbound + + val table = Table(tableId, emptySet(), emptySet()) + val action1 = SqlAction(table, emptySet(), 10L, listOf(pk)) + val action2 = SqlAction(table, emptySet(), 11L, listOf(fk1, fk2, fk3)) + + TestIndividual(mutableListOf(action1, action2)) + + pk.shiftIdBy(5L) + + assertEquals(15L, pk.uniqueId) + assertEquals(15L, fk1.uniqueIdOfPrimaryKey) + assertEquals(20L, fk2.uniqueIdOfPrimaryKey) + assertEquals(-1L, fk3.uniqueIdOfPrimaryKey) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/search/impact/impactinfocollection/sql/SqlForeignKeyGeneImpactTest.kt b/core/src/test/kotlin/org/evomaster/core/search/impact/impactinfocollection/sql/SqlForeignKeyGeneImpactTest.kt index d457a5ebd7..472df69ace 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/impact/impactinfocollection/sql/SqlForeignKeyGeneImpactTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/impact/impactinfocollection/sql/SqlForeignKeyGeneImpactTest.kt @@ -1,5 +1,6 @@ package org.evomaster.core.search.impact.impactinfocollection.sql +import org.evomaster.core.sql.schema.TableId import org.evomaster.core.search.gene.Gene import org.evomaster.core.search.gene.sql.SqlForeignKeyGene import org.evomaster.core.search.impact.impactinfocollection.GeneImpact @@ -25,7 +26,8 @@ class SqlForeignKeyGeneImpactTest : GeneImpactTest() { override fun getGene(): Gene = SqlForeignKeyGene( sourceColumn = "source", uniqueId = 1L, - targetTable = "fake", + targetTable = TableId("fake"), + targetColumn = "id", nullable = false, uniqueIdOfPrimaryKey = 1L ) @@ -34,4 +36,4 @@ class SqlForeignKeyGeneImpactTest : GeneImpactTest() { assert(impact is SqlForeignKeyGeneImpact) } -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/org/evomaster/core/search/structuralelement/gene/SqlGeneStructureTest.kt b/core/src/test/kotlin/org/evomaster/core/search/structuralelement/gene/SqlGeneStructureTest.kt index 410fd46ce4..6b4f86eaa5 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/structuralelement/gene/SqlGeneStructureTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/structuralelement/gene/SqlGeneStructureTest.kt @@ -5,6 +5,7 @@ import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.numeric.LongGene import org.evomaster.core.search.gene.wrapper.OptionalGene import org.evomaster.core.search.gene.wrapper.NullableGene +import org.evomaster.core.sql.schema.TableId import org.evomaster.core.search.gene.sql.* import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -29,14 +30,14 @@ class SqlForeignKeyGeneStructureTest : GeneStructuralElementBaseTest() { override fun throwExceptionInRandomnessTest(): Boolean = false - override fun getCopyFromTemplate(): Gene = SqlForeignKeyGene("id",1L, "table", false, 1L) + override fun getCopyFromTemplate(): Gene = SqlForeignKeyGene("id",1L, TableId("table"), "id", false, 1L) override fun assertCopyFrom(base: Gene) { assertTrue(base is SqlForeignKeyGene) assertEquals(1L, (base as SqlForeignKeyGene).uniqueIdOfPrimaryKey) } - override fun getStructuralElement(): SqlForeignKeyGene = SqlForeignKeyGene("id",1L, "table", false, 0L) + override fun getStructuralElement(): SqlForeignKeyGene = SqlForeignKeyGene("id",1L, TableId("table"), "id", false, 0L) override fun getExpectedChildrenSize(): Int = 0 } diff --git a/core/src/test/kotlin/org/evomaster/core/search/structuralelement/individual/RestIndividualStructureTest.kt b/core/src/test/kotlin/org/evomaster/core/search/structuralelement/individual/RestIndividualStructureTest.kt index 8a0c94076b..ab32178442 100644 --- a/core/src/test/kotlin/org/evomaster/core/search/structuralelement/individual/RestIndividualStructureTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/search/structuralelement/individual/RestIndividualStructureTest.kt @@ -11,6 +11,7 @@ import org.evomaster.core.problem.enterprise.SampleType import org.evomaster.core.problem.rest.param.BodyParam import org.evomaster.core.search.gene.numeric.IntegerGene import org.evomaster.core.search.gene.ObjectGene +import org.evomaster.core.sql.schema.TableId import org.evomaster.core.search.gene.sql.SqlForeignKeyGene import org.evomaster.core.search.gene.sql.SqlPrimaryKeyGene import org.evomaster.core.search.service.SearchGlobalState @@ -43,7 +44,7 @@ class RestIndividualStructureTest : StructuralElementBaseTest(){ unique = false, databaseType = DatabaseType.H2) - val foreignKey = ForeignKey(sourceColumns = setOf(fkColumn), targetTableId = foo.id) + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = foo.id, listOf(idColumn)) val bar = Table("Bar", setOf(barIdColumn, fkColumn), setOf(foreignKey)) @@ -53,7 +54,7 @@ class RestIndividualStructureTest : StructuralElementBaseTest(){ val insertId1 = 1002L val pkGeneBar = SqlPrimaryKeyGene("Id", "Bar", IntegerGene("Id", 2, 0, 10), insertId0) - val fkGene = SqlForeignKeyGene("fooId", insertId1, "Foo", false, insertId0) + val fkGene = SqlForeignKeyGene("fooId", insertId1, TableId("Foo"), "id", false, insertId0) val action1 = SqlAction(bar, setOf(barIdColumn, fkColumn), insertId1, listOf(pkGeneBar, fkGene)) diff --git a/core/src/test/kotlin/org/evomaster/core/sql/DbActionGeneBuilderTest.kt b/core/src/test/kotlin/org/evomaster/core/sql/DbActionGeneBuilderTest.kt index 8f1b9f8937..b61d2fcb62 100644 --- a/core/src/test/kotlin/org/evomaster/core/sql/DbActionGeneBuilderTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/sql/DbActionGeneBuilderTest.kt @@ -6,10 +6,49 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource +import org.evomaster.core.search.gene.sql.SqlForeignKeyGene +import org.evomaster.core.sql.schema.* +import org.junit.jupiter.api.Test import java.util.regex.Pattern class DbActionGeneBuilderTest { + @Test + fun testMultiColumnForeignKey() { + val foreignKeyColumn1 = Column("fkCol1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val foreignKeyColumn2 = Column("fkCol2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + + val targetColumn1 = Column("targetColumn1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + val targetColumn2 = Column("targetColumn2", ColumnDataType.INTEGER, databaseType = DatabaseType.H2) + + val targetTableId = TableId("target_table") + + val fk = ForeignKey( + sourceColumns = listOf(foreignKeyColumn1, foreignKeyColumn2), + targetTableId = targetTableId, + targetColumns = listOf(targetColumn1, targetColumn2) + ) + + val table = Table( + id = TableId("source_table"), + columns = setOf(foreignKeyColumn1, foreignKeyColumn2), + foreignKeys = setOf(fk) + ) + + val builder = SqlActionGeneBuilder() + + val gene1 = builder.buildGene(1L, table, foreignKeyColumn1) as SqlForeignKeyGene + val gene2 = builder.buildGene(1L, table, foreignKeyColumn2) as SqlForeignKeyGene + + assertEquals("fkCol1",gene1.name) + assertEquals(targetTableId, gene1.targetTable) + assertEquals("targetColumn1", gene1.targetColumn) + + assertEquals("fkCol2",gene2.name) + assertEquals(targetTableId, gene2.targetTable) + assertEquals("targetColumn2", gene2.targetColumn) + } + @ParameterizedTest @EnumSource(value = DatabaseType::class, names = ["H2", "MYSQL", "POSTGRES"]) fun testSimilarToBuilder(databaseType: DatabaseType ) { diff --git a/core/src/test/kotlin/org/evomaster/core/sql/DbActionUtilsTest.kt b/core/src/test/kotlin/org/evomaster/core/sql/DbActionUtilsTest.kt index e5ba8c384e..bbb246310b 100644 --- a/core/src/test/kotlin/org/evomaster/core/sql/DbActionUtilsTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/sql/DbActionUtilsTest.kt @@ -416,7 +416,7 @@ class DbActionUtilsTest { unique = false, databaseType = DatabaseType.H2) - val foreignKey = ForeignKey(sourceColumns = setOf(fkColumn), targetTableId = table0.id) + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = table0.id, targetColumns = listOf(idColumn)) val table1 = Table("Table1", setOf(fkColumn), setOf(foreignKey)) @@ -428,7 +428,7 @@ class DbActionUtilsTest { val action0 = SqlAction(table0, setOf(idColumn), insertId0, listOf(pkGeneTable0)) val insertId1 = 1002L - val fkGene = SqlForeignKeyGene("Id", insertId1, TableId("Table0"), false, insertId0) + val fkGene = SqlForeignKeyGene("Id", insertId1, TableId("Table0"), "Id", false, insertId0) val pkGeneTable1 = SqlPrimaryKeyGene("Id", "Table1", fkGene, insertId1) val action1 = SqlAction(table1, setOf(fkColumn), insertId1, listOf(pkGeneTable1)) @@ -460,7 +460,7 @@ class DbActionUtilsTest { unique = false, databaseType = DatabaseType.H2) - val foreignKey = ForeignKey(sourceColumns = setOf(fkColumn), targetTableId = table0.id) + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = table0.id, targetColumns = listOf(idColumn)) val table1 = Table("Table1", setOf(fkColumn), setOf(foreignKey)) @@ -471,7 +471,7 @@ class DbActionUtilsTest { val action0 = SqlAction(table0, setOf(idColumn), insertId0, listOf(pkGeneTable0)) val insertId1 = 1002L - val fkGene = SqlForeignKeyGene("Id", insertId1, TableId("Table0"), false, insertId0) + val fkGene = SqlForeignKeyGene("Id", insertId1, TableId("Table0"), "Id", false, insertId0) val pkGeneTable1 = SqlPrimaryKeyGene("Id", "Table1", fkGene, insertId1) val action1 = SqlAction(table1, setOf(fkColumn), insertId1, listOf(pkGeneTable1)) @@ -502,7 +502,7 @@ class DbActionUtilsTest { unique = false, databaseType = DatabaseType.H2) - val foreignKey = ForeignKey(sourceColumns = setOf(fkColumn), targetTableId = table0.id) + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = table0.id, targetColumns = listOf(idColumn)) val table1 = Table("Table1", setOf(fkColumn), setOf(foreignKey)) @@ -521,14 +521,14 @@ class DbActionUtilsTest { //PK on table1, with FK to table0 first PK val insertId2 = 1002L - val fkGene0 = SqlForeignKeyGene("Id", insertId2, TableId("Table0"), false, insertId0) + val fkGene0 = SqlForeignKeyGene("Id", insertId2, TableId("Table0"), "Id", false, insertId0) val pkGene2 = SqlPrimaryKeyGene("Id", "Table1", fkGene0, insertId2) val action2 = SqlAction(table1, setOf(fkColumn), insertId2, listOf(pkGene2)) //another PK on table1, with FK to same first PK in table0 //but this is technically invalid, as same column for FK is used as PK, so we have a duplicated PK in table1 val insertId3 = 1003L - val fkGene1 = SqlForeignKeyGene("Id", insertId3, TableId("Table0"), false, insertId0) + val fkGene1 = SqlForeignKeyGene("Id", insertId3, TableId("Table0"), "Id", false, insertId0) val pkGene3 = SqlPrimaryKeyGene("Id", "Table1", fkGene1, insertId3) val action3 = SqlAction(table1, setOf(fkColumn), insertId3, listOf(pkGene3)) @@ -562,7 +562,7 @@ class DbActionUtilsTest { autoIncrement = false, unique = false, databaseType = DatabaseType.H2) - val foreignKey = ForeignKey(sourceColumns = setOf(fkColumn), targetTableId = table0.id) + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = table0.id, targetColumns = listOf(idColumn)) val table1 = Table("Table1", setOf(fkColumn), setOf(foreignKey)) val set = listOf(table0, table0, table1) diff --git a/core/src/test/kotlin/org/evomaster/core/sql/SqlActionUtilsTest.kt b/core/src/test/kotlin/org/evomaster/core/sql/SqlActionUtilsTest.kt new file mode 100644 index 0000000000..3b767807fb --- /dev/null +++ b/core/src/test/kotlin/org/evomaster/core/sql/SqlActionUtilsTest.kt @@ -0,0 +1,468 @@ +package org.evomaster.core.sql + +import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType +import org.evomaster.core.search.gene.numeric.IntegerGene +import org.evomaster.core.search.gene.sql.SqlForeignKeyGene +import org.evomaster.core.search.gene.sql.SqlPrimaryKeyGene +import org.evomaster.core.sql.schema.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class SqlActionUtilsTest { + + @Test + fun testValidSingleColumnForeignKey() { + val idColumn = Column("Id", ColumnDataType.INTEGER, 10, + primaryKey = true, + autoIncrement = false, + unique = false, + databaseType = DatabaseType.H2) + + val targetTable = Table("Table0", setOf(idColumn), setOf()) + + val fkColumn = Column("Id", ColumnDataType.INTEGER, 10, + primaryKey = true, + autoIncrement = false, + unique = false, + databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = targetTable.id, targetColumns = listOf(idColumn)) + + val sourceTable = Table("Table1", setOf(fkColumn), setOf(foreignKey)) + + val integerGene = IntegerGene("Id", 42, 0, 100) + + + val insertId0 = 1001L + val pkGeneTable0 = SqlPrimaryKeyGene("Id", targetTable.id, integerGene, insertId0) + val action0 = SqlAction(targetTable, setOf(idColumn), insertId0, listOf(pkGeneTable0)) + + val insertId1 = 1002L + val fkGene = SqlForeignKeyGene("Id", insertId1, targetTable.id, "Id", false, insertId0) + val pkGeneTable1 = SqlPrimaryKeyGene("Id", sourceTable.id, fkGene, insertId1) + + val action1 = SqlAction(sourceTable, setOf(fkColumn), insertId1, listOf(pkGeneTable1)) + + + val actions = mutableListOf(action0, action1) + + assertTrue(SqlActionUtils.isValidForeignKeys(actions)) + } + + @Test + fun testValidMultiColumnForeignKeys() { + val idColumn1 = Column("Id1", ColumnDataType.INTEGER, 10, + primaryKey = true, + autoIncrement = false, + unique = false, + databaseType = DatabaseType.H2) + + val idColumn2 = Column("Id2", ColumnDataType.INTEGER, 10, + primaryKey = true, + autoIncrement = false, + unique = false, + databaseType = DatabaseType.H2) + + val targetTable = Table("Table0", setOf(idColumn1, idColumn2), setOf()) + + val fkColumn1 = Column("Fk1", ColumnDataType.INTEGER, 10, + primaryKey = false, + autoIncrement = false, + unique = false, + databaseType = DatabaseType.H2) + + val fkColumn2 = Column("Fk2", ColumnDataType.INTEGER, 10, + primaryKey = false, + autoIncrement = false, + unique = false, + databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey( + sourceColumns = listOf(fkColumn1, fkColumn2), + targetTableId = targetTable.id, + targetColumns = listOf(idColumn1, idColumn2) + ) + + val sourceTable = Table("Table1", setOf(fkColumn1, fkColumn2), setOf(foreignKey)) + + val integerGene1 = IntegerGene("Id1", 42, 0, 100) + val integerGene2 = IntegerGene("Id2", 84, 0, 100) + + val insertId0 = 1001L + val pkGene1Table0 = SqlPrimaryKeyGene("Id1", targetTable.id, integerGene1, insertId0) + val pkGene2Table0 = SqlPrimaryKeyGene("Id2", targetTable.id, integerGene2, insertId0) + val action0 = SqlAction(targetTable, setOf(idColumn1, idColumn2), insertId0, listOf(pkGene1Table0, pkGene2Table0)) + + val insertId1 = 1002L + val fkGene1 = SqlForeignKeyGene("Fk1", insertId1, targetTable.id, "Id1", false, insertId0) + val fkGene2 = SqlForeignKeyGene("Fk2", insertId1, targetTable.id, "Id2", false, insertId0) + + val action1 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), insertId1, listOf(fkGene1, fkGene2)) + + val actions = mutableListOf(action0, action1) + + assertTrue(SqlActionUtils.isValidForeignKeys(actions)) + } + + @Test + fun testInvalidMultiColumnForeignKeys() { + val idColumn1 = Column("Id1", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val idColumn2 = Column("Id2", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val targetTable = Table("Table0", setOf(idColumn1, idColumn2), setOf()) + + val fkColumn1 = Column("Fk1", ColumnDataType.INTEGER, 10, + databaseType = DatabaseType.H2) + + val fkColumn2 = Column("Fk2", ColumnDataType.INTEGER, 10, + databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey( + sourceColumns = listOf(fkColumn1, fkColumn2), + targetTableId = targetTable.id, + targetColumns = listOf(idColumn1, idColumn2) + ) + + val sourceTable = Table("Table1", setOf(fkColumn1, fkColumn2), setOf(foreignKey)) + + val integerGene0 = IntegerGene("Id1", 21, 0, 100) + val integerGene1 = IntegerGene("Id1", 42, 0, 100) + val integerGene2 = IntegerGene("Id2", 84, 0, 100) + + // Inserts (21,84) to Table0(Id1,Id2) + val insertId0 = 1001L + val pkGene1Table0_0 = SqlPrimaryKeyGene("Id1", targetTable.id, integerGene0, insertId0) + val pkGene2Table0_0 = SqlPrimaryKeyGene("Id2", targetTable.id, integerGene2, insertId0) + val action0 = SqlAction(targetTable, setOf(idColumn1, idColumn2), insertId0, listOf(pkGene1Table0_0, pkGene2Table0_0)) + + // Inserts (42,84) to Table0(Id1,Id2) + val insertId1 = 1002L + val pkGene1Table0_1 = SqlPrimaryKeyGene("Id1", targetTable.id, integerGene1, insertId1) + val pkGene2Table0_1 = SqlPrimaryKeyGene("Id2", targetTable.id, integerGene2, insertId1) + val action1 = SqlAction(targetTable, setOf(idColumn1, idColumn2), insertId1, listOf(pkGene1Table0_1, pkGene2Table0_1)) + + val insertId2 = 1003L + // Points to DIFFERENT but VALID PK IDs (1001L and 1002L) + val fkGene1 = SqlForeignKeyGene("Fk1", insertId2, targetTable.id, "Id1", false, insertId0) + val fkGene2 = SqlForeignKeyGene("Fk2", insertId2, targetTable.id, "Id2", false, insertId1) + + val action2 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), insertId2, listOf(fkGene1, fkGene2)) + + val actions = mutableListOf(action0, action1, action2) + + assertFalse(SqlActionUtils.isValidForeignKeys(actions)) + } + + @Test + fun testInvalidForeignKeyReferringToWrongTable() { + val idColumnTable0 = Column("Id0", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val table0 = Table("Table0", setOf(idColumnTable0), setOf()) + + val idColumnTable1 = Column("Id1", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val table1 = Table("Table1", setOf(idColumnTable1), setOf()) + + val fkColumn = Column("Fk", ColumnDataType.INTEGER, 10, + databaseType = DatabaseType.H2) + + // FK in Table2 points to Table1 + val foreignKey = ForeignKey( + sourceColumns = listOf(fkColumn), + targetTableId = table1.id, + targetColumns = listOf(idColumnTable1) + ) + + val table2 = Table("Table2", setOf(fkColumn), setOf(foreignKey)) + + val integerGene0 = IntegerGene("Id0", 1, 0, 100) + + // Action 0: Insert into Table0 (ID = 1, uniqueId = 1001L) + val insertId0 = 1001L + val pkGeneTable0 = SqlPrimaryKeyGene("Id0", table0.id, integerGene0, insertId0) + val action0 = SqlAction(table0, setOf(idColumnTable0), insertId0, listOf(pkGeneTable0)) + + // Action 1: Insert into Table2, but FK points to uniqueId = 1001L (which belongs to Table0, NOT Table1) + val insertId1 = 1002L + val fkGene = SqlForeignKeyGene("Fk", insertId1, table1.id, "Id1", false, insertId0) + + val action1 = SqlAction(table2, setOf(fkColumn), insertId1, listOf(fkGene)) + + val actions = mutableListOf(action0, action1) + + // This SHOULD be false, because the FK points to an insertion in Table0, but the constraint says it should point to Table1 + assertFalse(SqlActionUtils.isValidForeignKeys(actions)) + } + + @Test + fun testValidForeignKeyInsidePrimaryKey() { + // Table 0 (Target): PK "Id" + val idColumnT0 = Column("Id", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + val table0 = Table("Table0", setOf(idColumnT0), setOf()) + + // Table 1 (Source): PK "Id" which is also FK to Table 0 "Id" + val idColumnT1 = Column("Id", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + val foreignKey = ForeignKey( + sourceColumns = listOf(idColumnT1), + targetTableId = table0.id, + targetColumns = listOf(idColumnT0) + ) + val table1 = Table("Table1", setOf(idColumnT1), setOf(foreignKey)) + + // Action 0: Insert into Table 0 + val insertId0 = 1001L + val integerGene = IntegerGene("Id", 42, 0, 100) + val pkGeneT0 = SqlPrimaryKeyGene("Id", table0.id, integerGene, insertId0) + val action0 = SqlAction(table0, setOf(idColumnT0), insertId0, listOf(pkGeneT0)) + + // Action 1: Insert into Table 1, where its PK wraps an FK to Table 0 + val insertId1 = 1002L + val fkGene = SqlForeignKeyGene("Id", insertId1, table0.id, "Id", false, insertId0) + val pkGeneT1 = SqlPrimaryKeyGene("Id", table1.id, fkGene, insertId1) + val action1 = SqlAction(table1, setOf(idColumnT1), insertId1, listOf(pkGeneT1)) + + val actions = mutableListOf(action0, action1) + + assertTrue(SqlActionUtils.isValidForeignKeys(actions)) + } + + @Test + fun testValidNullableUnboundForeignKey() { + val idColumn = Column("Id", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val targetTable = Table("Table0", setOf(idColumn), setOf()) + + val fkColumn = Column("FkId", ColumnDataType.INTEGER, 10, + nullable = true, + databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = targetTable.id, targetColumns = listOf(idColumn)) + + val sourceTable = Table("Table1", setOf(fkColumn), setOf(foreignKey)) + + val insertId1 = 1002L + // Unbound FK gene (representing NULL) + val fkGene = SqlForeignKeyGene("FkId", insertId1, targetTable.id, "Id", true, -1L) + + val action1 = SqlAction(sourceTable, setOf(fkColumn), insertId1, listOf(fkGene)) + + val integerGene = IntegerGene("Id", 42, 0, 100) + val insertId0 = 1001L + val pkGeneTable0 = SqlPrimaryKeyGene("Id", targetTable.id, integerGene, insertId0) + val action0 = SqlAction(targetTable, setOf(idColumn), insertId0, listOf(pkGeneTable0)) + + val actions = mutableListOf(action0, action1) + + val result = SqlActionUtils.isValidForeignKeys(actions) + assertEquals(true, result) + } + + @Test + fun testValidMissingForeignKeyGene() { + + val idColumn = Column("Id", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val targetTable = Table("Table0", setOf(idColumn), setOf()) + + val integerGene = IntegerGene("Id", 42, 0, 100) + val insertId0 = 1001L + val pkGeneTable0 = SqlPrimaryKeyGene("Id", targetTable.id, integerGene, insertId0) + val action0 = SqlAction(targetTable, setOf(idColumn), insertId0, listOf(pkGeneTable0)) + + + val fkColumn = Column("FkId", ColumnDataType.INTEGER, 10, + nullable = true, + databaseType = DatabaseType.H2) + + val pkColumn = Column("SourceId", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + // The table HAS a foreign key constraint + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = targetTable.id, targetColumns = listOf(idColumn)) + + val sourceTable = Table("Table1", setOf(fkColumn, pkColumn), setOf(foreignKey)) + + val insertId1 = 1002L + val integerGene1 = IntegerGene("SourceId", 55, 0, 100) + val pkGeneTable1 = SqlPrimaryKeyGene("SourceId", sourceTable.id, integerGene1, insertId1) + + // Action 1 does NOT have the gene for FkId, even though the table has a FK constraint on it + val action1 = SqlAction(sourceTable, setOf(pkColumn), insertId1, listOf(pkGeneTable1)) + + + val actions = mutableListOf(action0, action1) + + val result = SqlActionUtils.isValidForeignKeys(actions) + + // A nullable missing column in the insertion action is considered a NULL value. + // NULL values are valid foreign keys values since they *disable* the foreign key constraint. + // Therefore, the FK constraint is valid. + assertEquals(true, result) + } + + @Test + fun testInvalidMissingNonNullableForeignKeyGene() { + + val idColumn = Column("Id", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val targetTable = Table("Table0", setOf(idColumn), setOf()) + + val integerGene = IntegerGene("Id", 42, 0, 100) + val insertId0 = 1001L + val pkGeneTable0 = SqlPrimaryKeyGene("Id", targetTable.id, integerGene, insertId0) + val action0 = SqlAction(targetTable, setOf(idColumn), insertId0, listOf(pkGeneTable0)) + + + val fkColumn = Column("FkId", ColumnDataType.INTEGER, 10, + nullable = false, + databaseType = DatabaseType.H2) + + val pkColumn = Column("SourceId", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + // The table HAS a foreign key constraint + val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = targetTable.id, targetColumns = listOf(idColumn)) + + val sourceTable = Table("Table1", setOf(fkColumn, pkColumn), setOf(foreignKey)) + + val insertId1 = 1002L + val integerGene1 = IntegerGene("SourceId", 55, 0, 100) + val pkGeneTable1 = SqlPrimaryKeyGene("SourceId", sourceTable.id, integerGene1, insertId1) + + // Action 1 does NOT have the gene for FkId, even though the table has a FK constraint on it + // and FkId is not nullable + val action1 = SqlAction(sourceTable, setOf(pkColumn), insertId1, listOf(pkGeneTable1)) + + + val actions = mutableListOf(action0, action1) + + val result = SqlActionUtils.isValidForeignKeys(actions) + + // A non-nullable missing column in the insertion action is invalid. + assertFalse(result) + } + + @Test + fun testValidPartialNullableMultiColumnForeignKey() { + val idColumn1 = Column("Id1", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val idColumn2 = Column("Id2", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val targetTable = Table("Table0", setOf(idColumn1, idColumn2), setOf()) + + val fkColumn1 = Column("Fk1", ColumnDataType.INTEGER, 10, + nullable = true, // One column is nullable + databaseType = DatabaseType.H2) + + val fkColumn2 = Column("Fk2", ColumnDataType.INTEGER, 10, + nullable = false, // The other column is not nullable + databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey( + sourceColumns = listOf(fkColumn1, fkColumn2), + targetTableId = targetTable.id, + targetColumns = listOf(idColumn1, idColumn2) + ) + + val sourceTable = Table("Table1", setOf(fkColumn1, fkColumn2), setOf(foreignKey)) + + val integerGene1 = IntegerGene("Id1", 42, 0, 100) + val integerGene2 = IntegerGene("Id2", 84, 0, 100) + + val insertId0 = 1001L + val pkGene1Table0 = SqlPrimaryKeyGene("Id1", targetTable.id, integerGene1, insertId0) + val pkGene2Table0 = SqlPrimaryKeyGene("Id2", targetTable.id, integerGene2, insertId0) + val action0 = SqlAction(targetTable, setOf(idColumn1, idColumn2), insertId0, listOf(pkGene1Table0, pkGene2Table0)) + + val insertId1 = 1002L + // Fk1 is NULL (unbound), Fk2 is bound to insertId0 + val fkGene1 = SqlForeignKeyGene("Fk1", insertId1, targetTable.id, "Id1", true, -1L) + val fkGene2 = SqlForeignKeyGene("Fk2", insertId1, targetTable.id, "Id2", false, insertId0) + + val action1 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), insertId1, listOf(fkGene1, fkGene2)) + + val actions = mutableListOf(action0, action1) + + // In SQL, if any column in a composite foreign key is NULL, the constraint is usually satisfied + // (depending on the MATCH type, but default is SIMPLE where one NULL is enough to satisfy it) + assertTrue(SqlActionUtils.isValidForeignKeys(actions)) + } + + @Test + fun testValidPartialNullableMultiColumnForeignKeyPointingToAnotherTable() { + val idColumn1 = Column("Id1", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val idColumn2 = Column("Id2", ColumnDataType.INTEGER, 10, + primaryKey = true, + databaseType = DatabaseType.H2) + + val targetTable = Table("Table0", setOf(idColumn1, idColumn2), setOf()) + + val fkColumn1 = Column("Fk1", ColumnDataType.INTEGER, 10, + nullable = true, // One column is nullable + databaseType = DatabaseType.H2) + + val fkColumn2 = Column("Fk2", ColumnDataType.INTEGER, 10, + nullable = false, // The other column is not nullable + databaseType = DatabaseType.H2) + + val foreignKey = ForeignKey( + sourceColumns = listOf(fkColumn1, fkColumn2), + targetTableId = targetTable.id, + targetColumns = listOf(idColumn1, idColumn2) + ) + + val sourceTable = Table("Table1", setOf(fkColumn1, fkColumn2), setOf(foreignKey)) + + + val integerGene1 = IntegerGene("Id1", 42, 0, 100) + val integerGene2 = IntegerGene("Id2", 84, 0, 100) + + val insertId0 = 1001L + val pkGene1Table0 = SqlPrimaryKeyGene("Id1", targetTable.id, integerGene1, insertId0) + val pkGene2Table0 = SqlPrimaryKeyGene("Id2", targetTable.id, integerGene2, insertId0) + val action0 = SqlAction(targetTable, setOf(idColumn1, idColumn2), insertId0, listOf(pkGene1Table0, pkGene2Table0)) + + val insertId1 = 1002L + // Fk1 is NULL (unbound) + val fkGene1 = SqlForeignKeyGene("Fk1", insertId1, targetTable.id, "Id1", true, -1L) + // Fk2 points to Table1.Id2 + val fkGene2 = SqlForeignKeyGene("Fk2", insertId1, targetTable.id, "Id2", false, 1001L) + + val action1 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), insertId1, listOf(fkGene1, fkGene2)) + + val actions = mutableListOf(action0, action1) + + // This SHOULD be valid because Fk1 is nullable which disables the FK constraint + val result = SqlActionUtils.isValidForeignKeys(actions) + + assertEquals(true, result) + } +} diff --git a/core/src/test/kotlin/org/evomaster/core/sql/TableConstraintEvaluatorTest.kt b/core/src/test/kotlin/org/evomaster/core/sql/TableConstraintEvaluatorTest.kt index 28c4e5c397..5db7472297 100644 --- a/core/src/test/kotlin/org/evomaster/core/sql/TableConstraintEvaluatorTest.kt +++ b/core/src/test/kotlin/org/evomaster/core/sql/TableConstraintEvaluatorTest.kt @@ -444,6 +444,44 @@ class TableConstraintEvaluatorTest { assertTrue(constraintValue) } + @Test + fun testUniqueConstraintMultiColumn() { + val column0 = Column("column0", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, nullable=false) + val column1 = Column("column1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, nullable=false) + val constraint = UniqueConstraint("table0", listOf("column0", "column1")) + val table = Table("table0", setOf(column0, column1), setOf(), setOf(constraint)) + + val action0 = SqlAction(table = table, selectedColumns = setOf(column0, column1), insertionId = 0L) + (action0.seeTopGenes()[0] as IntegerGene).value = 10 + (action0.seeTopGenes()[1] as IntegerGene).value = 1 + + val evaluator = TableConstraintEvaluator(listOf(action0)) + + // Same values for both columns -> False + val action1 = SqlAction(table = table, selectedColumns = setOf(column0, column1), insertionId = 1L) + (action1.seeTopGenes()[0] as IntegerGene).value = 10 + (action1.seeTopGenes()[1] as IntegerGene).value = 1 + assertFalse(constraint.accept(evaluator, action1)) + + // Different value for column0 -> True + val action2 = SqlAction(table = table, selectedColumns = setOf(column0, column1), insertionId = 2L) + (action2.seeTopGenes()[0] as IntegerGene).value = 20 + (action2.seeTopGenes()[1] as IntegerGene).value = 1 + assertTrue(constraint.accept(evaluator, action2)) + + // Different value for column1 -> True + val action3 = SqlAction(table = table, selectedColumns = setOf(column0, column1), insertionId = 3L) + (action3.seeTopGenes()[0] as IntegerGene).value = 10 + (action3.seeTopGenes()[1] as IntegerGene).value = 2 + assertTrue(constraint.accept(evaluator, action3)) + + // Different values for both columns -> True + val action4 = SqlAction(table = table, selectedColumns = setOf(column0, column1), insertionId = 4L) + (action4.seeTopGenes()[0] as IntegerGene).value = 20 + (action4.seeTopGenes()[1] as IntegerGene).value = 2 + assertTrue(constraint.accept(evaluator, action4)) + } + @Test fun testLikeConstraint() { val column = Column("column0", ColumnDataType.TEXT, databaseType = DatabaseType.POSTGRES, nullable=false, size=10) @@ -500,6 +538,48 @@ class TableConstraintEvaluatorTest { assertFalse(constraintValue) } + @Test + fun testUniqueConstraintWithNull() { + val column0 = Column("column0", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, nullable = true) + val constraint = UniqueConstraint("table0", listOf("column0", "column1")) + val table = Table("table0", setOf(column0), setOf(), setOf(constraint)) + + val action0 = SqlAction(table = table, selectedColumns = setOf(column0), insertionId = 0L) + (action0.seeTopGenes()[0] as NullableGene).isActive = false + + val evaluator = TableConstraintEvaluator(listOf(action0)) + + val action1 = SqlAction(table = table, selectedColumns = setOf(column0), insertionId = 1L) + (action1.seeTopGenes()[0] as NullableGene).isActive = false + + assertTrue(constraint.accept(evaluator, action1)) + } + + + @Test + fun testUniqueConstraintMultiColumnWithNulls() { + val column0 = Column("column0", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, nullable = true) + val column1 = Column("column1", ColumnDataType.INTEGER, databaseType = DatabaseType.H2, nullable = true) + val constraint = UniqueConstraint("table0", listOf("column0", "column1")) + val table = Table("table0", setOf(column0, column1), setOf(), setOf(constraint)) + + val action0 = SqlAction(table = table, selectedColumns = setOf(column0, column1), insertionId = 0L) + (action0.seeTopGenes()[0] as NullableGene).isActive = true + ((action0.seeTopGenes()[0] as NullableGene).gene as IntegerGene).value = 10 + (action0.seeTopGenes()[1] as NullableGene).isActive = false // NULL + + val evaluator = TableConstraintEvaluator(listOf(action0)) + + // Same values (10, NULL) -> True + // This happens since SQL disables the unique constraints when NULL values + // are present. + val action1 = SqlAction(table = table, selectedColumns = setOf(column0, column1), insertionId = 1L) + (action1.seeTopGenes()[0] as NullableGene).isActive = true + ((action1.seeTopGenes()[0] as NullableGene).gene as IntegerGene).value = 10 + (action1.seeTopGenes()[1] as NullableGene).isActive = false // NULL + assertTrue(constraint.accept(evaluator, action1)) + } + @Test fun testSimilarToConstraintDiffTable() { val column = Column("column0", ColumnDataType.TEXT, databaseType = DatabaseType.POSTGRES, nullable=false) @@ -513,4 +593,4 @@ class TableConstraintEvaluatorTest { val constraintValue = constraint.accept(evaluator, action) assertTrue(constraintValue) } -} \ No newline at end of file +}