fix(namespace): stop the backfill from querying with a null keyword

The backfill preview returned 500 on PostgreSQL:

    SQLState 42883: function lower(bytea) does not exist

It reused UserAccountRepository.search(keyword, status, pageable) with a
null keyword. That query compares the keyword with lower(...), and a null
bind leaves PostgreSQL to infer the parameter type as bytea, so lower()
has no matching signature.

Nothing had exercised that branch before: the admin user list goes through
AdminUserSearchRepository, and the member-candidate lookup always passes a
real keyword. The backfill was the first caller to pass null.

Give callers that want every account in a status a query without a keyword
to bind, rather than papering over the null with a cast or an empty string.

Neither test layer would have caught this. The unit tests mock the
repository, and the integration tests run on H2 in PostgreSQL mode, which
accepts the null-typed bind that PostgreSQL rejects. Verified instead
against a real PostgreSQL: preview, apply, and a second preview showing
alreadyProvisioned with nothing left to do, with namespace_member rows
confirming each owner holds OWNER on a TEAM namespace.
This commit is contained in:
青柠 2026-08-15 18:10:43 +08:00
parent 2d50437e4f
commit 639e081ca7
3 changed files with 13 additions and 2 deletions

View file

@ -141,7 +141,7 @@ public class PersonalNamespaceProvisioningService {
boolean truncated = false;
for (int page = 0; !truncated; page++) {
Page<UserAccount> batch = userAccountRepository.search(null, UserStatus.ACTIVE,
Page<UserAccount> batch = userAccountRepository.findByStatus(UserStatus.ACTIVE,
PageRequest.of(page, BACKFILL_PAGE_SIZE, Sort.by("id")));
if (batch.isEmpty()) {
break;

View file

@ -14,5 +14,16 @@ public interface UserAccountRepository {
List<UserAccount> findByIdIn(List<String> ids);
Optional<UserAccount> findByEmailIgnoreCase(String email);
Page<UserAccount> search(String keyword, UserStatus status, Pageable pageable);
/**
* Lists accounts in one status.
*
* <p>Separate from {@link #search} on purpose: that query compares the keyword with
* {@code lower(...)}, and passing a null keyword leaves PostgreSQL to infer the bind type as
* {@code bytea}, which fails with "function lower(bytea) does not exist". Callers that want
* every account in a status have no keyword to give, so they get a query without one.
*/
Page<UserAccount> findByStatus(UserStatus status, Pageable pageable);
UserAccount save(UserAccount user);
}

View file

@ -63,7 +63,7 @@ class PersonalNamespaceProvisioningServiceTest {
}
private void directoryContains(UserAccount... users) {
when(userAccountRepository.search(isNull(), eq(UserStatus.ACTIVE), any()))
when(userAccountRepository.findByStatus(eq(UserStatus.ACTIVE), any()))
.thenReturn(new PageImpl<>(List.of(users)));
}