From bb024c862609c9e2baffc08fef795cfa3418df16 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 14 Oct 2026 00:33:29 +0200 Subject: [PATCH] perf(server): check shared link asset access without scanning the album (#32411) * perf(server): check shared link asset access without scanning the album checkSharedLinkAccess runs on every thumbnail, preview, playback and asset info request made through a shared link. It joined every asset of the link's album and only then filtered on an array overlap across four outer-joined columns, so each request cost O(album size). Split it into two branches (link assets, album assets) that find the requested ids through the asset pkey / livePhotoVideoId index and then probe the album_asset pkey. Median per request, PG 14, 1.36M-asset instance: | album behind the link | before | after | |-----------------------|--------|---------| | 290 assets | 2.1 ms | 0.17 ms | | 150k assets | 296 ms | 0.19 ms | * chore: clean up --------- Co-authored-by: Jason Rasmussen --- server/src/queries/access.repository.sql | 64 ++++++++++++------ server/src/repositories/access.repository.ts | 68 +++++++++++--------- 2 files changed, 83 insertions(+), 49 deletions(-) diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index f66abe3b91..681860117a 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -125,29 +125,53 @@ where and "asset"."id" in ($2) -- AccessRepository.asset.checkSharedLinkAccess +with + "target" as ( + select + array[$1]::uuid[] as "ids" + ), + "link" as ( + select + "shared_link"."id", + "shared_link"."albumId" + from + "shared_link" + where + "shared_link"."id" = $2 + ) select - "asset"."id" as "assetId", - "asset"."livePhotoVideoId" as "assetLivePhotoVideoId", - "albumAssets"."id" as "albumAssetId", - "albumAssets"."livePhotoVideoId" as "albumAssetLivePhotoVideoId" + "asset"."id", + "asset"."livePhotoVideoId" from - "shared_link" - left join "album" on "album"."id" = "shared_link"."albumId" - and "album"."deletedAt" is null - left join "shared_link_asset" on "shared_link_asset"."sharedLinkId" = "shared_link"."id" - left join "asset" on "asset"."id" = "shared_link_asset"."assetId" - and "asset"."deletedAt" is null - left join "album_asset" on "album_asset"."albumId" = "album"."id" - left join "asset" as "albumAssets" on "albumAssets"."id" = "album_asset"."assetId" - and "albumAssets"."deletedAt" is null + "asset" + cross join "target" + inner join lateral ( + select + 1 as "match" + from + "link" + inner join "shared_link_asset" on "shared_link_asset"."sharedLinkId" = "link"."id" + where + "shared_link_asset"."assetId" = "asset"."id" + union all + select + 1 as "match" + from + "link" + inner join "album" on "album"."id" = "link"."albumId" + and "album"."deletedAt" is null + inner join "album_asset" on "album_asset"."albumId" = "album"."id" + where + "album_asset"."assetId" = "asset"."id" + limit + $3 + ) as "membership" on true where - "shared_link"."id" = $1 - and array[ - "asset"."id", - "asset"."livePhotoVideoId", - "albumAssets"."id", - "albumAssets"."livePhotoVideoId" - ] && array[$2]::uuid[] + "asset"."deletedAt" is null + and ( + "asset"."id" = any ("target"."ids") + or "asset"."livePhotoVideoId" = any ("target"."ids") + ) -- AccessRepository.assetFile.checkOwnerAccess select diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 6e9a3c2b3e..99520ec9a4 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -236,43 +236,53 @@ class AssetAccess { } return this.db - .selectFrom('shared_link') - .leftJoin('album', (join) => join.onRef('album.id', '=', 'shared_link.albumId').on('album.deletedAt', 'is', null)) - .leftJoin('shared_link_asset', 'shared_link_asset.sharedLinkId', 'shared_link.id') - .leftJoin('asset', (join) => - join.onRef('asset.id', '=', 'shared_link_asset.assetId').on('asset.deletedAt', 'is', null), + .with('target', (qb) => qb.selectNoFrom(sql`array[${sql.join([...assetIds])}]::uuid[]`.as('ids'))) + .with('link', (qb) => + qb + .selectFrom('shared_link') + .select(['shared_link.id', 'shared_link.albumId']) + .where('shared_link.id', '=', sharedLinkId), ) - .leftJoin('album_asset', 'album_asset.albumId', 'album.id') - .leftJoin('asset as albumAssets', (join) => - join.onRef('albumAssets.id', '=', 'album_asset.assetId').on('albumAssets.deletedAt', 'is', null), + .selectFrom('asset') + .crossJoin('target') + .innerJoinLateral( + (eb) => + eb + .selectFrom('link') + .innerJoin('shared_link_asset', 'shared_link_asset.sharedLinkId', 'link.id') + .select(sql.lit(1).as('match')) + .whereRef('shared_link_asset.assetId', '=', 'asset.id') + .unionAll( + eb + .selectFrom('link') + .innerJoin('album', (join) => + join.onRef('album.id', '=', 'link.albumId').on('album.deletedAt', 'is', null), + ) + .innerJoin('album_asset', 'album_asset.albumId', 'album.id') + .select(sql.lit(1).as('match')) + .whereRef('album_asset.assetId', '=', 'asset.id'), + ) + .limit(1) + .as('membership'), + (join) => join.onTrue(), ) - .select([ - 'asset.id as assetId', - 'asset.livePhotoVideoId as assetLivePhotoVideoId', - 'albumAssets.id as albumAssetId', - 'albumAssets.livePhotoVideoId as albumAssetLivePhotoVideoId', - ]) - .where('shared_link.id', '=', sharedLinkId) - .where( - sql`array["asset"."id", "asset"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"]`, - '&&', - sql`array[${sql.join([...assetIds])}]::uuid[] `, + .select(['asset.id', 'asset.livePhotoVideoId']) + .where('asset.deletedAt', 'is', null) + .where((eb) => + eb.or([ + eb('asset.id', '=', eb.fn.any('target.ids')), + eb('asset.livePhotoVideoId', '=', eb.fn.any('target.ids')), + ]), ) .execute() .then((rows) => { const allowedIds = new Set(); for (const row of rows) { - if (row.assetId && assetIds.has(row.assetId)) { - allowedIds.add(row.assetId); + if (assetIds.has(row.id)) { + allowedIds.add(row.id); } - if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) { - allowedIds.add(row.assetLivePhotoVideoId); - } - if (row.albumAssetId && assetIds.has(row.albumAssetId)) { - allowedIds.add(row.albumAssetId); - } - if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) { - allowedIds.add(row.albumAssetLivePhotoVideoId); + if (row.livePhotoVideoId && assetIds.has(row.livePhotoVideoId)) { + allowedIds.add(row.livePhotoVideoId); } } return allowedIds;