Skip to content

Commit 12e542a

Browse files
Fix for VB-6479 - to add a sort order to paged results to ensure results are not repeated in subsequent paged results.
1 parent fcaa854 commit 12e542a

File tree

4 files changed

+80
-23
lines changed

4 files changed

+80
-23
lines changed

src/main/kotlin/uk/gov/justice/digital/hmpps/personalrelationships/repository/ContactSearchRepository.kt

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
2424
select c.contactId
2525
from ContactEntity c
2626
where c.dateOfBirth = :dateOfBirth
27-
order by c.contactId
2827
""",
2928
)
3029
fun findAllByDateOfBirthEquals(dateOfBirth: LocalDate, pageable: Pageable): Page<Long>
@@ -37,7 +36,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
3736
and (:lastName is null or c.lastName ilike %:lastName% escape '#')
3837
and (:firstName is null or c.firstName ilike %:firstName% escape '#')
3938
and (:middleNames is null or c.middleNames ilike %:middleNames% escape '#')
40-
order by c.contactId
4139
""",
4240
)
4341
fun findAllByDateOfBirthAndNamesMatch(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -50,7 +48,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
5048
and (:lastName is null or c.lastName ilike :lastName)
5149
and (:firstName is null or c.firstName ilike :firstName)
5250
and (:middleNames is null or c.middleNames ilike :middleNames)
53-
order by c.contactId
5451
""",
5552
)
5653
fun findAllByDateOfBirthAndNamesExact(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -63,7 +60,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
6360
and ( :lastName is null or c.lastNameSoundex = CAST(function('soundex', CAST(:lastName AS string)) AS char(4)))
6461
and ( :firstName is null or c.firstNameSoundex = CAST(function('soundex', CAST(:firstName AS string)) AS char(4)))
6562
and (:middleNames is null or c.middleNamesSoundex = CAST(function('soundex', CAST(:middleNames AS string)) AS char(4)))
66-
order by c.contactId
6763
""",
6864
)
6965
fun findAllByDateOfBirthAndNamesSoundLike(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -75,7 +71,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
7571
where (:lastName is null or c.lastName ilike :lastName)
7672
and (:firstName is null or c.firstName ilike :firstName)
7773
and (:middleNames is null or c.middleNames ilike :middleNames)
78-
order by c.contactId
7974
""",
8075
)
8176
fun findAllByNamesExact(firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -87,7 +82,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
8782
where (:lastName is null or c.lastName ilike %:lastName% escape '#')
8883
and (:firstName is null or c.firstName ilike %:firstName% escape '#')
8984
and (:middleNames is null or c.middleNames ilike %:middleNames% escape '#')
90-
order by c.contactId
9185
""",
9286
)
9387
fun findAllByNamesMatch(firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -99,7 +93,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
9993
where (:lastName is null or c.lastNameSoundex = CAST(function('soundex', CAST(:lastName AS string)) AS char(4)))
10094
and (:firstName is null or c.firstNameSoundex = CAST(function('soundex', CAST(:firstName AS string)) AS char(4)))
10195
and (:middleNames is null or c.middleNamesSoundex = CAST(function('soundex', CAST(:middleNames AS string)) AS char(4)))
102-
order by c.contactId
10396
""",
10497
)
10598
fun findAllByNamesSoundLike(firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -114,8 +107,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
114107
and (:firstName is null or ca.firstName ilike :firstName)
115108
and (:middleNames is null or ca.middleNames ilike :middleNames)
116109
and ca.revType in (0, 1)
117-
)
118-
order by c.contactId
110+
)
119111
""",
120112
)
121113
fun findAllByNamesExactAndHistory(firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -134,7 +126,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
134126
select c.contact_id
135127
from contact c
136128
where c.contact_id in (select contact_id from filtered_contacts)
137-
order by c.contact_id
138129
""",
139130
countQuery = """
140131
with filtered_contacts AS (
@@ -168,7 +159,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
168159
select c.contact_id
169160
from contact c
170161
where c.contact_id in (select contact_id from filtered_contacts)
171-
order by c.contact_id
172162
""",
173163
countQuery = """
174164
with filtered_contacts AS (
@@ -201,7 +191,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
201191
and (:middleNames is null or ca.middleNames ilike :middleNames)
202192
and ca.revType in (0, 1)
203193
)
204-
order by c.contactId
205194
""",
206195
)
207196
fun findAllByDateOfBirthAndNamesExactAndHistory(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -218,8 +207,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
218207
and (:firstName is null or ca.firstName ilike %:firstName% escape '#')
219208
and (:middleNames is null or ca.middleNames ilike %:middleNames% escape '#')
220209
and ca.revType in (0, 1)
221-
)
222-
order by c.contactId
210+
)
223211
""",
224212
)
225213
fun findAllByDateOfBirthAndNamesMatchAndHistory(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -237,7 +225,6 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
237225
and (:middleNames is null or ca.middleNamesSoundex = CAST(function('soundex', CAST(:middleNames as string)) AS char(4)))
238226
and ca.revType in (0, 1)
239227
)
240-
order by c.contactId
241228
""",
242229
)
243230
fun findAllByDateOfBirthAndNamesSoundLikeAndHistory(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>

src/main/kotlin/uk/gov/justice/digital/hmpps/personalrelationships/service/ContactSearchService.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,20 @@ class ContactSearchService(
4242
) {
4343
fun searchContacts(request: ContactSearchRequest, pageable: Pageable): Page<ContactSearchResultItem> {
4444
validateRequest(request)
45-
val pageOfContactIds = getPageOfContactIds(request, pageable)
45+
46+
// force adding contactId sort to the pageable as without it same ID is encountered across multiple pages.
47+
val pageableWithIdSort = PageRequest.of(
48+
pageable.pageNumber,
49+
pageable.pageSize,
50+
pageable.sort.and(Sort.by("contactId").ascending()),
51+
)
52+
53+
val pageOfContactIds = getPageOfContactIds(request, pageableWithIdSort)
4654

4755
logger.info("PageOfContacts is (elementsInThisPage) ${pageOfContactIds.content.size} and (totalElements) ${pageOfContactIds.totalElements}")
4856

4957
return if (!pageOfContactIds.isEmpty) {
50-
enrichOnePage(pageOfContactIds, request, pageable, pageOfContactIds.totalElements)
58+
enrichOnePage(pageOfContactIds, request, pageableWithIdSort, pageOfContactIds.totalElements)
5159
} else {
5260
Page.empty()
5361
}

src/test/kotlin/uk/gov/justice/digital/hmpps/personalrelationships/integration/resource/SearchContactsPaginationIntegrationTest.kt

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.springframework.web.util.UriComponentsBuilder
99
import uk.gov.justice.digital.hmpps.personalrelationships.integration.SecureAPIIntegrationTestBase
1010
import uk.gov.justice.digital.hmpps.personalrelationships.util.StubUser
1111
import java.net.URI
12+
import java.time.LocalDate
1213
import kotlin.collections.toList
1314

1415
class SearchContactsPaginationIntegrationTest : SecureAPIIntegrationTestBase() {
@@ -25,22 +26,56 @@ class SearchContactsPaginationIntegrationTest : SecureAPIIntegrationTestBase() {
2526
.accept(MediaType.APPLICATION_JSON)
2627

2728
@Test
28-
fun `when contacts search is done then proper search results are returned irrespective of sort order`() {
29+
fun `when contacts search with results sorted by last name ascending then all results are returned in correct order`() {
2930
// test to check fix for VB-6479
3031
// 50 records have been added with lastName starting with ABCD and firstName as Test to replicate live scenario
3132
// starting from contact ID 2001 to 2050
3233
// the asserts check that all Ids starting from 2001 to 2050 are being returned which was not the case before the fix
33-
var sortValues = listOf("lastName,asc")
34+
val sortValues = listOf("lastName,asc")
35+
36+
// assert that all Ids from 2001 t0 2050 are there
3437
assertPagedData(sortValues)
38+
val contactLastNames = getLastNames(sortValues)
39+
val sortedContactLastNames = contactLastNames.sorted()
40+
for (i in 0 until sortedContactLastNames.size) {
41+
assertThat(contactLastNames[i]).isEqualTo(sortedContactLastNames[i])
42+
}
43+
}
3544

36-
sortValues = listOf("lastName,desc")
45+
@Test
46+
fun `when contacts search with results sorted by last name descending then all results are returned in correct order`() {
47+
val sortValues = listOf("lastName,desc")
3748
assertPagedData(sortValues)
49+
val contactLastNamesDesc = getLastNames(sortValues)
50+
val sortedContactLastNamesDesc = contactLastNamesDesc.sortedDescending()
51+
52+
for (i in 0 until sortedContactLastNamesDesc.size) {
53+
assertThat(contactLastNamesDesc[i]).isEqualTo(sortedContactLastNamesDesc[i])
54+
}
55+
}
3856

39-
sortValues = listOf("dateOfBirth,asc")
57+
@Test
58+
fun `when contacts search with results sorted by dateOfBirth ascending then all results are returned in correct order`() {
59+
val sortValues = listOf("dateOfBirth,asc")
4060
assertPagedData(sortValues)
61+
val contactDOBs = getDateOfBirths(sortValues)
62+
val sortedDOBs = contactDOBs.sortedWith(nullsLast(naturalOrder()))
63+
64+
for (i in 0 until sortedDOBs.size) {
65+
assertThat(contactDOBs[i]).isEqualTo(sortedDOBs[i])
66+
}
67+
}
4168

42-
sortValues = listOf("dateOfBirth,desc")
69+
@Test
70+
fun `when contacts search with results sorted by dateOfBirth descending then all results are returned in correct order`() {
71+
val sortValues = listOf("dateOfBirth,desc")
4372
assertPagedData(sortValues)
73+
val contactDOBsDesc = getDateOfBirths(sortValues)
74+
val sortedDOBsDesc = contactDOBsDesc.sortedWith(nullsFirst(reverseOrder()))
75+
76+
for (i in 0 until contactDOBsDesc.size) {
77+
assertThat(contactDOBsDesc[i]).isEqualTo(sortedDOBsDesc[i])
78+
}
4479
}
4580

4681
private fun getContactSearchUrl(pageNumber: Int? = 0, sortValues: List<String>): URI {
@@ -49,7 +84,7 @@ class SearchContactsPaginationIntegrationTest : SecureAPIIntegrationTestBase() {
4984
.queryParam("lastName", "ABCD")
5085
.queryParam("firstName", "Test")
5186
.queryParam("page", pageNumber)
52-
.queryParam("sort", sortValues.joinToString(","))
87+
.queryParam("sort", sortValues)
5388
.build()
5489
.toUri()
5590

@@ -87,6 +122,32 @@ class SearchContactsPaginationIntegrationTest : SecureAPIIntegrationTestBase() {
87122
assertThat(contactIds.max()).isEqualTo(2050)
88123
}
89124

125+
private fun getLastNames(sortValues: List<String>): List<String> {
126+
val contactLastNames = mutableListOf<String>()
127+
128+
// iterate over the 5 pages and collate all results into a list
129+
for (pageNumber in 0..4) {
130+
val uri = getContactSearchUrl(pageNumber, sortValues)
131+
val body = testAPIClient.getSearchContactResults(uri)
132+
contactLastNames.addAll(body!!.content.map { it.lastName })
133+
}
134+
135+
return contactLastNames
136+
}
137+
138+
private fun getDateOfBirths(sortValues: List<String>): List<LocalDate?> {
139+
val contactDateOfBirths = mutableListOf<LocalDate?>()
140+
141+
// iterate over the 5 pages and collate all results into a list
142+
for (pageNumber in 0..4) {
143+
val uri = getContactSearchUrl(pageNumber, sortValues)
144+
val body = testAPIClient.getSearchContactResults(uri)
145+
contactDateOfBirths.addAll(body!!.content.map { it.dateOfBirth })
146+
}
147+
148+
return contactDateOfBirths
149+
}
150+
90151
companion object {
91152
private val CONTACT_SEARCH_URL = UriComponentsBuilder.fromPath("contact/search")
92153
.queryParam("searchType", "PARTIAL")

src/test/kotlin/uk/gov/justice/digital/hmpps/personalrelationships/service/ContactSearchServiceTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ class ContactSearchServiceTest {
141141
Sort.Order(Sort.Direction.ASC, "last_name"),
142142
Sort.Order(Sort.Direction.ASC, "first_name"),
143143
Sort.Order(Sort.Direction.ASC, "date_of_birth"),
144+
Sort.Order(Sort.Direction.ASC, "contact_id"),
144145
)
145146
}
146147

0 commit comments

Comments
 (0)