Skip to content

Commit e250b1a

Browse files
committed
feat(scanner): Store detected and effective license in database
Store detected licenses and effective license for packages and projects during the scanner phase to make them easily available via API routes without having to traverse all license findings from scan results. - Add detectedLicenses and effectiveLicense columns to packages and projects tables (Flyway V139 migration) - Add fields to Package and Project models - Add DAO support for new columns with comma-separated serialization - Create LicenseComputation.kt with functions to compute licenses - Integrate license computation in ScannerWorker - Expose new fields in API v1 Package and Project responses - Add unit tests for DAO and LicenseComputation Resolves #4607.
1 parent 5020d0d commit e250b1a

17 files changed

Lines changed: 507 additions & 22 deletions

File tree

api/v1/mapping/src/commonMain/kotlin/ApiMappings.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,9 @@ fun Package.mapToApi(
809809
shortestDependencyPaths = shortestDependencyPaths.map { it.mapToApi() },
810810
curations = curations,
811811
sourceCodeOrigins = sourceCodeOrigins?.map { it.mapToApi() },
812-
labels = labels
812+
labels = labels,
813+
detectedLicenses = detectedLicenses,
814+
effectiveLicense = effectiveLicense
813815
)
814816

815817
fun PackageCurationData.mapToApi() = ApiPackageCurationData(
@@ -859,7 +861,9 @@ fun Project.mapToApi() = ApiProject(
859861
vcsProcessed = vcsProcessed.mapToApi(),
860862
description = description,
861863
homepageUrl = homepageUrl,
862-
scopeNames = scopeNames
864+
scopeNames = scopeNames,
865+
detectedLicenses = detectedLicenses,
866+
effectiveLicense = effectiveLicense
863867
)
864868

865869
fun UserDisplayName.mapToApi() = ApiUserDisplayName(username = username, fullName = fullName)

api/v1/model/src/commonMain/kotlin/Package.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ data class Package(
4040
val shortestDependencyPaths: List<ShortestDependencyPath>,
4141
val curations: List<PackageCuration>,
4242
val sourceCodeOrigins: List<SourceCodeOrigin>? = null,
43-
val labels: Map<String, String> = emptyMap()
43+
val labels: Map<String, String> = emptyMap(),
44+
val detectedLicenses: Set<String> = emptySet(),
45+
val effectiveLicense: String? = null
4446
)
4547

4648
/**

api/v1/model/src/commonMain/kotlin/Project.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,7 @@ data class Project(
3333
val vcsProcessed: VcsInfo,
3434
val description: String,
3535
val homepageUrl: String,
36-
val scopeNames: Set<String>
36+
val scopeNames: Set<String>,
37+
val detectedLicenses: Set<String> = emptySet(),
38+
val effectiveLicense: String? = null
3739
)

dao/src/main/kotlin/queries/analyzer/GetPackagesForAnalyzerRunQuery.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,13 @@ class GetPackagesForAnalyzerRunQuery(
149149
vcs = vcs,
150150
vcsProcessed = vcsProcessed,
151151
isMetadataOnly = resultRow[PackagesTable.isMetadataOnly],
152-
isModified = resultRow[PackagesTable.isModified]
152+
isModified = resultRow[PackagesTable.isModified],
153+
detectedLicenses = resultRow[PackagesTable.detectedLicenses]
154+
?.split(",")
155+
?.filterNot { it.isEmpty() }
156+
?.toSet()
157+
.orEmpty(),
158+
effectiveLicense = resultRow[PackagesTable.effectiveLicense]
153159
)
154160
}
155161
}

dao/src/main/kotlin/queries/analyzer/GetProjectsForAnalyzerRunQuery.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,13 @@ class GetProjectsForAnalyzerRunQuery(
120120
vcsProcessed = vcsProcessed,
121121
description = resultRow[ProjectsTable.description],
122122
homepageUrl = resultRow[ProjectsTable.homepageUrl],
123-
scopeNames = scopeNamesByProjectId[projectId].orEmpty()
123+
scopeNames = scopeNamesByProjectId[projectId].orEmpty(),
124+
detectedLicenses = resultRow[ProjectsTable.detectedLicenses]
125+
?.split(",")
126+
?.filterNot { it.isEmpty() }
127+
?.toSet()
128+
.orEmpty(),
129+
effectiveLicense = resultRow[ProjectsTable.effectiveLicense]
124130
)
125131
}
126132
}

dao/src/main/kotlin/repositories/analyzerrun/DaoAnalyzerRunRepository.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ private fun insertProject(
211211

212212
createProcessedDeclaredLicense(project.processedDeclaredLicense, projectDao = projectDao)
213213

214+
projectDao.detectedLicenses = project.detectedLicenses.joinToString(",").takeIf { it.isNotEmpty() }
215+
projectDao.effectiveLicense = project.effectiveLicense
216+
214217
return projectDao
215218
}
216219

@@ -284,6 +287,9 @@ private fun insertPackage(
284287

285288
createProcessedDeclaredLicense(pkg.processedDeclaredLicense, pkgDao = pkgDao)
286289

290+
pkgDao.detectedLicenses = pkg.detectedLicenses.joinToString(",").takeIf { it.isNotEmpty() }
291+
pkgDao.effectiveLicense = pkg.effectiveLicense
292+
287293
return pkgDao
288294
}
289295

dao/src/main/kotlin/repositories/analyzerrun/PackagesTable.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ object PackagesTable : SortableTable("packages") {
6262
val isMetadataOnly = bool("is_metadata_only").default(false)
6363
val isModified = bool("is_modified").default(false)
6464
val sourceCodeOrigins = text("source_code_origins").nullable()
65+
val detectedLicenses = text("detected_licenses").nullable()
66+
val effectiveLicense = text("effective_license").nullable()
6567
}
6668

6769
class PackageDao(id: EntityID<Long>) : LongEntity(id) {
@@ -185,6 +187,8 @@ class PackageDao(id: EntityID<Long>) : LongEntity(id) {
185187
ProcessedDeclaredLicensesTable.packageId
186188

187189
var sourceCodeOrigins by PackagesTable.sourceCodeOrigins
190+
var detectedLicenses by PackagesTable.detectedLicenses
191+
var effectiveLicense by PackagesTable.effectiveLicense
188192
val labels by PackageLabelDao referrersOn PackageLabelsTable.packageId
189193

190194
fun mapToModel() = Package(
@@ -206,6 +210,12 @@ class PackageDao(id: EntityID<Long>) : LongEntity(id) {
206210
?.split(",")
207211
?.filterNot { it.isEmpty() }
208212
?.map { SourceCodeOrigin.valueOf(it) },
209-
labels = labels.associate { it.key to it.value }
213+
labels = labels.associate { it.key to it.value },
214+
detectedLicenses = detectedLicenses
215+
?.split(",")
216+
?.filterNot { it.isEmpty() }
217+
?.toSet()
218+
.orEmpty(),
219+
effectiveLicense = effectiveLicense
210220
)
211221
}

dao/src/main/kotlin/repositories/analyzerrun/ProjectsTable.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ object ProjectsTable : SortableTable("projects") {
5353
val definitionFilePath = text("definition_file_path")
5454
val description = text("description")
5555
val homepageUrl = text("homepage_url")
56+
val detectedLicenses = text("detected_licenses").nullable()
57+
val effectiveLicense = text("effective_license").nullable()
5658
}
5759

5860
class ProjectDao(id: EntityID<Long>) : LongEntity(id) {
@@ -134,6 +136,9 @@ class ProjectDao(id: EntityID<Long>) : LongEntity(id) {
134136
val processedDeclaredLicense by ProcessedDeclaredLicenseDao backReferencedOn
135137
ProcessedDeclaredLicensesTable.projectId
136138

139+
var detectedLicenses by ProjectsTable.detectedLicenses
140+
var effectiveLicense by ProjectsTable.effectiveLicense
141+
137142
fun mapToModel() = Project(
138143
identifier = identifier.mapToModel(),
139144
cpe = cpe,
@@ -145,6 +150,12 @@ class ProjectDao(id: EntityID<Long>) : LongEntity(id) {
145150
vcsProcessed = vcsProcessed.mapToModel(),
146151
description = description,
147152
homepageUrl = homepageUrl,
148-
scopeNames = scopeNames.mapTo(mutableSetOf(), ProjectScopeDao::name)
153+
scopeNames = scopeNames.mapTo(mutableSetOf(), ProjectScopeDao::name),
154+
detectedLicenses = detectedLicenses
155+
?.split(",")
156+
?.filterNot { it.isEmpty() }
157+
?.toSet()
158+
.orEmpty(),
159+
effectiveLicense = effectiveLicense
149160
)
150161
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ALTER TABLE packages
2+
ADD COLUMN detected_licenses TEXT,
3+
ADD COLUMN effective_license TEXT;
4+
5+
ALTER TABLE projects
6+
ADD COLUMN detected_licenses TEXT,
7+
ADD COLUMN effective_license TEXT;

dao/src/test/kotlin/repositories/analyzerrun/DaoAnalyzerRunRepositoryTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,38 @@ class DaoAnalyzerRunRepositoryTest : StringSpec({
143143
dbExtension.db.dbQuery { ProjectsTable.selectAll().count() } shouldBe 1
144144
}
145145

146+
"create should deduplicate packages when license fields differ" {
147+
val pkg1 = createPackage(1).copy(
148+
detectedLicenses = setOf("MIT"),
149+
effectiveLicense = "MIT"
150+
)
151+
val pkg2 = createPackage(1).copy(
152+
detectedLicenses = setOf("Apache-2.0"),
153+
effectiveLicense = "Apache-2.0"
154+
)
155+
156+
analyzerRunRepository.create(analyzerJobId, analyzerRun.copy(packages = setOf(pkg1)))
157+
analyzerRunRepository.create(analyzerJobId, analyzerRun.copy(packages = setOf(pkg2)))
158+
159+
dbExtension.db.dbQuery { PackagesTable.selectAll().count() } shouldBe 1
160+
}
161+
162+
"create should deduplicate projects when license fields differ" {
163+
val project1 = project.copy(
164+
detectedLicenses = setOf("MIT"),
165+
effectiveLicense = "MIT"
166+
)
167+
val project2 = project.copy(
168+
detectedLicenses = setOf("Apache-2.0"),
169+
effectiveLicense = "Apache-2.0"
170+
)
171+
172+
analyzerRunRepository.create(analyzerJobId, analyzerRun.copy(projects = setOf(project1)))
173+
analyzerRunRepository.create(analyzerJobId, analyzerRun.copy(projects = setOf(project2)))
174+
175+
dbExtension.db.dbQuery { ProjectsTable.selectAll().count() } shouldBe 1
176+
}
177+
146178
"create should handle unique constraint violations" {
147179
val txCount = 64
148180
withContext(Dispatchers.IO) {

0 commit comments

Comments
 (0)