Skip to content

Commit 52f706b

Browse files
committed
fix(dao): Use upsert for resolved items junction tables
When the Evaluator stores resolved issues, the same issue message can be reported for multiple packages at different timestamps. ORT's Issue includes `timestamp` in its equals, so these survive as separate map keys in `resolveResolutionsWithMappings()`. However, `findOrtRunIssueId()` matches only on `source`, `message`, `severity`, and `affectedPath` — not `timestamp` — so both map to the same database row. When both match the same resolution, the second insert of the identical `(ortRunId, ortRunIssueId, issueResolutionId)` tuple violates the unique constraint. Fix by changing `insert` to `upsert` for `ResolvedIssuesTable`, `ResolvedRuleViolationsTable`, and `ResolvedVulnerabilitiesTable`, matching the pattern already used for the `ResolvedConfigurations*` tables. Resolves #4461. Signed-off-by: Jyrki Keisala <jyrki.keisala@doubleopen.org>
1 parent d760fd4 commit 52f706b

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

dao/src/main/kotlin/repositories/resolvedconfiguration/DaoResolvedConfigurationRepository.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ class DaoResolvedConfigurationRepository(private val db: Database) : ResolvedCon
130130

131131
// Store resolved item mapping if the issue was found in the run
132132
if (ortRunIssueId != null) {
133-
ResolvedIssuesTable.insert {
133+
ResolvedIssuesTable.upsert(
134+
ResolvedIssuesTable.ortRunId,
135+
ResolvedIssuesTable.ortRunIssueId,
136+
ResolvedIssuesTable.issueResolutionId
137+
) {
134138
it[ResolvedIssuesTable.ortRunId] = ortRunId
135139
it[ResolvedIssuesTable.ortRunIssueId] = ortRunIssueId
136140
it[issueResolutionId] = issueResolutionDao.id.value
@@ -156,7 +160,11 @@ class DaoResolvedConfigurationRepository(private val db: Database) : ResolvedCon
156160

157161
// Store resolved item mapping if the rule violation was found
158162
if (ruleViolationId != null) {
159-
ResolvedRuleViolationsTable.insert {
163+
ResolvedRuleViolationsTable.upsert(
164+
ResolvedRuleViolationsTable.ortRunId,
165+
ResolvedRuleViolationsTable.ruleViolationId,
166+
ResolvedRuleViolationsTable.ruleViolationResolutionId
167+
) {
160168
it[ResolvedRuleViolationsTable.ortRunId] = ortRunId
161169
it[ResolvedRuleViolationsTable.ruleViolationId] = ruleViolationId
162170
it[ruleViolationResolutionId] = ruleViolationResolutionDao.id.value
@@ -182,7 +190,12 @@ class DaoResolvedConfigurationRepository(private val db: Database) : ResolvedCon
182190

183191
// Store resolved item mappings for each vulnerability-identifier pair
184192
vulnerabilityIdentifierPairs.forEach { (vulnId, identifierId) ->
185-
ResolvedVulnerabilitiesTable.insert {
193+
ResolvedVulnerabilitiesTable.upsert(
194+
ResolvedVulnerabilitiesTable.ortRunId,
195+
ResolvedVulnerabilitiesTable.vulnerabilityId,
196+
ResolvedVulnerabilitiesTable.identifierId,
197+
ResolvedVulnerabilitiesTable.vulnerabilityResolutionId
198+
) {
186199
it[ResolvedVulnerabilitiesTable.ortRunId] = ortRunId
187200
it[vulnerabilityId] = vulnId
188201
it[ResolvedVulnerabilitiesTable.identifierId] = identifierId

dao/src/test/kotlin/repositories/resolvedconfiguration/DaoResolvedConfigurationRepositoryTest.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,41 @@ class DaoResolvedConfigurationRepositoryTest : WordSpec({
364364
storedResolvedIssues.size shouldBe 0
365365
}
366366

367+
"handle duplicate addResolutions calls without constraint violation" {
368+
// This simulates both the evaluator and reporter calling storeResolvedItems()
369+
// for the same ORT run with overlapping data.
370+
val issue = Issue(
371+
timestamp = Clock.System.now(),
372+
source = "Analyzer",
373+
message = "Duplicate test issue",
374+
severity = Severity.WARNING
375+
)
376+
fixtures.createAnalyzerRun(
377+
analyzerJobId = fixtures.analyzerJob.id,
378+
issues = listOf(issue)
379+
)
380+
381+
val resolvedItems = ResolvedItemsResult(
382+
issues = mapOf(issue to listOf(issueResolution1)),
383+
ruleViolations = emptyMap(),
384+
vulnerabilities = emptyMap()
385+
)
386+
387+
// First call (e.g. evaluator)
388+
resolvedConfigurationRepository.addResolutions(ortRunId, resolvedItems)
389+
390+
// Second call with the same data (e.g. reporter) should not throw
391+
resolvedConfigurationRepository.addResolutions(ortRunId, resolvedItems)
392+
393+
// Verify only one mapping was stored (upsert, not duplicate)
394+
val storedResolvedIssues = dbExtension.db.dbQuery {
395+
ResolvedIssuesTable.selectAll()
396+
.where { ResolvedIssuesTable.ortRunId eq ortRunId }
397+
.toList()
398+
}
399+
storedResolvedIssues.size shouldBe 1
400+
}
401+
367402
"handle same resolution matching multiple issues without constraint violation" {
368403
// Create two issues in the database
369404
val issue1 = Issue(

0 commit comments

Comments
 (0)