Skip to content

Commit 061a355

Browse files
committed
feat(genomics): Refactor coverage handling with embedded JSONB in alignment metadata
- Removed `AlignmentCoverage` model and related Slick table (`AlignmentCoverageTable`). - Introduced `EmbeddedCoverage` for compact JSONB-based coverage representation directly on `AlignmentMetadata`. - Refactored `AlignmentRepository` and `CoverageRepository` to support new JSONB structure for coverage updates and queries. - Optimized SQL queries and benchmarks to utilize JSONB fields for coverage data. - Added unit tests for `EmbeddedCoverage` serialization, deserialization, and integration validation across repositories.
1 parent 04ebf99 commit 061a355

8 files changed

Lines changed: 189 additions & 269 deletions

File tree

app/models/dal/DatabaseSchema.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ object DatabaseSchema {
6363
val analysisMethods = TableQuery[AnalysisMethodTable]
6464
val ancestryAnalyses = TableQuery[AncestryAnalysisTable]
6565
val alignmentMetadata = TableQuery[AlignmentMetadataTable]
66-
val alignmentCoverages = TableQuery[AlignmentCoverageTable]
66+
6767
val assemblyMetadata = TableQuery[AssemblyMetadataTable]
6868
val biosampleHaplogroups = TableQuery[BiosampleHaplogroupsTable]
6969
val biosamples = TableQuery[BiosamplesTable]

app/models/dal/domain/genomics/AlignmentCoverageTable.scala

Lines changed: 0 additions & 53 deletions
This file was deleted.

app/models/dal/domain/genomics/AlignmentMetadataTable.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,27 @@ class AlignmentMetadataTable(tag: Tag) extends Table[AlignmentMetadata](tag, Som
6161

6262
def metadata = column[Option[JsValue]]("metadata")
6363

64+
def coverage = column[Option[JsValue]]("coverage")
65+
6466
def * = (
6567
(id.?, sequenceFileId, genbankContigId, metricLevel),
6668
(regionName, regionStartPos, regionEndPos, regionLengthBp),
6769
(referenceBuild, variantCaller, genomeTerritory, meanCoverage, medianCoverage, sdCoverage, pctExcDupe, pctExcMapq, pct10x, pct20x, pct30x, hetSnpSensitivity),
68-
(metricsDate, analysisTool, analysisToolVersion, notes, metadata)
70+
(metricsDate, analysisTool, analysisToolVersion, notes, metadata, coverage)
6971
).shaped <> ( {
70-
case ((id, seqId, contigId, lvl), (rName, rStart, rEnd, rLen), (refBuild, vCaller, gTerr, meanCov, medCov, sdCov, pDupe, pMapq, p10, p20, p30, hetSens), (mDate, tool, toolVer, notes, meta)) =>
72+
case ((id, seqId, contigId, lvl), (rName, rStart, rEnd, rLen), (refBuild, vCaller, gTerr, meanCov, medCov, sdCov, pDupe, pMapq, p10, p20, p30, hetSens), (mDate, tool, toolVer, notes, meta, cov)) =>
7173
AlignmentMetadata(
7274
id, seqId, contigId, lvl,
7375
rName, rStart, rEnd, rLen,
7476
refBuild, vCaller, gTerr, meanCov, medCov, sdCov, pDupe, pMapq, p10, p20, p30, hetSens,
75-
mDate, tool, toolVer, notes, meta
77+
mDate, tool, toolVer, notes, meta, cov
7678
)
7779
}, { (m: AlignmentMetadata) =>
7880
Some((
7981
(m.id, m.sequenceFileId, m.genbankContigId, m.metricLevel),
8082
(m.regionName, m.regionStartPos, m.regionEndPos, m.regionLengthBp),
8183
(m.referenceBuild, m.variantCaller, m.genomeTerritory, m.meanCoverage, m.medianCoverage, m.sdCoverage, m.pctExcDupe, m.pctExcMapq, m.pct10x, m.pct20x, m.pct30x, m.hetSnpSensitivity),
82-
(m.metricsDate, m.analysisTool, m.analysisToolVersion, m.notes, m.metadata)
84+
(m.metricsDate, m.analysisTool, m.analysisToolVersion, m.notes, m.metadata, m.coverage)
8385
))
8486
}
8587
)

app/models/domain/genomics/AlignmentCoverage.scala

Lines changed: 0 additions & 39 deletions
This file was deleted.

app/models/domain/genomics/AlignmentMetadata.scala

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,33 @@ case class AlignmentMetadata(
7575
analysisTool: String,
7676
analysisToolVersion: Option[String] = None,
7777
notes: Option[String] = None,
78-
metadata: Option[JsValue] = None
79-
)
78+
metadata: Option[JsValue] = None,
79+
coverage: Option[JsValue] = None
80+
) {
81+
82+
def embeddedCoverage: Option[EmbeddedCoverage] = coverage.flatMap(_.asOpt[EmbeddedCoverage])
83+
84+
def withCoverage(ec: EmbeddedCoverage): AlignmentMetadata = copy(coverage = Some(Json.toJson(ec)))
85+
}
8086

8187
object AlignmentMetadata {
8288
implicit val format: OFormat[AlignmentMetadata] = Json.format[AlignmentMetadata]
89+
}
90+
91+
case class EmbeddedCoverage(
92+
meanDepth: Option[Double] = None,
93+
medianDepth: Option[Double] = None,
94+
percentCoverageAt1x: Option[Double] = None,
95+
percentCoverageAt5x: Option[Double] = None,
96+
percentCoverageAt10x: Option[Double] = None,
97+
percentCoverageAt20x: Option[Double] = None,
98+
percentCoverageAt30x: Option[Double] = None,
99+
basesNoCoverage: Option[Long] = None,
100+
basesLowQualityMapping: Option[Long] = None,
101+
basesCallable: Option[Long] = None,
102+
meanMappingQuality: Option[Double] = None
103+
)
104+
105+
object EmbeddedCoverage {
106+
implicit val format: OFormat[EmbeddedCoverage] = Json.format[EmbeddedCoverage]
83107
}

app/repositories/AlignmentRepository.scala

Lines changed: 16 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,30 @@ package repositories
22

33
import jakarta.inject.{Inject, Singleton}
44
import models.dal.DatabaseSchema
5-
import models.domain.genomics.{AlignmentCoverage, AlignmentMetadata, MetricLevel}
5+
import models.domain.genomics.{AlignmentMetadata, EmbeddedCoverage, MetricLevel}
66
import play.api.db.slick.DatabaseConfigProvider
7+
import play.api.libs.json.Json
78

89
import scala.concurrent.{ExecutionContext, Future}
910

1011
/**
1112
* Repository interface for managing alignment metadata and coverage statistics.
1213
*/
1314
trait AlignmentRepository {
14-
/**
15-
* Creates a new alignment metadata record.
16-
*
17-
* @param metadata The alignment metadata to create
18-
* @return A future containing the created metadata with its assigned ID
19-
*/
2015
def createMetadata(metadata: AlignmentMetadata): Future[AlignmentMetadata]
2116

22-
/**
23-
* Creates or updates coverage statistics for an alignment.
24-
*
25-
* @param coverage The coverage statistics to upsert
26-
* @return A future containing the upserted coverage record
27-
*/
28-
def upsertCoverage(coverage: AlignmentCoverage): Future[AlignmentCoverage]
29-
30-
/**
31-
* Finds all alignment metadata for a specific sequence file.
32-
*
33-
* @param sequenceFileId The sequence file ID
34-
* @return A future containing a sequence of alignment metadata records
35-
*/
17+
def updateCoverage(metadataId: Long, coverage: EmbeddedCoverage): Future[Boolean]
18+
3619
def findMetadataBySequenceFile(sequenceFileId: Long): Future[Seq[AlignmentMetadata]]
3720

38-
/**
39-
* Finds alignment metadata for a specific contig.
40-
*
41-
* @param genbankContigId The GenBank contig ID
42-
* @param metricLevel Optional filter by metric level
43-
* @return A future containing a sequence of alignment metadata records
44-
*/
4521
def findMetadataByContig(genbankContigId: Int, metricLevel: Option[MetricLevel] = None): Future[Seq[AlignmentMetadata]]
4622

47-
/**
48-
* Retrieves alignment metadata with associated coverage statistics.
49-
*
50-
* @param metadataId The alignment metadata ID
51-
* @return A future containing an optional tuple of (metadata, coverage)
52-
*/
53-
def findMetadataWithCoverage(metadataId: Long): Future[Option[(AlignmentMetadata, Option[AlignmentCoverage])]]
54-
55-
/**
56-
* Finds all alignment metadata and coverage for a sequence file.
57-
*
58-
* @param sequenceFileId The sequence file ID
59-
* @return A future containing a sequence of tuples (metadata, coverage)
60-
*/
61-
def findAllWithCoverageBySequenceFile(sequenceFileId: Long): Future[Seq[(AlignmentMetadata, Option[AlignmentCoverage])]]
62-
63-
/**
64-
* Deletes alignment metadata and associated coverage by ID.
65-
*
66-
* @param metadataId The alignment metadata ID to delete
67-
* @return A future containing the number of deleted records
68-
*/
23+
def findMetadataById(metadataId: Long): Future[Option[AlignmentMetadata]]
24+
25+
def findAllBySequenceFile(sequenceFileId: Long): Future[Seq[AlignmentMetadata]]
26+
6927
def deleteMetadata(metadataId: Long): Future[Int]
7028

71-
/**
72-
* Finds regional alignment statistics for a specific genomic region.
73-
*
74-
* @param genbankContigId The GenBank contig ID
75-
* @param startPos Start position (1-based, inclusive)
76-
* @param endPos End position (1-based, inclusive)
77-
* @return A future containing a sequence of regional alignment metadata
78-
*/
7929
def findRegionalMetadata(genbankContigId: Int, startPos: Long, endPos: Long): Future[Seq[AlignmentMetadata]]
8030
}
8131

@@ -89,7 +39,6 @@ class AlignmentRepositoryImpl @Inject()(
8939
import models.dal.MyPostgresProfile.api.*
9040

9141
private val metadataTable = DatabaseSchema.domain.genomics.alignmentMetadata
92-
private val coverageTable = DatabaseSchema.domain.genomics.alignmentCoverages
9342

9443
override def createMetadata(metadata: AlignmentMetadata): Future[AlignmentMetadata] = {
9544
val insertQuery = (metadataTable returning metadataTable.map(_.id)
@@ -99,12 +48,12 @@ class AlignmentRepositoryImpl @Inject()(
9948
db.run(insertQuery.transactionally)
10049
}
10150

102-
override def upsertCoverage(coverage: AlignmentCoverage): Future[AlignmentCoverage] = {
51+
override def updateCoverage(metadataId: Long, coverage: EmbeddedCoverage): Future[Boolean] = {
10352
db.run(
104-
coverageTable.insertOrUpdate(coverage)
105-
.map(_ => coverage)
106-
.transactionally
107-
)
53+
metadataTable.filter(_.id === metadataId)
54+
.map(_.coverage)
55+
.update(Some(Json.toJson(coverage)))
56+
).map(_ > 0)
10857
}
10958

11059
override def findMetadataBySequenceFile(sequenceFileId: Long): Future[Seq[AlignmentMetadata]] = {
@@ -125,23 +74,19 @@ class AlignmentRepositoryImpl @Inject()(
12574
db.run(filteredQuery.result)
12675
}
12776

128-
override def findMetadataWithCoverage(metadataId: Long): Future[Option[(AlignmentMetadata, Option[AlignmentCoverage])]] = {
77+
override def findMetadataById(metadataId: Long): Future[Option[AlignmentMetadata]] = {
12978
db.run(
13079
metadataTable
13180
.filter(_.id === metadataId)
132-
.joinLeft(coverageTable)
133-
.on(_.id === _.alignmentMetadataId)
13481
.result
13582
.headOption
13683
)
13784
}
13885

139-
override def findAllWithCoverageBySequenceFile(sequenceFileId: Long): Future[Seq[(AlignmentMetadata, Option[AlignmentCoverage])]] = {
86+
override def findAllBySequenceFile(sequenceFileId: Long): Future[Seq[AlignmentMetadata]] = {
14087
db.run(
14188
metadataTable
14289
.filter(_.sequenceFileId === sequenceFileId)
143-
.joinLeft(coverageTable)
144-
.on(_.id === _.alignmentMetadataId)
14590
.result
14691
)
14792
}
@@ -168,4 +113,4 @@ class AlignmentRepositoryImpl @Inject()(
168113
.result
169114
)
170115
}
171-
}
116+
}

0 commit comments

Comments
 (0)