Skip to content

Commit c2e2cef

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 f63ddd5 commit c2e2cef

File tree

3 files changed

+169
-2
lines changed

3 files changed

+169
-2
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
2424
select c.contactId
2525
from ContactEntity c
2626
where c.dateOfBirth = :dateOfBirth
27+
order by c.contactId
2728
""",
2829
)
2930
fun findAllByDateOfBirthEquals(dateOfBirth: LocalDate, pageable: Pageable): Page<Long>
@@ -36,6 +37,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
3637
and (:lastName is null or c.lastName ilike %:lastName% escape '#')
3738
and (:firstName is null or c.firstName ilike %:firstName% escape '#')
3839
and (:middleNames is null or c.middleNames ilike %:middleNames% escape '#')
40+
order by c.contactId
3941
""",
4042
)
4143
fun findAllByDateOfBirthAndNamesMatch(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -48,6 +50,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
4850
and (:lastName is null or c.lastName ilike :lastName)
4951
and (:firstName is null or c.firstName ilike :firstName)
5052
and (:middleNames is null or c.middleNames ilike :middleNames)
53+
order by c.contactId
5154
""",
5255
)
5356
fun findAllByDateOfBirthAndNamesExact(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -60,6 +63,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
6063
and ( :lastName is null or c.lastNameSoundex = CAST(function('soundex', CAST(:lastName AS string)) AS char(4)))
6164
and ( :firstName is null or c.firstNameSoundex = CAST(function('soundex', CAST(:firstName AS string)) AS char(4)))
6265
and (:middleNames is null or c.middleNamesSoundex = CAST(function('soundex', CAST(:middleNames AS string)) AS char(4)))
66+
order by c.contactId
6367
""",
6468
)
6569
fun findAllByDateOfBirthAndNamesSoundLike(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -71,6 +75,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
7175
where (:lastName is null or c.lastName ilike :lastName)
7276
and (:firstName is null or c.firstName ilike :firstName)
7377
and (:middleNames is null or c.middleNames ilike :middleNames)
78+
order by c.contactId
7479
""",
7580
)
7681
fun findAllByNamesExact(firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -82,6 +87,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
8287
where (:lastName is null or c.lastName ilike %:lastName% escape '#')
8388
and (:firstName is null or c.firstName ilike %:firstName% escape '#')
8489
and (:middleNames is null or c.middleNames ilike %:middleNames% escape '#')
90+
order by c.contactId
8591
""",
8692
)
8793
fun findAllByNamesMatch(firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -93,6 +99,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
9399
where (:lastName is null or c.lastNameSoundex = CAST(function('soundex', CAST(:lastName AS string)) AS char(4)))
94100
and (:firstName is null or c.firstNameSoundex = CAST(function('soundex', CAST(:firstName AS string)) AS char(4)))
95101
and (:middleNames is null or c.middleNamesSoundex = CAST(function('soundex', CAST(:middleNames AS string)) AS char(4)))
102+
order by c.contactId
96103
""",
97104
)
98105
fun findAllByNamesSoundLike(firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -107,7 +114,8 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
107114
and (:firstName is null or ca.firstName ilike :firstName)
108115
and (:middleNames is null or ca.middleNames ilike :middleNames)
109116
and ca.revType in (0, 1)
110-
)
117+
)
118+
order by c.contactId
111119
""",
112120
)
113121
fun findAllByNamesExactAndHistory(firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -126,6 +134,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
126134
select c.contact_id
127135
from contact c
128136
where c.contact_id in (select contact_id from filtered_contacts)
137+
order by c.contact_id
129138
""",
130139
countQuery = """
131140
with filtered_contacts AS (
@@ -159,6 +168,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
159168
select c.contact_id
160169
from contact c
161170
where c.contact_id in (select contact_id from filtered_contacts)
171+
order by c.contact_id
162172
""",
163173
countQuery = """
164174
with filtered_contacts AS (
@@ -191,6 +201,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
191201
and (:middleNames is null or ca.middleNames ilike :middleNames)
192202
and ca.revType in (0, 1)
193203
)
204+
order by c.contactId
194205
""",
195206
)
196207
fun findAllByDateOfBirthAndNamesExactAndHistory(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -207,7 +218,8 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
207218
and (:firstName is null or ca.firstName ilike %:firstName% escape '#')
208219
and (:middleNames is null or ca.middleNames ilike %:middleNames% escape '#')
209220
and ca.revType in (0, 1)
210-
)
221+
)
222+
order by c.contactId
211223
""",
212224
)
213225
fun findAllByDateOfBirthAndNamesMatchAndHistory(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
@@ -225,6 +237,7 @@ interface ContactSearchRepository : JpaRepository<ContactEntity, Long> {
225237
and (:middleNames is null or ca.middleNamesSoundex = CAST(function('soundex', CAST(:middleNames as string)) AS char(4)))
226238
and ca.revType in (0, 1)
227239
)
240+
order by c.contactId
228241
""",
229242
)
230243
fun findAllByDateOfBirthAndNamesSoundLikeAndHistory(dateOfBirth: LocalDate, firstName: String?, middleNames: String?, lastName: String?, pageable: Pageable): Page<Long>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
-- ==========================================================
2+
-- Example data
3+
-- Not loaded into any real environments - DEV, PREPROD or PROD
4+
-- Intended for integration tests and local-running only.
5+
-- ===========================================================
6+
insert into public.contact (contact_id, title, last_name, first_name, middle_names, date_of_birth, gender, domestic_status, language_code, created_by, deceased_date, interpreter_required, staff_flag)
7+
values (2001,'MR','Abcdon', 'Test',null,'2000-11-21','M','M','ENG','TIM',null,false,true),
8+
(2002,'MR','Abcdon','Test',null,'1999-11-01','M','M','ENG','TIM',null,false,true),
9+
(2003,'MR','Abcdcott','Test',null,'1972-05-02','M','M','ENG','TIM',null,false,true),
10+
(2004,'MR','Abcdern','Test',null,'1932-05-02','M','M','ENG','TIM',null,false,true),
11+
(2005,'MR','Abcdern','Test',null,'2011-11-04','M','M','ENG','TIM',null,false,true),
12+
(2006,'MR','Abcdern','Test',null,'1980-03-21','M','M','ENG','TIM',null,false,true),
13+
(2007,'MR','Abcdern','Test',null,'2000-11-27','M','M','ENG','TIM',null,false,true),
14+
(2008,'MR','Abcd','Test',null,'1966-09-01','M','M','ENG','TIM',null,false,true),
15+
(2009,'MR','Abcd','Test',null,'1976-10-01','M','M','ENG','TIM',null,false,true),
16+
(2010,'MR','Abcdon','Test',null,'1976-11-01','M','M','ENG','TIM',null,false,true),
17+
(2011,'MR','Abcdon','Test',null,'1923-01-01','M','M','ENG','TIM',null,false,true),
18+
(2012,'MR','Abcdon','Test',null,'2000-12-02','M','M','ENG','TIM',null,false,true),
19+
(2013,'MR','Abcdy','Test',null,'1923-01-03','M','M','ENG','TIM',null,false,true),
20+
(2014,'MR','Abcd','Test',null,'1955-02-04','M','M','ENG','TIM',null,false,true),
21+
(2015,'MR','Abcdern','Test',null,'2013-05-01','M','M','ENG','TIM',null,false,true),
22+
(2016,'MR','Abcdon','Test',null,'2012-08-21','M','M','ENG','TIM',null,false,true),
23+
(2017,'MR','Abcd','Test',null,'2011=03=11','M','M','ENG','TIM',null,false,true),
24+
(2018,'MR','Abcdcott','Test',null,'1987-01-01','M','M','ENG','TIM',null,false,true),
25+
(2019,'MR','Abcdon','Test',null,'1986-05-04','M','M','ENG','TIM',null,false,true),
26+
(2020,'MR','Abcdcott','Test',null,'1965-06-02','M','M','ENG','TIM',null,false,true),
27+
(2021,'MR','Abcdcott','Test',null,'1987-09-01','M','M','ENG','TIM',null,false,true),
28+
(2022,'MR','Abcdern','Test',null,'1987-01-04','M','M','ENG','TIM',null,false,true),
29+
(2023,'MR','Abcd','Test',null,'1985-05-26','M','M','ENG','TIM',null,false,true),
30+
(2024,'MR','Abcd','Test',null,'1955-08-13','M','M','ENG','TIM',null,false,true),
31+
(2025,'MR','Abcdon','Test',null,'1966-04-19','M','M','ENG','TIM',null,false,true),
32+
(2026,'MR','Abcd','Test',null,'1977-04-11','M','M','ENG','TIM',null,false,true),
33+
(2027,'MR','Abcdwood','Test',null,'1999-09-11','M','M','ENG','TIM',null,false,true),
34+
(2028,'MR','Abcdon','Test',null,'1976-04-01','M','M','ENG','TIM',null,false,true),
35+
(2029,'MR','Abcdbury','Test',null,'1930-05-01','M','M','ENG','TIM',null,false,true),
36+
(2030,'MR','Abcdon','Test',null,'2021-12-10','M','M','ENG','TIM',null,false,true),
37+
(2031,'MR','Abcdwood','Test',null,'1955-03-01','M','M','ENG','TIM',null,false,true),
38+
(2032,'MR','Abcd','Test',null,'2002-06-01','M','M','ENG','TIM',null,false,true),
39+
(2033,'MR','Abcd','Test',null,'1994-03-01','M','M','ENG','TIM',null,false,true),
40+
(2034,'MR','Abcdon','Test',null,'1983-11-10','M','M','ENG','TIM',null,false,true),
41+
(2035,'MR','Abcdon','Test',null,'1990-12-07','M','M','ENG','TIM',null,false,true),
42+
(2036,'MR','Abcdon','Test',null,'1945-04-05','M','M','ENG','TIM',null,false,true),
43+
(2037,'MR','Abcdwood','Test',null,'1922-03-02','M','M','ENG','TIM',null,false,true),
44+
(2038,'MR','Abcdon','Test',null,'2012-08-11','M','M','ENG','TIM',null,false,true),
45+
(2039,'MR','Abcd','Test',null,'1966-09-11','M','M','ENG','TIM',null,false,true),
46+
(2040,'MR','Abcdon','Test',null,'1984-03-21','M','M','ENG','TIM',null,false,true),
47+
(2041,'MR','Abcd','Test',null,null,'M','M','ENG','TIM',null,false,true),
48+
(2042,'MR','Abcd','Test',null,null,'M','M','ENG','TIM',null,false,true),
49+
(2043,'MR','Abcdern','Test',null,null,'M','M','ENG','TIM',null,false,true),
50+
(2044,'MR','Abcdon','Test',null,null,'M','M','ENG','TIM',null,false,true),
51+
(2045,'MR','Abcdern','Test',null,null,'M','M','ENG','TIM',null,false,true),
52+
(2046,'MR','Abcdon','Test',null,null,'M','M','ENG','TIM',null,false,true),
53+
(2047,'MR','Abcdon','Test',null,null,'M','M','ENG','TIM',null,false,true),
54+
(2048,'MR','Abcdon','Test',null,null,'M','M','ENG','TIM',null,false,true),
55+
(2049,'MR','Abcdon','Test',null,null,'M','M','ENG','TIM',null,false,true),
56+
(2050,'MR','Abcdon','Test',null,null,'M','M','ENG','TIM',null,false,true);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package uk.gov.justice.digital.hmpps.personalrelationships.integration.resource
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.jupiter.api.BeforeEach
5+
import org.junit.jupiter.api.Test
6+
import org.springframework.http.MediaType
7+
import org.springframework.test.web.reactive.server.WebTestClient
8+
import org.springframework.web.util.UriComponentsBuilder
9+
import uk.gov.justice.digital.hmpps.personalrelationships.integration.SecureAPIIntegrationTestBase
10+
import uk.gov.justice.digital.hmpps.personalrelationships.util.StubUser
11+
import java.net.URI
12+
import kotlin.collections.toList
13+
14+
class SearchContactsPaginationIntegrationTest : SecureAPIIntegrationTestBase() {
15+
16+
@BeforeEach
17+
fun setUp() {
18+
setCurrentUser(StubUser.READ_ONLY_USER)
19+
}
20+
21+
override val allowedRoles: Set<String> = setOf("ROLE_CONTACTS_ADMIN", "ROLE_CONTACTS__RW", "ROLE_CONTACTS__R")
22+
23+
override fun baseRequestBuilder(): WebTestClient.RequestHeadersSpec<*> = webTestClient.get()
24+
.uri(CONTACT_SEARCH_URL.toString())
25+
.accept(MediaType.APPLICATION_JSON)
26+
27+
@Test
28+
fun `when contacts search is done then proper search results are returned irrespective of sort order`() {
29+
// test to check fix for VB-6479
30+
// 50 records have been added with lastName starting with ABCD and firstName as Test to replicate live scenario
31+
// starting from contact ID 2001 to 2050
32+
// 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+
assertPagedData(sortValues)
35+
36+
sortValues = listOf("lastName,desc")
37+
assertPagedData(sortValues)
38+
39+
sortValues = listOf("dateOfBirth,asc")
40+
assertPagedData(sortValues)
41+
42+
sortValues = listOf("dateOfBirth,desc")
43+
assertPagedData(sortValues)
44+
}
45+
46+
private fun getContactSearchUrl(pageNumber: Int? = 0, sortValues: List<String>): URI {
47+
val uri = UriComponentsBuilder.fromPath("contact/search")
48+
.queryParam("searchType", "PARTIAL")
49+
.queryParam("lastName", "ABCD")
50+
.queryParam("firstName", "Test")
51+
.queryParam("page", pageNumber)
52+
.queryParam("sort", sortValues.joinToString(","))
53+
.build()
54+
.toUri()
55+
56+
return uri
57+
}
58+
59+
private fun assertPagedData(
60+
sortValues: List<String>,
61+
) {
62+
fun expectedIds(start: Long, end: Long) = (start..end).toList()
63+
var uri = getContactSearchUrl(0, sortValues)
64+
val contactIds = mutableListOf<Long>()
65+
var body = testAPIClient.getSearchContactResults(uri)
66+
67+
// assert that 50 records are returned - split into 5 pages
68+
with(body!!) {
69+
assertThat(content).isNotEmpty()
70+
assertThat(content.size).isEqualTo(10)
71+
assertThat(page.totalElements).isEqualTo(50)
72+
assertThat(page.totalPages).isEqualTo(5)
73+
assertThat(page.size).isEqualTo(10)
74+
}
75+
76+
// iterate over the 5 pages and collate all results into a list
77+
for (pageNumber in 0..4) {
78+
uri = getContactSearchUrl(pageNumber, sortValues)
79+
body = testAPIClient.getSearchContactResults(uri)
80+
contactIds.addAll(body!!.content.map { it.id })
81+
}
82+
83+
assertThat(contactIds.distinct()).hasSize(50)
84+
// all contactIds from 2001 to 2050 should be present in the list
85+
assertThat(contactIds).containsAll(expectedIds(2001, 2050))
86+
assertThat(contactIds.min()).isEqualTo(2001)
87+
assertThat(contactIds.max()).isEqualTo(2050)
88+
}
89+
90+
companion object {
91+
private val CONTACT_SEARCH_URL = UriComponentsBuilder.fromPath("contact/search")
92+
.queryParam("searchType", "PARTIAL")
93+
.queryParam("lastName", "ABCD")
94+
.queryParam("firstName", "Test")
95+
.build()
96+
.toUri()
97+
}
98+
}

0 commit comments

Comments
 (0)