Fix family-member POST: null JPQL param caused bigint <> varchar SQL error

The relationship-uniqueness query used (:excludeId IS NULL OR fm.id <> :excludeId).
On create, excludeId is null and PostgreSQL parses the parameter as varchar before
the IS NULL branch can short-circuit, so adding a family member returned 403 with
"operator does not exist: bigint <> character varying". Split into two queries so
neither has a nullable Long parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-19 09:40:14 +05:30
co-authored by Claude Opus 4.7
parent 45a041b023
commit 026e5fb875
2 changed files with 19 additions and 7 deletions
@@ -28,16 +28,27 @@ public interface ClientFamilyMembersRepository
extends JpaRepository<ClientFamilyMembers, Long>, JpaSpecificationExecutor<ClientFamilyMembers> { extends JpaRepository<ClientFamilyMembers, Long>, JpaSpecificationExecutor<ClientFamilyMembers> {
/** /**
* Counts non-deleted family members for a client that hold a given relationship code, optionally * Counts non-deleted family members for a client that hold a given relationship code.
* ignoring one specific family member (used at update time so a row doesn't conflict with itself). * Backs the single-use relationship rule for Mother / Father / Spouse on create.
* Backs the single-use relationship rule for Mother / Father / Spouse.
*/ */
@Query("SELECT COUNT(fm) FROM ClientFamilyMembers fm " @Query("SELECT COUNT(fm) FROM ClientFamilyMembers fm "
+ "WHERE fm.client.id = :clientId " + "WHERE fm.client.id = :clientId "
+ "AND fm.relationship.id = :relationshipId " + "AND fm.relationship.id = :relationshipId "
+ "AND fm.isDeleted = false " + "AND fm.isDeleted = false")
+ "AND (:excludeFamilyMemberId IS NULL OR fm.id <> :excludeFamilyMemberId)")
long countActiveByClientAndRelationship(@Param("clientId") Long clientId, long countActiveByClientAndRelationship(@Param("clientId") Long clientId,
@Param("relationshipId") Long relationshipId);
// Update variant: excludes one specific row so it doesn't conflict with itself.
// Why: PostgreSQL parses parameter types at parse-time; a nullable Long parameter compared with
// <> against a bigint column gets bound as varchar when null, producing
// "operator does not exist: bigint <> character varying". Splitting create vs. update keeps
// both queries non-nullable.
@Query("SELECT COUNT(fm) FROM ClientFamilyMembers fm "
+ "WHERE fm.client.id = :clientId "
+ "AND fm.relationship.id = :relationshipId "
+ "AND fm.isDeleted = false "
+ "AND fm.id <> :excludeFamilyMemberId")
long countActiveByClientAndRelationshipExcluding(@Param("clientId") Long clientId,
@Param("relationshipId") Long relationshipId, @Param("relationshipId") Long relationshipId,
@Param("excludeFamilyMemberId") Long excludeFamilyMemberId); @Param("excludeFamilyMemberId") Long excludeFamilyMemberId);
@@ -99,8 +99,9 @@ public class ClientFamilyMembersWritePlatformServiceImpl implements ClientFamily
if (!SINGLE_USE_RELATIONSHIPS.contains(label)) { if (!SINGLE_USE_RELATIONSHIPS.contains(label)) {
return; return;
} }
final long existing = clientFamilyRepository.countActiveByClientAndRelationship( final long existing = (excludeMemberId == null)
clientId, relationship.getId(), excludeMemberId); ? clientFamilyRepository.countActiveByClientAndRelationship(clientId, relationship.getId())
: clientFamilyRepository.countActiveByClientAndRelationshipExcluding(clientId, relationship.getId(), excludeMemberId);
if (existing > 0) { if (existing > 0) {
final String displayName = relationship.getLabel(); final String displayName = relationship.getLabel();
throw new PlatformApiDataValidationException(java.util.List.of(ApiParameterError.parameterError( throw new PlatformApiDataValidationException(java.util.List.of(ApiParameterError.parameterError(