test #433
@@ -18,17 +18,30 @@ class RecommendationSnapshotPersistenceAdapter(
|
|||||||
return repository.findLatestSnapshots(sectionType.name, offset, limit).map { it.toRecord() }
|
return repository.findLatestSnapshots(sectionType.name, offset, limit).map { it.toRecord() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun findSnapshots(
|
||||||
|
sectionType: RecommendedSectionType,
|
||||||
|
snapshotAt: LocalDateTime,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int
|
||||||
|
): List<RecommendationSnapshotRecord> {
|
||||||
|
return repository.findSnapshots(sectionType.name, snapshotAt, offset, limit).map { it.toRecord() }
|
||||||
|
}
|
||||||
|
|
||||||
override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean {
|
override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean {
|
||||||
return repository.existsBySectionType(sectionType)
|
return repository.existsBySectionType(sectionType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean {
|
||||||
|
return repository.existsBySectionTypeAndSnapshotAt(sectionType, snapshotAt)
|
||||||
|
}
|
||||||
|
|
||||||
override fun replaceSnapshots(
|
override fun replaceSnapshots(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
snapshotAt: LocalDateTime,
|
snapshotAt: LocalDateTime,
|
||||||
newSnapshots: List<RecommendationSnapshotRecord>
|
newSnapshots: List<RecommendationSnapshotRecord>
|
||||||
) {
|
) {
|
||||||
if (newSnapshots.isEmpty() && sectionType == RecommendedSectionType.AI_CHARACTER) {
|
if (newSnapshots.isEmpty() && supportsEmptySnapshotMarker(sectionType)) {
|
||||||
repository.deleteBySectionType(sectionType)
|
repository.deleteBySectionTypeAndSnapshotAt(sectionType, snapshotAt)
|
||||||
repository.save(emptySnapshotMarker(sectionType, snapshotAt).toEntity())
|
repository.save(emptySnapshotMarker(sectionType, snapshotAt).toEntity())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -70,6 +83,10 @@ class RecommendationSnapshotPersistenceAdapter(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun supportsEmptySnapshotMarker(sectionType: RecommendedSectionType): Boolean {
|
||||||
|
return sectionType == RecommendedSectionType.AI_CHARACTER || sectionType == RecommendedSectionType.CHEER_CREATOR
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EMPTY_SNAPSHOT_MARKER_TARGET_ID = 0L
|
private const val EMPTY_SNAPSHOT_MARKER_TARGET_ID = 0L
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,28 @@ interface RecommendationSnapshotRepository : JpaRepository<RecommendationSnapsho
|
|||||||
@Param("limit") limit: Int
|
@Param("limit") limit: Int
|
||||||
): List<RecommendationSnapshot>
|
): List<RecommendationSnapshot>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
value = """
|
||||||
|
select *
|
||||||
|
from recommendation_snapshot rs
|
||||||
|
where rs.section_type = :sectionType
|
||||||
|
and rs.snapshot_at = :snapshotAt
|
||||||
|
and rs.target_id <> 0
|
||||||
|
order by rs.score desc, rs.random_tie_breaker asc
|
||||||
|
limit :limit offset :offset
|
||||||
|
""",
|
||||||
|
nativeQuery = true
|
||||||
|
)
|
||||||
|
fun findSnapshots(
|
||||||
|
@Param("sectionType") sectionType: String,
|
||||||
|
@Param("snapshotAt") snapshotAt: LocalDateTime,
|
||||||
|
@Param("offset") offset: Long,
|
||||||
|
@Param("limit") limit: Int
|
||||||
|
): List<RecommendationSnapshot>
|
||||||
|
|
||||||
fun deleteBySectionTypeAndSnapshotAt(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime)
|
fun deleteBySectionTypeAndSnapshotAt(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime)
|
||||||
|
|
||||||
fun deleteBySectionType(sectionType: RecommendedSectionType)
|
|
||||||
|
|
||||||
fun existsBySectionType(sectionType: RecommendedSectionType): Boolean
|
fun existsBySectionType(sectionType: RecommendedSectionType): Boolean
|
||||||
|
|
||||||
|
fun existsBySectionTypeAndSnapshotAt(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,17 @@ interface RecommendationSnapshotPort {
|
|||||||
limit: Int = Int.MAX_VALUE
|
limit: Int = Int.MAX_VALUE
|
||||||
): List<RecommendationSnapshotRecord>
|
): List<RecommendationSnapshotRecord>
|
||||||
|
|
||||||
|
fun findSnapshots(
|
||||||
|
sectionType: RecommendedSectionType,
|
||||||
|
snapshotAt: LocalDateTime,
|
||||||
|
offset: Long = 0,
|
||||||
|
limit: Int = Int.MAX_VALUE
|
||||||
|
): List<RecommendationSnapshotRecord>
|
||||||
|
|
||||||
fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean
|
fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean
|
||||||
|
|
||||||
|
fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean
|
||||||
|
|
||||||
fun replaceSnapshots(
|
fun replaceSnapshots(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
snapshotAt: LocalDateTime,
|
snapshotAt: LocalDateTime,
|
||||||
|
|||||||
@@ -77,6 +77,61 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor(
|
|||||||
assertEquals(listOf(2L, 3L), snapshots.map { it.targetId })
|
assertEquals(listOf(2L, 3L), snapshots.map { it.targetId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldFindSnapshotsByExactSnapshotAtAndExcludeEmptyMarker() {
|
||||||
|
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
|
||||||
|
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||||
|
repository.saveAll(
|
||||||
|
listOf(
|
||||||
|
snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 1L, score = 999.0, snapshotAt = oldSnapshotAt),
|
||||||
|
snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 0L, score = 0.0, snapshotAt = snapshotAt),
|
||||||
|
snapshot(
|
||||||
|
RecommendedSectionType.CHEER_CREATOR,
|
||||||
|
targetId = 2L,
|
||||||
|
score = 100.0,
|
||||||
|
snapshotAt = snapshotAt,
|
||||||
|
randomTieBreaker = 0.9
|
||||||
|
),
|
||||||
|
snapshot(
|
||||||
|
RecommendedSectionType.CHEER_CREATOR,
|
||||||
|
targetId = 3L,
|
||||||
|
score = 200.0,
|
||||||
|
snapshotAt = snapshotAt,
|
||||||
|
randomTieBreaker = 0.8
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val snapshots = adapter.findSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, offset = 0, limit = 10)
|
||||||
|
|
||||||
|
assertEquals(listOf(3L, 2L), snapshots.map { it.targetId })
|
||||||
|
assertEquals(listOf(snapshotAt, snapshotAt), snapshots.map { it.snapshotAt })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldCheckSnapshotExistenceByExactSnapshotAtIncludingEmptyMarker() {
|
||||||
|
val staleSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
|
||||||
|
val expectedSnapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||||
|
repository.save(
|
||||||
|
snapshot(
|
||||||
|
RecommendedSectionType.CHEER_CREATOR,
|
||||||
|
targetId = 1L,
|
||||||
|
score = 100.0,
|
||||||
|
snapshotAt = staleSnapshotAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(false, adapter.existsSnapshot(RecommendedSectionType.CHEER_CREATOR, expectedSnapshotAt))
|
||||||
|
|
||||||
|
adapter.replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, expectedSnapshotAt, emptyList())
|
||||||
|
|
||||||
|
assertEquals(true, adapter.existsSnapshot(RecommendedSectionType.CHEER_CREATOR, expectedSnapshotAt))
|
||||||
|
assertEquals(
|
||||||
|
emptyList<RecommendationSnapshotRecord>(),
|
||||||
|
adapter.findSnapshots(RecommendedSectionType.CHEER_CREATOR, expectedSnapshotAt)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldReplaceSnapshotsByDeletingSameSectionSnapshotAtOnly() {
|
fun shouldReplaceSnapshotsByDeletingSameSectionSnapshotAtOnly() {
|
||||||
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
|
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
|
||||||
@@ -109,7 +164,7 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun shouldClearAiCharacterSectionSnapshotsWhenReplacingWithEmptySnapshots() {
|
fun shouldSaveAiCharacterEmptySnapshotMarkerWhenReplacingWithEmptySnapshots() {
|
||||||
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
|
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
|
||||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||||
repository.saveAll(
|
repository.saveAll(
|
||||||
@@ -125,9 +180,10 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor(
|
|||||||
assertEquals(listOf(2L), adapter.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId })
|
assertEquals(listOf(2L), adapter.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId })
|
||||||
assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.AI_CHARACTER))
|
assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.AI_CHARACTER))
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf(0L),
|
listOf(1L, 0L),
|
||||||
repository.findAll()
|
repository.findAll()
|
||||||
.filter { it.sectionType == RecommendedSectionType.AI_CHARACTER }
|
.filter { it.sectionType == RecommendedSectionType.AI_CHARACTER }
|
||||||
|
.sortedBy { it.snapshotAt }
|
||||||
.map { it.targetId }
|
.map { it.targetId }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -142,6 +198,53 @@ class RecommendationSnapshotPersistenceAdapterTest @Autowired constructor(
|
|||||||
assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.AI_CHARACTER))
|
assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.AI_CHARACTER))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldSaveCheerCreatorEmptySnapshotMarkerWhenReplacingWithEmptySnapshots() {
|
||||||
|
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
|
||||||
|
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||||
|
repository.save(snapshot(RecommendedSectionType.CHEER_CREATOR, targetId = 1L, score = 100.0, snapshotAt = oldSnapshotAt))
|
||||||
|
|
||||||
|
adapter.replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, emptyList())
|
||||||
|
|
||||||
|
assertEquals(emptyList<RecommendationSnapshotRecord>(), adapter.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR))
|
||||||
|
assertEquals(true, adapter.existsLatestSnapshot(RecommendedSectionType.CHEER_CREATOR))
|
||||||
|
assertEquals(
|
||||||
|
listOf(1L, 0L),
|
||||||
|
repository.findAll()
|
||||||
|
.filter { it.sectionType == RecommendedSectionType.CHEER_CREATOR }
|
||||||
|
.sortedBy { it.snapshotAt }
|
||||||
|
.map { it.targetId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun shouldReplaceCheerCreatorEmptyMarkerWithRealSnapshots() {
|
||||||
|
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||||
|
adapter.replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, emptyList())
|
||||||
|
|
||||||
|
adapter.replaceSnapshots(
|
||||||
|
RecommendedSectionType.CHEER_CREATOR,
|
||||||
|
snapshotAt,
|
||||||
|
listOf(
|
||||||
|
RecommendationSnapshotRecord(
|
||||||
|
sectionType = RecommendedSectionType.CHEER_CREATOR,
|
||||||
|
targetId = 9L,
|
||||||
|
score = 90.0,
|
||||||
|
snapshotAt = snapshotAt,
|
||||||
|
randomTieBreaker = 0.9
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(9L), adapter.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).map { it.targetId })
|
||||||
|
assertEquals(
|
||||||
|
listOf(9L),
|
||||||
|
repository.findAll()
|
||||||
|
.filter { it.sectionType == RecommendedSectionType.CHEER_CREATOR }
|
||||||
|
.map { it.targetId }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun snapshot(
|
private fun snapshot(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
targetId: Long,
|
targetId: Long,
|
||||||
|
|||||||
Reference in New Issue
Block a user