Skip to content

Commit 1ab4736

Browse files
authored
Merge pull request #1494 from WebFuzzing/sql-multi-column-foreign-keys
Support multi-column foreign keys in SQL
2 parents c7b2175 + 3f7072f commit 1ab4736

39 files changed

Lines changed: 2110 additions & 105 deletions

File tree

client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/h2/H2SchemaExtractorTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ public void testBasicForeignKey() throws Exception {
146146
assertEquals(1, foreignKey.sourceColumns.size());
147147
assertTrue(foreignKey.sourceColumns.stream().anyMatch(c -> c.equalsIgnoreCase("barId")));
148148
assertTrue(foreignKey.targetTable.equalsIgnoreCase("Bar"));
149+
150+
assertEquals(1, foreignKey.targetColumns.size());
151+
assertTrue(foreignKey.targetColumns.stream().anyMatch(c -> c.equalsIgnoreCase("id")));
149152
}
150153

151154
@Test

client-java/controller/src/test/java/org/evomaster/client/java/controller/internal/db/sql/mysql/MySQLSchemaExtractorTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.evomaster.client.java.controller.internal.db.sql.mysql;
22

33
import org.evomaster.client.java.controller.DatabaseTestTemplate;
4+
import org.evomaster.client.java.controller.api.dto.database.schema.ForeignKeyDto;
45
import org.evomaster.client.java.controller.api.dto.database.schema.DbInfoDto;
56
import org.evomaster.client.java.controller.api.dto.database.schema.TableDto;
67
import org.evomaster.client.java.sql.SqlScriptRunner;

client-java/sql-dto/src/main/java/org/evomaster/client/java/controller/api/dto/database/schema/ForeignKeyDto.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,52 @@
33
import java.util.ArrayList;
44
import java.util.List;
55

6+
/**
7+
* Represents a foreign key relationship in a database schema.
8+
*
9+
* A foreign key establishes a connection between two database tables,
10+
* where one table (source) references the primary key or a unique column
11+
* in another table (target).
12+
*
13+
* This class captures the metadata for the foreign key, including the columns
14+
* involved in the relationship and the target table being referenced.
15+
*/
616
public class ForeignKeyDto {
717

18+
/**
19+
* A list of column names in the source table of the foreign key relationship.
20+
*
21+
* These column names correspond to the columns in the source table
22+
* that are used in the foreign key constraint. They establish the
23+
* connection to the target table by referencing its primary key
24+
* or unique columns.
25+
*
26+
* The order of the columns in this list corresponds to the order
27+
* in which they are defined in the foreign key constraint.
28+
*/
829
public List<String> sourceColumns = new ArrayList<>();
930

31+
/**
32+
* The name of the target table in a foreign key relationship.
33+
*
34+
* This variable specifies the table being referenced by the foreign key.
35+
* The value corresponds to the physical name of the target table in the database.
36+
* The foreign key relationship indicates that a column or set of columns in
37+
* the source table references a column or set of columns (usually the primary key)
38+
* in the target table.
39+
*/
1040
public String targetTable;
1141

12-
//TODO likely ll need to handle targetColumns if we have multi-columns
42+
/**
43+
* A list of column names in the target table of the foreign key relationship.
44+
*
45+
* These column names represent the columns in the target table
46+
* that are referenced by the foreign key constraint. They typically
47+
* refer to primary key columns or unique columns in the target table.
48+
*
49+
* The order of the columns in this list corresponds to the order
50+
* defined in the foreign key relationship, ensuring a one-to-one
51+
* mapping with the source columns.
52+
*/
53+
public List<String> targetColumns = new ArrayList<>();
1354
}

client-java/sql/src/main/java/org/evomaster/client/java/sql/DbCleaner.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ private static List<String> cleanDataInTables(List<String> tableToSkip,
282282
if (doDropTable) {
283283
dropTableIfExists(statement, ts);
284284
} else {
285-
truncateTable(statement, ts, restartIdentityWhenTruncating);
285+
truncateTable(statement, ts, restartIdentityWhenTruncating, type);
286286
}
287287
} else {
288288
//note: if one at a time, need to make sure to first disable FK checks
@@ -300,7 +300,7 @@ private static List<String> cleanDataInTables(List<String> tableToSkip,
300300
if (type == DatabaseType.MS_SQL_SERVER)
301301
deleteTables(statement, t, schema, tablesHaveIdentifies);
302302
else
303-
truncateTable(statement, t, restartIdentityWhenTruncating);
303+
truncateTable(statement, t, restartIdentityWhenTruncating, type);
304304
}
305305
}
306306
}
@@ -327,12 +327,15 @@ private static void deleteTables(Statement statement, String table, String schem
327327
statement.executeUpdate("DBCC CHECKIDENT ('" + tableWithSchema + "', RESEED, 0)");
328328
}
329329

330-
private static void truncateTable(Statement statement, String table, boolean restartIdentityWhenTruncating) throws SQLException {
330+
private static void truncateTable(Statement statement, String table, boolean restartIdentityWhenTruncating, DatabaseType type) throws SQLException {
331+
String sql = "TRUNCATE TABLE " + table;
331332
if (restartIdentityWhenTruncating) {
332-
statement.executeUpdate("TRUNCATE TABLE " + table + " RESTART IDENTITY");
333-
} else {
334-
statement.executeUpdate("TRUNCATE TABLE " + table);
333+
sql += " RESTART IDENTITY";
334+
}
335+
if (type == DatabaseType.POSTGRES) {
336+
sql += " CASCADE";
335337
}
338+
statement.executeUpdate(sql);
336339
}
337340

338341
private static void resetSequences(Statement s, DatabaseType type, String schemaName, List<String> sequenceToClean) throws SQLException {

client-java/sql/src/main/java/org/evomaster/client/java/sql/DbInfoExtractor.java

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -654,14 +654,22 @@ private static void handleTableEntry(Connection connection, DbInfoDto schemaDto,
654654

655655

656656
ResultSet fks = md.getImportedKeys(tableDto.id.catalog, tableDto.id.schema, tableDto.id.name);
657+
Map<String, ForeignKeyDto> foreignKeysByName = new HashMap<>();
657658
while (fks.next()) {
658-
//TODO need to see how to handle case of multi-columns
659-
660-
ForeignKeyDto fkDto = new ForeignKeyDto();
661-
fkDto.sourceColumns.add(fks.getString("FKCOLUMN_NAME"));
662-
fkDto.targetTable = fks.getString("PKTABLE_NAME");
663-
664-
tableDto.foreignKeys.add(fkDto);
659+
String fkName = fks.getString("FK_NAME");
660+
String sourceColumn = fks.getString("FKCOLUMN_NAME");
661+
String targetTable = fks.getString("PKTABLE_NAME");
662+
String targetColumn = fks.getString("PKCOLUMN_NAME");
663+
664+
ForeignKeyDto fkDto = foreignKeysByName.get(fkName);
665+
if (fkDto == null) {
666+
fkDto = new ForeignKeyDto();
667+
fkDto.targetTable = targetTable;
668+
foreignKeysByName.put(fkName, fkDto);
669+
tableDto.foreignKeys.add(fkDto);
670+
}
671+
fkDto.sourceColumns.add(sourceColumn);
672+
fkDto.targetColumns.add(targetColumn);
665673
}
666674
fks.close();
667675
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.evomaster.client.java.sql;
2+
3+
import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType;
4+
import org.junit.jupiter.api.AfterAll;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.BeforeEach;
7+
8+
import java.sql.Connection;
9+
import java.sql.DriverManager;
10+
11+
12+
public class DbInfoExtractorH2Test extends DbInfoExtractorTestBase {
13+
14+
private static Connection connection;
15+
16+
@BeforeAll
17+
public static void initClass() throws Exception {
18+
connection = DriverManager.getConnection("jdbc:h2:mem:db_test", "sa", "");
19+
}
20+
21+
@AfterAll
22+
public static void afterClass() throws Exception {
23+
connection.close();
24+
}
25+
26+
@BeforeEach
27+
public void initTest() throws Exception {
28+
//custom H2 command
29+
SqlScriptRunner.execCommand(connection, "DROP ALL OBJECTS;");
30+
}
31+
32+
33+
@Override
34+
protected DatabaseType getDbType() {
35+
return DatabaseType.H2;
36+
}
37+
38+
@Override
39+
protected Connection getConnection() {
40+
return connection;
41+
}
42+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.evomaster.client.java.sql;
2+
3+
import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType;
4+
import org.junit.jupiter.api.AfterAll;
5+
import org.junit.jupiter.api.AfterEach;
6+
import org.junit.jupiter.api.BeforeAll;
7+
import org.junit.jupiter.api.Test;
8+
import org.testcontainers.containers.GenericContainer;
9+
10+
import java.sql.Connection;
11+
import java.sql.DriverManager;
12+
import java.sql.SQLException;
13+
import java.text.SimpleDateFormat;
14+
import java.util.Date;
15+
import java.util.HashMap;
16+
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
import static org.junit.jupiter.api.Assertions.assertTrue;
19+
20+
public class DbInfoExtractorMySQLTest extends DbInfoExtractorTestBase {
21+
22+
private static final String DB_NAME = "test";
23+
24+
private static final int PORT = 3306;
25+
26+
private static final String MYSQL_VERSION = "8.0.27";
27+
28+
public static final GenericContainer mysql = new GenericContainer("mysql:" + MYSQL_VERSION)
29+
.withEnv(new HashMap<String, String>(){{
30+
put("MYSQL_ROOT_PASSWORD", "root");
31+
put("MYSQL_DATABASE", DB_NAME);
32+
put("MYSQL_USER", "test");
33+
put("MYSQL_PASSWORD", "test");
34+
}})
35+
.withExposedPorts(PORT);
36+
37+
private static Connection connection;
38+
39+
@BeforeAll
40+
public static void initClass() throws Exception {
41+
42+
mysql.start();
43+
44+
String host = mysql.getContainerIpAddress();
45+
int port = mysql.getMappedPort(PORT);
46+
String url = "jdbc:mysql://"+host+":"+port+"/"+DB_NAME;
47+
48+
connection = DriverManager.getConnection(url, "test", "test");
49+
50+
}
51+
52+
@AfterAll
53+
public static void afterClass() throws Exception {
54+
connection.close();
55+
mysql.stop();
56+
}
57+
58+
@AfterEach
59+
public void afterTest() throws SQLException {
60+
SqlScriptRunner.execCommand(connection, "DROP TABLE IF EXISTS example_table;");
61+
}
62+
63+
64+
@Override
65+
protected DatabaseType getDbType() {
66+
return DatabaseType.MYSQL;
67+
}
68+
69+
@Override
70+
protected Connection getConnection() {
71+
return connection;
72+
}
73+
74+
75+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package org.evomaster.client.java.sql;
2+
3+
import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType;
4+
import org.junit.jupiter.api.AfterAll;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.testcontainers.containers.GenericContainer;
8+
9+
import java.sql.Connection;
10+
import java.sql.DriverManager;
11+
import java.util.Collections;
12+
13+
public class DbInfoExtractorPostgresTest extends DbInfoExtractorTestBase {
14+
15+
private static final String POSTGRES_VERSION = "14";
16+
17+
private static final GenericContainer<?> postgres = new GenericContainer("postgres:" + POSTGRES_VERSION)
18+
.withExposedPorts(5432)
19+
.withTmpFs(Collections.singletonMap("/var/lib/postgresql/data", "rw"))
20+
.withEnv("POSTGRES_HOST_AUTH_METHOD","trust");
21+
22+
private static Connection connection;
23+
24+
@BeforeAll
25+
public static void initClass() throws Exception{
26+
postgres.start();
27+
String host = postgres.getHost();
28+
int port = postgres.getMappedPort(5432);
29+
String url = "jdbc:postgresql://"+host+":"+port+"/postgres";
30+
31+
connection = DriverManager.getConnection(url, "postgres", "");
32+
}
33+
34+
@AfterAll
35+
public static void afterClass() throws Exception{
36+
connection.close();
37+
postgres.stop();
38+
}
39+
40+
@BeforeEach
41+
public void initTest() throws Exception {
42+
/*
43+
see:
44+
https://stackoverflow.com/questions/3327312/how-can-i-drop-all-the-tables-in-a-postgresql-database
45+
*/
46+
SqlScriptRunner.execCommand(connection, "DROP SCHEMA public CASCADE;");
47+
SqlScriptRunner.execCommand(connection, "CREATE SCHEMA public;");
48+
SqlScriptRunner.execCommand(connection, "GRANT ALL ON SCHEMA public TO postgres;");
49+
SqlScriptRunner.execCommand(connection, "GRANT ALL ON SCHEMA public TO public;");
50+
}
51+
52+
@Override
53+
protected Connection getConnection(){
54+
return connection;
55+
}
56+
57+
@Override
58+
protected DatabaseType getDbType() {
59+
return DatabaseType.POSTGRES;
60+
}
61+
62+
63+
}

0 commit comments

Comments
 (0)