Skip to content

Commit b26ddda

Browse files
committed
isValidForeignKeys checks for constraints accross multiple columns
1 parent b5ccd1d commit b26ddda

2 files changed

Lines changed: 283 additions & 24 deletions

File tree

core/src/main/kotlin/org/evomaster/core/sql/SqlActionUtils.kt

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.evomaster.core.search.gene.Gene
66
import org.evomaster.core.search.gene.sql.SqlForeignKeyGene
77
import org.evomaster.core.search.gene.sql.SqlPrimaryKeyGene
88
import org.evomaster.core.search.service.Randomness
9+
import org.evomaster.core.sql.schema.ForeignKey
910
import org.slf4j.Logger
1011
import org.slf4j.LoggerFactory
1112
import org.evomaster.core.sql.schema.Table
@@ -140,49 +141,101 @@ object SqlActionUtils {
140141
}
141142

142143

144+
/**
145+
* Validates the foreign key constraints in the provided list of SQL actions.
146+
*
147+
* @param actions A list of SQL actions to validate their foreign key constraints.
148+
* @param errors An optional mutable list to store error messages if any invalidity is found.
149+
* If null, errors will not be collected.
150+
*
151+
* @return A boolean value indicating whether all foreign key constraints are valid.
152+
* Returns true if valid, otherwise false.
153+
*/
143154
fun isValidForeignKeys(actions: List<SqlAction>, errors: MutableList<String>? = null): Boolean {
144155

145156
for (i in 0 until actions.size) {
146-
147-
val fks = actions[i].seeTopGenes()
157+
val currentAction = actions[i]
158+
val currentFkGenes = currentAction.seeTopGenes()
148159
.flatMap { it.flatView() }
149160
.filterIsInstance<SqlForeignKeyGene>()
150161

151-
fks.find { !it.nullable && !it.isBound() }
162+
currentFkGenes.find { !it.nullable && !it.isBound() }
152163
?.let {
153164
errors?.add("FK ${it.name} is not nullable and not bound: this is invalid")
154165
return false
155166
}
156167

157168
if (i == 0) {
158-
if(fks.isEmpty()) {
169+
if (currentFkGenes.isEmpty()) {
159170
continue
160-
}
161-
else {
171+
} else {
162172
errors?.add("First SQL action has FKs")
163173
return false
164174
}
165175
}
166176

167-
/*
168-
note: a row could have FK to itself... weird, but possible.
169-
but not sure if we should allow it
170-
*/
171-
val previous = actions.subList(0, i)
172-
173-
fks.filter { it.isBound() }
174-
.map { it.uniqueIdOfPrimaryKey }
175-
.forEach { id ->
176-
val match = previous.asSequence()
177-
.flatMap { it.seeTopGenes().asSequence() }
178-
.filterIsInstance<SqlPrimaryKeyGene>()
179-
.any { it.uniqueId == id }
180-
181-
if (!match) {
182-
errors?.add("FK is pointing to $id, but such action could not be found in previous insertions.")
183-
return false
184-
}
177+
// Collect all unique Ids and their associated table IDs
178+
val previousSqlActions = actions.subList(0, i)
179+
val previousPkMap = mutableMapOf<Long, TableId>()
180+
previousSqlActions.forEach { action ->
181+
action.seeTopGenes()
182+
.flatMap { it.flatView() }
183+
.filterIsInstance<SqlPrimaryKeyGene>()
184+
.forEach { previousPkMap[it.uniqueId] = it.tableName }
185+
}
186+
187+
// Check each foreign key constraint defined in the table
188+
for (fkConstraint in currentAction.table.foreignKeys) {
189+
if (!isValidForeignKey(fkConstraint, currentFkGenes, previousPkMap, errors)) {
190+
return false
185191
}
192+
}
193+
}
194+
195+
return true
196+
}
197+
198+
/**
199+
* Validates if the provided foreign key constraint is satisfied given the specified context.
200+
*
201+
* @param fkConstraint the foreign key constraint to validate
202+
* @param currentFkGenes a list of `SqlForeignKeyGene` representing the current foreign key elements
203+
* @param previousPkUniqueIds a set of unique IDs of primary keys from previous operations
204+
* @param errors a mutable list to which validation error messages will be added, if any
205+
* @return `true` if the foreign key constraint is valid, `false` otherwise
206+
*/
207+
private fun isValidForeignKey(
208+
fkConstraint: ForeignKey,
209+
currentFkGenes: List<SqlForeignKeyGene>,
210+
previousPkMap: Map<Long, TableId>,
211+
errors: MutableList<String>?
212+
): Boolean {
213+
val sourceColumnNames = fkConstraint.sourceColumns.map { it.name }
214+
val genesInFk = currentFkGenes.filter { sourceColumnNames.contains(it.name) }
215+
216+
if (genesInFk.isEmpty()) {
217+
errors?.add("FK constraint $fkConstraint is not satisfied: no genes found for source columns ${sourceColumnNames.joinToString(",")}")
218+
return false
219+
}
220+
221+
val boundIds = genesInFk.filter { it.isBound() }.map { it.uniqueIdOfPrimaryKey }.distinct()
222+
if (boundIds.size > 1) {
223+
errors?.add("Composite FK $fkConstraint points to multiple different primary keys: $boundIds")
224+
return false
225+
}
226+
227+
if (boundIds.isNotEmpty()) {
228+
val referencedPkUniqueId = boundIds.first()
229+
if (!previousPkMap.containsKey(referencedPkUniqueId)) {
230+
errors?.add("FK is pointing to $referencedPkUniqueId, but such action could not be found in previous insertions.")
231+
return false
232+
}
233+
234+
val actualTableId = previousPkMap[referencedPkUniqueId]
235+
if (actualTableId != fkConstraint.targetTableId) {
236+
errors?.add("FK is pointing to $referencedPkUniqueId from table $actualTableId, but the constraint expects table ${fkConstraint.targetTableId}")
237+
return false
238+
}
186239
}
187240

188241
return true
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package org.evomaster.core.sql
2+
3+
import org.evomaster.client.java.controller.api.dto.database.schema.DatabaseType
4+
import org.evomaster.core.search.gene.numeric.IntegerGene
5+
import org.evomaster.core.search.gene.sql.SqlForeignKeyGene
6+
import org.evomaster.core.search.gene.sql.SqlPrimaryKeyGene
7+
import org.evomaster.core.sql.schema.*
8+
import org.junit.jupiter.api.Assertions.*
9+
import org.junit.jupiter.api.Test
10+
11+
class SqlActionUtilsTest {
12+
13+
@Test
14+
fun testValidSingleColumnForeignKey() {
15+
val idColumn = Column("Id", ColumnDataType.INTEGER, 10,
16+
primaryKey = true,
17+
autoIncrement = false,
18+
unique = false,
19+
databaseType = DatabaseType.H2)
20+
21+
val targetTable = Table("Table0", setOf(idColumn), setOf())
22+
23+
val fkColumn = Column("Id", ColumnDataType.INTEGER, 10,
24+
primaryKey = true,
25+
autoIncrement = false,
26+
unique = false,
27+
databaseType = DatabaseType.H2)
28+
29+
val foreignKey = ForeignKey(sourceColumns = listOf(fkColumn), targetTableId = targetTable.id, targetColumns = listOf(idColumn))
30+
31+
val sourceTable = Table("Table1", setOf(fkColumn), setOf(foreignKey))
32+
33+
val integerGene = IntegerGene("Id", 42, 0, 100)
34+
35+
36+
val insertId0 = 1001L
37+
val pkGeneTable0 = SqlPrimaryKeyGene("Id", targetTable.id, integerGene, insertId0)
38+
val action0 = SqlAction(targetTable, setOf(idColumn), insertId0, listOf(pkGeneTable0))
39+
40+
val insertId1 = 1002L
41+
val fkGene = SqlForeignKeyGene("Id", insertId1, targetTable.id, "Id", false, insertId0)
42+
val pkGeneTable1 = SqlPrimaryKeyGene("Id", sourceTable.id, fkGene, insertId1)
43+
44+
val action1 = SqlAction(sourceTable, setOf(fkColumn), insertId1, listOf(pkGeneTable1))
45+
46+
47+
val actions = mutableListOf(action0, action1)
48+
49+
assertTrue(SqlActionUtils.isValidForeignKeys(actions))
50+
}
51+
52+
@Test
53+
fun testValidMultiColumnForeignKeys() {
54+
val idColumn1 = Column("Id1", ColumnDataType.INTEGER, 10,
55+
primaryKey = true,
56+
autoIncrement = false,
57+
unique = false,
58+
databaseType = DatabaseType.H2)
59+
60+
val idColumn2 = Column("Id2", ColumnDataType.INTEGER, 10,
61+
primaryKey = true,
62+
autoIncrement = false,
63+
unique = false,
64+
databaseType = DatabaseType.H2)
65+
66+
val targetTable = Table("Table0", setOf(idColumn1, idColumn2), setOf())
67+
68+
val fkColumn1 = Column("Fk1", ColumnDataType.INTEGER, 10,
69+
primaryKey = false,
70+
autoIncrement = false,
71+
unique = false,
72+
databaseType = DatabaseType.H2)
73+
74+
val fkColumn2 = Column("Fk2", ColumnDataType.INTEGER, 10,
75+
primaryKey = false,
76+
autoIncrement = false,
77+
unique = false,
78+
databaseType = DatabaseType.H2)
79+
80+
val foreignKey = ForeignKey(
81+
sourceColumns = listOf(fkColumn1, fkColumn2),
82+
targetTableId = targetTable.id,
83+
targetColumns = listOf(idColumn1, idColumn2)
84+
)
85+
86+
val sourceTable = Table("Table1", setOf(fkColumn1, fkColumn2), setOf(foreignKey))
87+
88+
val integerGene1 = IntegerGene("Id1", 42, 0, 100)
89+
val integerGene2 = IntegerGene("Id2", 84, 0, 100)
90+
91+
val insertId0 = 1001L
92+
val pkGene1Table0 = SqlPrimaryKeyGene("Id1", targetTable.id, integerGene1, insertId0)
93+
val pkGene2Table0 = SqlPrimaryKeyGene("Id2", targetTable.id, integerGene2, insertId0)
94+
val action0 = SqlAction(targetTable, setOf(idColumn1, idColumn2), insertId0, listOf(pkGene1Table0, pkGene2Table0))
95+
96+
val insertId1 = 1002L
97+
val fkGene1 = SqlForeignKeyGene("Fk1", insertId1, targetTable.id, "Id1", false, insertId0)
98+
val fkGene2 = SqlForeignKeyGene("Fk2", insertId1, targetTable.id, "Id2", false, insertId0)
99+
100+
val action1 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), insertId1, listOf(fkGene1, fkGene2))
101+
102+
val actions = mutableListOf(action0, action1)
103+
104+
assertTrue(SqlActionUtils.isValidForeignKeys(actions))
105+
}
106+
107+
@Test
108+
fun testInvalidMultiColumnForeignKeys() {
109+
val idColumn1 = Column("Id1", ColumnDataType.INTEGER, 10,
110+
primaryKey = true,
111+
databaseType = DatabaseType.H2)
112+
113+
val idColumn2 = Column("Id2", ColumnDataType.INTEGER, 10,
114+
primaryKey = true,
115+
databaseType = DatabaseType.H2)
116+
117+
val targetTable = Table("Table0", setOf(idColumn1, idColumn2), setOf())
118+
119+
val fkColumn1 = Column("Fk1", ColumnDataType.INTEGER, 10,
120+
databaseType = DatabaseType.H2)
121+
122+
val fkColumn2 = Column("Fk2", ColumnDataType.INTEGER, 10,
123+
databaseType = DatabaseType.H2)
124+
125+
val foreignKey = ForeignKey(
126+
sourceColumns = listOf(fkColumn1, fkColumn2),
127+
targetTableId = targetTable.id,
128+
targetColumns = listOf(idColumn1, idColumn2)
129+
)
130+
131+
val sourceTable = Table("Table1", setOf(fkColumn1, fkColumn2), setOf(foreignKey))
132+
133+
val integerGene0 = IntegerGene("Id1", 21, 0, 100)
134+
val integerGene1 = IntegerGene("Id1", 42, 0, 100)
135+
val integerGene2 = IntegerGene("Id2", 84, 0, 100)
136+
137+
// Inserts (21,84) to Table0(Id1,Id2)
138+
val insertId0 = 1001L
139+
val pkGene1Table0_0 = SqlPrimaryKeyGene("Id1", targetTable.id, integerGene0, insertId0)
140+
val pkGene2Table0_0 = SqlPrimaryKeyGene("Id2", targetTable.id, integerGene2, insertId0)
141+
val action0 = SqlAction(targetTable, setOf(idColumn1, idColumn2), insertId0, listOf(pkGene1Table0_0, pkGene2Table0_0))
142+
143+
// Inserts (42,84) to Table0(Id1,Id2)
144+
val insertId1 = 1002L
145+
val pkGene1Table0_1 = SqlPrimaryKeyGene("Id1", targetTable.id, integerGene1, insertId1)
146+
val pkGene2Table0_1 = SqlPrimaryKeyGene("Id2", targetTable.id, integerGene2, insertId1)
147+
val action1 = SqlAction(targetTable, setOf(idColumn1, idColumn2), insertId1, listOf(pkGene1Table0_1, pkGene2Table0_1))
148+
149+
val insertId2 = 1003L
150+
// Points to DIFFERENT but VALID PK IDs (1001L and 1002L)
151+
val fkGene1 = SqlForeignKeyGene("Fk1", insertId2, targetTable.id, "Id1", false, insertId0)
152+
val fkGene2 = SqlForeignKeyGene("Fk2", insertId2, targetTable.id, "Id2", false, insertId1)
153+
154+
val action2 = SqlAction(sourceTable, setOf(fkColumn1, fkColumn2), insertId2, listOf(fkGene1, fkGene2))
155+
156+
val actions = mutableListOf(action0, action1, action2)
157+
158+
assertFalse(SqlActionUtils.isValidForeignKeys(actions))
159+
}
160+
161+
@Test
162+
fun testInvalidForeignKeyReferringToWrongTable() {
163+
val idColumnTable0 = Column("Id0", ColumnDataType.INTEGER, 10,
164+
primaryKey = true,
165+
databaseType = DatabaseType.H2)
166+
167+
val table0 = Table("Table0", setOf(idColumnTable0), setOf())
168+
169+
val idColumnTable1 = Column("Id1", ColumnDataType.INTEGER, 10,
170+
primaryKey = true,
171+
databaseType = DatabaseType.H2)
172+
173+
val table1 = Table("Table1", setOf(idColumnTable1), setOf())
174+
175+
val fkColumn = Column("Fk", ColumnDataType.INTEGER, 10,
176+
databaseType = DatabaseType.H2)
177+
178+
// FK in Table2 points to Table1
179+
val foreignKey = ForeignKey(
180+
sourceColumns = listOf(fkColumn),
181+
targetTableId = table1.id,
182+
targetColumns = listOf(idColumnTable1)
183+
)
184+
185+
val table2 = Table("Table2", setOf(fkColumn), setOf(foreignKey))
186+
187+
val integerGene0 = IntegerGene("Id0", 1, 0, 100)
188+
189+
// Action 0: Insert into Table0 (ID = 1, uniqueId = 1001L)
190+
val insertId0 = 1001L
191+
val pkGeneTable0 = SqlPrimaryKeyGene("Id0", table0.id, integerGene0, insertId0)
192+
val action0 = SqlAction(table0, setOf(idColumnTable0), insertId0, listOf(pkGeneTable0))
193+
194+
// Action 1: Insert into Table2, but FK points to uniqueId = 1001L (which belongs to Table0, NOT Table1)
195+
val insertId1 = 1002L
196+
val fkGene = SqlForeignKeyGene("Fk", insertId1, table1.id, "Id1", false, insertId0)
197+
198+
val action1 = SqlAction(table2, setOf(fkColumn), insertId1, listOf(fkGene))
199+
200+
val actions = mutableListOf(action0, action1)
201+
202+
// This SHOULD be false, because the FK points to an insertion in Table0, but the constraint says it should point to Table1
203+
assertFalse(SqlActionUtils.isValidForeignKeys(actions))
204+
}
205+
206+
}

0 commit comments

Comments
 (0)