diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index 6890b8f5a9..7da3b9457c 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -179,39 +179,42 @@ export class AssetEntity { duplicateId!: string | null; } -export const withExif = (qb: SelectQueryBuilder) => { +export function withExif(qb: SelectQueryBuilder) { return qb .leftJoin('exif', 'assets.id', 'exif.assetId') .select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo')); -}; +} -export const withExifInner = (qb: SelectQueryBuilder) => { +export function withExifInner(qb: SelectQueryBuilder) { return qb .innerJoin('exif', 'assets.id', 'exif.assetId') .select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo')); -}; +} -export const withSmartSearch = (qb: SelectQueryBuilder, options?: { inner: boolean }) => { - const join = options?.inner - ? qb.innerJoin('smart_search', 'assets.id', 'smart_search.assetId') - : qb.leftJoin('smart_search', 'assets.id', 'smart_search.assetId'); - return join.select(sql`smart_search.embedding`.as('embedding')); -}; +export function withSmartSearch(qb: SelectQueryBuilder) { + return qb + .leftJoin('smart_search', 'assets.id', 'smart_search.assetId') + .select(sql`smart_search.embedding`.as('embedding')); +} -export const withFaces = (eb: ExpressionBuilder) => - jsonArrayFrom(eb.selectFrom('asset_faces').selectAll().whereRef('asset_faces.assetId', '=', 'assets.id')).as('faces'); +export function withFaces(eb: ExpressionBuilder) { + return jsonArrayFrom(eb.selectFrom('asset_faces').selectAll().whereRef('asset_faces.assetId', '=', 'assets.id')).as( + 'faces', + ); +} -export const withFiles = (eb: ExpressionBuilder, type?: AssetFileType) => - jsonArrayFrom( +export function withFiles(eb: ExpressionBuilder, type?: AssetFileType) { + return jsonArrayFrom( eb .selectFrom('asset_files') .selectAll() .whereRef('asset_files.assetId', '=', 'assets.id') .$if(!!type, (qb) => qb.where('type', '=', type!)), ).as('files'); +} -export const withFacesAndPeople = (eb: ExpressionBuilder) => - eb +export function withFacesAndPeople(eb: ExpressionBuilder) { + return eb .selectFrom('asset_faces') .leftJoin('person', 'person.id', 'asset_faces.personId') .whereRef('asset_faces.assetId', '=', 'assets.id') @@ -234,10 +237,11 @@ export const withFacesAndPeople = (eb: ExpressionBuilder) => .as('faces'), ) .as('faces'); +} /** Adds a `has_people` CTE that can be inner joined on to filter out assets */ -export const hasPeopleCte = (db: Kysely, personIds: string[]) => - db.with('has_people', (qb) => +export function hasPeopleCte(db: Kysely, personIds: string[]) { + return db.with('has_people', (qb) => qb .selectFrom('asset_faces') .select('assetId') @@ -245,66 +249,67 @@ export const hasPeopleCte = (db: Kysely, personIds: string[]) => .groupBy('assetId') .having((eb) => eb.fn.count('personId'), '>=', personIds.length), ); +} -export const hasPeople = (db: Kysely, personIds?: string[]) => - personIds && personIds.length > 0 +export function hasPeople(db: Kysely, personIds?: string[]) { + return personIds && personIds.length > 0 ? hasPeopleCte(db, personIds).selectFrom('assets').innerJoin('has_people', 'has_people.assetId', 'assets.id') : db.selectFrom('assets'); +} -export const withOwner = (eb: ExpressionBuilder) => - jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner'); +export function withOwner(eb: ExpressionBuilder) { + return jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner'); +} -export const withLibrary = (eb: ExpressionBuilder) => - jsonObjectFrom(eb.selectFrom('libraries').selectAll().whereRef('libraries.id', '=', 'assets.libraryId')).as( +export function withLibrary(eb: ExpressionBuilder) { + return jsonObjectFrom(eb.selectFrom('libraries').selectAll().whereRef('libraries.id', '=', 'assets.libraryId')).as( 'library', ); +} -type Stacked = SelectQueryBuilder< - DB & { stacked: Selectable }, - 'assets' | 'asset_stack' | 'stacked', - { assets: Selectable[] } ->; +export function withStackedAssets(qb: SelectQueryBuilder) { + return qb + .innerJoinLateral( + (eb: ExpressionBuilder) => + eb + .selectFrom('assets as stacked') + .select((eb) => eb.fn[]>('array_agg', [eb.table('stacked')]).as('assets')) + .whereRef('asset_stack.id', '=', 'stacked.stackId') + .whereRef('asset_stack.primaryAssetId', '!=', 'stacked.id') + .as('s'), + (join) => + join.on((eb) => + eb.or([eb('asset_stack.primaryAssetId', '=', eb.ref('assets.id')), eb('assets.stackId', 'is', null)]), + ), + ) + .select('s.assets'); +} -type StackExpression = (eb: Stacked) => Stacked; - -export const withStack = ( +export function withStack( qb: SelectQueryBuilder, - { assets }: { assets?: boolean | StackExpression }, -) => - qb + { assets, count }: { assets: boolean; count: boolean }, +) { + return qb .leftJoinLateral( (eb) => eb .selectFrom('asset_stack') .selectAll('asset_stack') .whereRef('assets.stackId', '=', 'asset_stack.id') - .$if(!!assets, (qb) => - qb - .innerJoinLateral( - (eb: ExpressionBuilder) => - eb - .selectFrom('assets as stacked') - .select((eb) => eb.fn[]>('array_agg', [eb.table('stacked')]).as('assets')) - .whereRef('asset_stack.id', '=', 'stacked.stackId') - .whereRef('asset_stack.primaryAssetId', '!=', 'stacked.id') - .$if(typeof assets === 'function', assets as StackExpression) - .as('s'), - (join) => - join.on((eb) => - eb.or([ - eb('asset_stack.primaryAssetId', '=', eb.ref('assets.id')), - eb('assets.stackId', 'is', null), - ]), - ), - ) - .select('s.assets'), + .$if(assets, withStackedAssets) + .$if(count, (qb) => + // There is no `selectNoFrom` method for expression builders + qb.select( + sql`(select count(*) as "assetCount" where "asset_stack"."id" = "assets"."stackId")`.as('assetCount'), + ), ) .as('stacked_assets'), (join) => join.onTrue(), ) .select((eb) => eb.fn('to_jsonb', [eb.table('stacked_assets')]).as('stack')); +} -export const withAlbums = (qb: SelectQueryBuilder, { albumId }: { albumId?: string }) => { +export function withAlbums(qb: SelectQueryBuilder, { albumId }: { albumId?: string }) { return qb .select((eb) => jsonArrayFrom( @@ -330,16 +335,17 @@ export const withAlbums = (qb: SelectQueryBuilder, { albumId ), ), ); -}; +} -export const withTags = (eb: ExpressionBuilder) => - jsonArrayFrom( +export function withTags(eb: ExpressionBuilder) { + return jsonArrayFrom( eb .selectFrom('tags') .selectAll('tags') .innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId') .whereRef('assets.id', '=', 'tag_asset.assetsId'), ).as('tags'); +} const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 4ff6f39297..c33edfaada 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -248,7 +248,7 @@ where and "assets"."isVisible" = $2 and "deletedAt" is null order by - "createdAt" asc + "createdAt" limit $3 offset @@ -367,33 +367,60 @@ limit -- AssetRepository.getAllForUserFullSync select + "assets".*, + to_jsonb("exif") as "exifInfo", + to_jsonb("stacked_assets") as "stack" from "assets" + left join "exif" on "assets"."id" = "exif"."assetId" + left join lateral ( + select + "asset_stack".*, + ( + select + count(*) as "assetCount" + where + "asset_stack"."id" = "assets"."stackId" + ) as "assetCount" + from + "asset_stack" + where + "assets"."stackId" = "asset_stack"."id" + ) as "stacked_assets" on true where - "ownerId" = $1::uuid + "assets"."ownerId" = $1::uuid and "isVisible" = $2 and "updatedAt" <= $3 - and "id" > $4 + and "assets"."id" > $4 order by - "id" asc + "assets"."id" limit $5 -- AssetRepository.getChangedDeltaSync select "assets".*, - ( + to_jsonb("exif") as "exifInfo", + to_jsonb("stacked_assets") as "stack" +from + "assets" + left join "exif" on "assets"."id" = "exif"."assetId" + left join lateral ( select - count(*) as "stackedAssetsCount" + "asset_stack".*, + ( + select + count(*) as "assetCount" + where + "asset_stack"."id" = "assets"."stackId" + ) as "assetCount" from "asset_stack" where - "asset_stack"."id" = "assets"."stackId" - ) as "stackedAssetsCount" -from - "assets" + "assets"."stackId" = "asset_stack"."id" + ) as "stacked_assets" on true where - "ownerId" = any ($1::uuid []) + "assets"."ownerId" = any ($1::uuid []) and "isVisible" = $2 and "updatedAt" > $3 limit diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index f5f9d47179..9ad8bf0a6c 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -164,8 +164,8 @@ export class AssetRepository implements IAssetRepository { .$if(!!files, (qb) => qb.select(withFiles)) .$if(!!library, (qb) => qb.select(withLibrary)) .$if(!!owner, (qb) => qb.select(withOwner)) - .$if(!!smartSearch, (qb) => withSmartSearch(qb)) - .$if(!!stack, (qb) => withStack(qb, { assets: stack!.assets })) + .$if(!!smartSearch, withSmartSearch) + .$if(!!stack, (qb) => withStack(qb, { assets: !!stack!.assets, count: false })) .$if(!!tags, (qb) => qb.select(withTags)) .execute(); @@ -181,7 +181,7 @@ export class AssetRepository implements IAssetRepository { .select(withFacesAndPeople) .select(withTags) .$call(withExif) - .$call((qb) => withStack(qb, { assets: true })) + .$call((qb) => withStack(qb, { assets: true, count: false })) .where('assets.id', '=', anyUuid(ids)) .execute() as any as Promise; } @@ -290,7 +290,7 @@ export class AssetRepository implements IAssetRepository { .$if(!!library, (qb) => qb.select(withLibrary)) .$if(!!owner, (qb) => qb.select(withOwner)) .$if(!!smartSearch, withSmartSearch) - .$if(!!stack, (qb) => withStack(qb, { assets: stack!.assets })) + .$if(!!stack, (qb) => withStack(qb, { assets: !!stack!.assets, count: false })) .$if(!!files, (qb) => qb.select(withFiles)) .$if(!!tags, (qb) => qb.select(withTags)) .limit(1) @@ -455,7 +455,7 @@ export class AssetRepository implements IAssetRepository { .where('deletedAt', 'is', null) .limit(pagination.take + 1) .offset(pagination.skip ?? 0) - .orderBy('createdAt', 'asc') + .orderBy('createdAt') .execute(); return paginationHelper(items as any as AssetEntity[], pagination.take); @@ -586,7 +586,7 @@ export class AssetRepository implements IAssetRepository { .$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!))) .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) - .$if(!!options.withStacked, (qb) => withStack(qb, { assets: true })) // TODO: optimize this; it's a huge performance hit + .$if(!!options.withStacked, (qb) => withStack(qb, { assets: true, count: false })) // TODO: optimize this; it's a huge performance hit .$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!)) .$if(options.isDuplicate !== undefined, (qb) => qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null), @@ -687,12 +687,12 @@ export class AssetRepository implements IAssetRepository { .selectFrom('assets') .selectAll('assets') .$call(withExif) - .$call((qb) => withStack(qb, { assets: false })) - .where('ownerId', '=', asUuid(ownerId)) + .$call((qb) => withStack(qb, { assets: false, count: true })) + .where('assets.ownerId', '=', asUuid(ownerId)) .where('isVisible', '=', true) .where('updatedAt', '<=', updatedUntil) - .$if(!!lastId, (qb) => qb.where('id', '>', lastId!)) - .orderBy('id', 'asc') + .$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!)) + .orderBy('assets.id') .limit(limit) .execute() as any as Promise; } @@ -703,8 +703,8 @@ export class AssetRepository implements IAssetRepository { .selectFrom('assets') .selectAll('assets') .$call(withExif) - .$call((qb) => withStack(qb, { assets: false })) - .where('ownerId', '=', anyUuid(options.userIds)) + .$call((qb) => withStack(qb, { assets: false, count: true })) + .where('assets.ownerId', '=', anyUuid(options.userIds)) .where('isVisible', '=', true) .where('updatedAt', '>', options.updatedAfter) .limit(options.limit)