From de2cc677a9720e7ef6952642e27383ca4b3d53f3 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 8 Apr 2026 11:59:41 +0200 Subject: [PATCH] fix(search): handle null firstName in all search queries Use COALESCE to convert null firstName to empty string in: - PersonRepository.searchByName (JPQL) - PersonRepository.searchWithDocumentCount (native SQL) - PersonRepository.findCorrespondentsWithFilter (native SQL) - DocumentSpecifications.hasText (Criteria API, sender + receiver) Co-Authored-By: Claude Sonnet 4.6 --- .../repository/DocumentSpecifications.java | 4 ++-- .../repository/PersonRepository.java | 12 +++++----- .../repository/PersonRepositoryTest.java | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentSpecifications.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentSpecifications.java index ee9550c1..d8d572bc 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentSpecifications.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/DocumentSpecifications.java @@ -41,7 +41,7 @@ public class DocumentSpecifications { cb.equal(receiverRoot.get("id"), root.get("id")), cb.or( cb.like(cb.lower(receiverJoin.get("lastName")), likePattern), - cb.like(cb.lower(receiverJoin.get("firstName")), likePattern) + cb.like(cb.lower(cb.coalesce(receiverJoin.get("firstName"), "")), likePattern) ) ); @@ -74,7 +74,7 @@ public class DocumentSpecifications { cb.like(cb.lower(root.get("transcription")), likePattern), cb.like(cb.lower(root.get("location")), likePattern), cb.like(cb.lower(senderJoin.get("lastName")), likePattern), - cb.like(cb.lower(senderJoin.get("firstName")), likePattern), + cb.like(cb.lower(cb.coalesce(senderJoin.get("firstName"), "")), likePattern), cb.like(cb.lower(senderAliasJoin.get("lastName")), likePattern), cb.exists(receiverSub), cb.exists(receiverAliasSub), diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java index abbed802..e41be561 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/PersonRepository.java @@ -16,8 +16,8 @@ import org.springframework.stereotype.Repository; public interface PersonRepository extends JpaRepository { @Query("SELECT DISTINCT p FROM Person p LEFT JOIN p.nameAliases a WHERE " + - "LOWER(CONCAT(p.firstName,' ',p.lastName)) LIKE LOWER(CONCAT('%', :query, '%')) OR " + - "LOWER(CONCAT(p.lastName, ' ', p.firstName)) LIKE LOWER(CONCAT('%', :query, '%')) OR " + + "LOWER(CONCAT(COALESCE(p.firstName, ''),' ',p.lastName)) LIKE LOWER(CONCAT('%', :query, '%')) OR " + + "LOWER(CONCAT(p.lastName, ' ', COALESCE(p.firstName, ''))) LIKE LOWER(CONCAT('%', :query, '%')) OR " + "LOWER(p.alias) LIKE LOWER(CONCAT('%', :query, '%')) OR " + "LOWER(a.lastName) LIKE LOWER(CONCAT('%', :query, '%')) " + "ORDER BY p.lastName ASC, p.firstName ASC") @@ -52,8 +52,8 @@ public interface PersonRepository extends JpaRepository { + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id) AS documentCount FROM persons p LEFT JOIN person_name_aliases a ON a.person_id = p.id - WHERE LOWER(CONCAT(p.first_name,' ',p.last_name)) LIKE LOWER(CONCAT('%',:query,'%')) - OR LOWER(CONCAT(p.last_name,' ',p.first_name)) LIKE LOWER(CONCAT('%',:query,'%')) + WHERE LOWER(CONCAT(COALESCE(p.first_name,''),' ',p.last_name)) LIKE LOWER(CONCAT('%',:query,'%')) + OR LOWER(CONCAT(p.last_name,' ',COALESCE(p.first_name,''))) LIKE LOWER(CONCAT('%',:query,'%')) OR LOWER(p.alias) LIKE LOWER(CONCAT('%',:query,'%')) OR LOWER(a.last_name) LIKE LOWER(CONCAT('%',:query,'%')) GROUP BY p.id, p.first_name, p.last_name, p.alias, p.birth_year, p.death_year, p.notes @@ -98,8 +98,8 @@ public interface PersonRepository extends JpaRepository { WHERE dr.person_id = :personId AND d.sender_id IS NOT NULL ) shared ON shared.other_id = p.id WHERE p.id != :personId - AND (LOWER(CONCAT(p.first_name,' ',p.last_name)) LIKE LOWER(CONCAT('%',:q,'%')) - OR LOWER(CONCAT(p.last_name,' ',p.first_name)) LIKE LOWER(CONCAT('%',:q,'%')) + AND (LOWER(CONCAT(COALESCE(p.first_name,''),' ',p.last_name)) LIKE LOWER(CONCAT('%',:q,'%')) + OR LOWER(CONCAT(p.last_name,' ',COALESCE(p.first_name,''))) LIKE LOWER(CONCAT('%',:q,'%')) OR LOWER(p.alias) LIKE LOWER(CONCAT('%',:q,'%'))) GROUP BY p.id ORDER BY COUNT(DISTINCT shared.doc_id) DESC diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java index b0873d35..26eccf6a 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/PersonRepositoryTest.java @@ -440,4 +440,26 @@ class PersonRepositoryTest { assertThat(results).hasSize(1); assertThat(results.get(0).getLastName()).isEqualTo("Cram"); } + + // ─── null firstName handling ──────────────────────────────────────────── + + @Test + void searchByName_findsPersonWithNullFirstName() { + personRepository.save(Person.builder().lastName("Gesellschafter des Verlages").build()); + + List result = personRepository.searchByName("Gesellschafter"); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getLastName()).isEqualTo("Gesellschafter des Verlages"); + } + + @Test + void searchWithDocumentCount_findsPersonWithNullFirstName() { + personRepository.save(Person.builder().lastName("Gesellschafter des Verlages").build()); + + List result = personRepository.searchWithDocumentCount("Gesellschafter"); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getLastName()).isEqualTo("Gesellschafter des Verlages"); + } }