fix(server): avoid GROUP BY on full person row in getAllForUser

Aggregate visible face counts per person in a narrow subquery grouped
only by asset_face.personId, then join back to person. Fixes #30562
This commit is contained in:
djadji-gueye 2026-08-05 23:02:20 +00:00
parent 0293414abd
commit 717707fa0c
2 changed files with 36 additions and 26 deletions

View file

@ -28,38 +28,44 @@ select
"person".*
from
"person"
inner join "asset_face" on "asset_face"."personId" = "person"."id"
inner join "asset" on "asset_face"."assetId" = "asset"."id"
and "asset"."visibility" = 'timeline'
and "asset"."deletedAt" is null
inner join (
select
"asset_face"."personId" as "personId",
count("asset_face"."assetId") as "faceCount"
from
"asset_face"
inner join "asset" on "asset_face"."assetId" = "asset"."id"
and "asset"."visibility" = 'timeline'
and "asset"."deletedAt" is null
where
"asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true
group by
"asset_face"."personId"
) as "face_counts" on "face_counts"."personId" = "person"."id"
where
"person"."ownerId" = $1
and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true
and "person"."isHidden" = $2
group by
"person"."id"
having
(
"person"."name" != $3
or count("asset_face"."assetId") >= COALESCE(
and (
"person"."name" != $2
or "face_counts"."faceCount" >= COALESCE(
(
SELECT
value -> 'people' ->> 'minimumFaces'
FROM
user_metadata
WHERE
"userId" = $4
"userId" = $3
AND key = 'preferences'
),
'3'
)::int
)
and "person"."isHidden" = $4
order by
"person"."isHidden" asc,
"person"."isFavorite" desc,
NULLIF(person.name, '') is null asc,
count("asset_face"."assetId") desc,
"face_counts"."faceCount" desc,
NULLIF(person.name, '') asc nulls last,
"person"."createdAt"
limit

View file

@ -149,26 +149,29 @@ export class PersonRepository {
@GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] })
async getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions) {
const items = await this.db
.selectFrom('person')
.selectAll('person')
.innerJoin('asset_face', 'asset_face.personId', 'person.id')
const faceCounts = this.db
.selectFrom('asset_face')
.innerJoin('asset', (join) =>
join
.onRef('asset_face.assetId', '=', 'asset.id')
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.on('asset.deletedAt', 'is', null),
)
.where('person.ownerId', '=', userId)
.select(['asset_face.personId as personId', (eb) => eb.fn.count('asset_face.assetId').as('faceCount')])
.where('asset_face.deletedAt', 'is', null)
.where('asset_face.isVisible', 'is', true)
.orderBy('person.isHidden', 'asc')
.orderBy('person.isFavorite', 'desc')
.having((eb) =>
.groupBy('asset_face.personId');
const items = await this.db
.selectFrom('person')
.selectAll('person')
.innerJoin(faceCounts.as('face_counts'), 'face_counts.personId', 'person.id')
.where('person.ownerId', '=', userId)
.where((eb) =>
eb.or([
eb('person.name', '!=', ''),
eb(
(innerEb) => innerEb.fn.count('asset_face.assetId'),
'face_counts.faceCount',
'>=',
sql<number>`COALESCE(
(SELECT value -> 'people' ->> 'minimumFaces'
@ -180,7 +183,8 @@ export class PersonRepository {
),
]),
)
.groupBy('person.id')
.orderBy('person.isHidden', 'asc')
.orderBy('person.isFavorite', 'desc')
.$if(!!options?.closestFaceAssetId, (qb) =>
qb.orderBy((eb) =>
eb(
@ -201,7 +205,7 @@ export class PersonRepository {
.$if(!options?.closestFaceAssetId, (qb) =>
qb
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
.orderBy((eb) => eb.fn.count('asset_face.assetId'), 'desc')
.orderBy('face_counts.faceCount', 'desc')
.orderBy(sql`NULLIF(person.name, '')`, (om) => om.asc().nullsLast())
.orderBy('person.createdAt'),
)