1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-26 10:50:29 +02:00

add count option to withStack

This commit is contained in:
mertalev 2024-12-20 18:28:47 -05:00
parent e3d08f0856
commit 45cd21a7b3
No known key found for this signature in database
GPG Key ID: 3A2B5BFC678DBC80
3 changed files with 115 additions and 82 deletions

View File

@ -179,39 +179,42 @@ export class AssetEntity {
duplicateId!: string | null; duplicateId!: string | null;
} }
export const withExif = <O>(qb: SelectQueryBuilder<DB, 'assets', O>) => { export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
return qb return qb
.leftJoin('exif', 'assets.id', 'exif.assetId') .leftJoin('exif', 'assets.id', 'exif.assetId')
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo')); .select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo'));
}; }
export const withExifInner = <O>(qb: SelectQueryBuilder<DB, 'assets', O>) => { export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
return qb return qb
.innerJoin('exif', 'assets.id', 'exif.assetId') .innerJoin('exif', 'assets.id', 'exif.assetId')
.select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo')); .select((eb) => eb.fn('to_jsonb', [eb.table('exif')]).as('exifInfo'));
}; }
export const withSmartSearch = <O>(qb: SelectQueryBuilder<DB, 'assets', O>, options?: { inner: boolean }) => { export function withSmartSearch<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
const join = options?.inner return qb
? qb.innerJoin('smart_search', 'assets.id', 'smart_search.assetId') .leftJoin('smart_search', 'assets.id', 'smart_search.assetId')
: qb.leftJoin('smart_search', 'assets.id', 'smart_search.assetId'); .select(sql<number[]>`smart_search.embedding`.as('embedding'));
return join.select(sql<number[]>`smart_search.embedding`.as('embedding')); }
};
export const withFaces = (eb: ExpressionBuilder<DB, 'assets'>) => export function withFaces(eb: ExpressionBuilder<DB, 'assets'>) {
jsonArrayFrom(eb.selectFrom('asset_faces').selectAll().whereRef('asset_faces.assetId', '=', 'assets.id')).as('faces'); return jsonArrayFrom(eb.selectFrom('asset_faces').selectAll().whereRef('asset_faces.assetId', '=', 'assets.id')).as(
'faces',
);
}
export const withFiles = (eb: ExpressionBuilder<DB, 'assets'>, type?: AssetFileType) => export function withFiles(eb: ExpressionBuilder<DB, 'assets'>, type?: AssetFileType) {
jsonArrayFrom( return jsonArrayFrom(
eb eb
.selectFrom('asset_files') .selectFrom('asset_files')
.selectAll() .selectAll()
.whereRef('asset_files.assetId', '=', 'assets.id') .whereRef('asset_files.assetId', '=', 'assets.id')
.$if(!!type, (qb) => qb.where('type', '=', type!)), .$if(!!type, (qb) => qb.where('type', '=', type!)),
).as('files'); ).as('files');
}
export const withFacesAndPeople = (eb: ExpressionBuilder<DB, 'assets'>) => export function withFacesAndPeople(eb: ExpressionBuilder<DB, 'assets'>) {
eb return eb
.selectFrom('asset_faces') .selectFrom('asset_faces')
.leftJoin('person', 'person.id', 'asset_faces.personId') .leftJoin('person', 'person.id', 'asset_faces.personId')
.whereRef('asset_faces.assetId', '=', 'assets.id') .whereRef('asset_faces.assetId', '=', 'assets.id')
@ -234,10 +237,11 @@ export const withFacesAndPeople = (eb: ExpressionBuilder<DB, 'assets'>) =>
.as('faces'), .as('faces'),
) )
.as('faces'); .as('faces');
}
/** Adds a `has_people` CTE that can be inner joined on to filter out assets */ /** Adds a `has_people` CTE that can be inner joined on to filter out assets */
export const hasPeopleCte = (db: Kysely<DB>, personIds: string[]) => export function hasPeopleCte(db: Kysely<DB>, personIds: string[]) {
db.with('has_people', (qb) => return db.with('has_people', (qb) =>
qb qb
.selectFrom('asset_faces') .selectFrom('asset_faces')
.select('assetId') .select('assetId')
@ -245,66 +249,67 @@ export const hasPeopleCte = (db: Kysely<DB>, personIds: string[]) =>
.groupBy('assetId') .groupBy('assetId')
.having((eb) => eb.fn.count('personId'), '>=', personIds.length), .having((eb) => eb.fn.count('personId'), '>=', personIds.length),
); );
}
export const hasPeople = (db: Kysely<DB>, personIds?: string[]) => export function hasPeople(db: Kysely<DB>, personIds?: string[]) {
personIds && personIds.length > 0 return personIds && personIds.length > 0
? hasPeopleCte(db, personIds).selectFrom('assets').innerJoin('has_people', 'has_people.assetId', 'assets.id') ? hasPeopleCte(db, personIds).selectFrom('assets').innerJoin('has_people', 'has_people.assetId', 'assets.id')
: db.selectFrom('assets'); : db.selectFrom('assets');
}
export const withOwner = (eb: ExpressionBuilder<DB, 'assets'>) => export function withOwner(eb: ExpressionBuilder<DB, 'assets'>) {
jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner'); return jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'assets.ownerId')).as('owner');
}
export const withLibrary = (eb: ExpressionBuilder<DB, 'assets'>) => export function withLibrary(eb: ExpressionBuilder<DB, 'assets'>) {
jsonObjectFrom(eb.selectFrom('libraries').selectAll().whereRef('libraries.id', '=', 'assets.libraryId')).as( return jsonObjectFrom(eb.selectFrom('libraries').selectAll().whereRef('libraries.id', '=', 'assets.libraryId')).as(
'library', 'library',
); );
}
type Stacked = SelectQueryBuilder< export function withStackedAssets<O>(qb: SelectQueryBuilder<DB, 'assets' | 'asset_stack', O>) {
DB & { stacked: Selectable<Assets> }, return qb
'assets' | 'asset_stack' | 'stacked', .innerJoinLateral(
{ assets: Selectable<Assets>[] } (eb: ExpressionBuilder<DB, 'assets' | 'asset_stack'>) =>
>; eb
.selectFrom('assets as stacked')
.select((eb) => eb.fn<Selectable<Assets>[]>('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 function withStack<O>(
export const withStack = <O>(
qb: SelectQueryBuilder<DB, 'assets', O>, qb: SelectQueryBuilder<DB, 'assets', O>,
{ assets }: { assets?: boolean | StackExpression }, { assets, count }: { assets: boolean; count: boolean },
) => ) {
qb return qb
.leftJoinLateral( .leftJoinLateral(
(eb) => (eb) =>
eb eb
.selectFrom('asset_stack') .selectFrom('asset_stack')
.selectAll('asset_stack') .selectAll('asset_stack')
.whereRef('assets.stackId', '=', 'asset_stack.id') .whereRef('assets.stackId', '=', 'asset_stack.id')
.$if(!!assets, (qb) => .$if(assets, withStackedAssets)
qb .$if(count, (qb) =>
.innerJoinLateral( // There is no `selectNoFrom` method for expression builders
(eb: ExpressionBuilder<DB, 'assets' | 'asset_stack'>) => qb.select(
eb sql`(select count(*) as "assetCount" where "asset_stack"."id" = "assets"."stackId")`.as('assetCount'),
.selectFrom('assets as stacked') ),
.select((eb) => eb.fn<Selectable<Assets>[]>('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'),
) )
.as('stacked_assets'), .as('stacked_assets'),
(join) => join.onTrue(), (join) => join.onTrue(),
) )
.select((eb) => eb.fn('to_jsonb', [eb.table('stacked_assets')]).as('stack')); .select((eb) => eb.fn('to_jsonb', [eb.table('stacked_assets')]).as('stack'));
}
export const withAlbums = <O>(qb: SelectQueryBuilder<DB, 'assets', O>, { albumId }: { albumId?: string }) => { export function withAlbums<O>(qb: SelectQueryBuilder<DB, 'assets', O>, { albumId }: { albumId?: string }) {
return qb return qb
.select((eb) => .select((eb) =>
jsonArrayFrom( jsonArrayFrom(
@ -330,16 +335,17 @@ export const withAlbums = <O>(qb: SelectQueryBuilder<DB, 'assets', O>, { albumId
), ),
), ),
); );
}; }
export const withTags = (eb: ExpressionBuilder<DB, 'assets'>) => export function withTags(eb: ExpressionBuilder<DB, 'assets'>) {
jsonArrayFrom( return jsonArrayFrom(
eb eb
.selectFrom('tags') .selectFrom('tags')
.selectAll('tags') .selectAll('tags')
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId') .innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
.whereRef('assets.id', '=', 'tag_asset.assetsId'), .whereRef('assets.id', '=', 'tag_asset.assetsId'),
).as('tags'); ).as('tags');
}
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();

View File

@ -248,7 +248,7 @@ where
and "assets"."isVisible" = $2 and "assets"."isVisible" = $2
and "deletedAt" is null and "deletedAt" is null
order by order by
"createdAt" asc "createdAt"
limit limit
$3 $3
offset offset
@ -367,33 +367,60 @@ limit
-- AssetRepository.getAllForUserFullSync -- AssetRepository.getAllForUserFullSync
select select
"assets".*,
to_jsonb("exif") as "exifInfo",
to_jsonb("stacked_assets") as "stack"
from from
"assets" "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 where
"ownerId" = $1::uuid "assets"."ownerId" = $1::uuid
and "isVisible" = $2 and "isVisible" = $2
and "updatedAt" <= $3 and "updatedAt" <= $3
and "id" > $4 and "assets"."id" > $4
order by order by
"id" asc "assets"."id"
limit limit
$5 $5
-- AssetRepository.getChangedDeltaSync -- AssetRepository.getChangedDeltaSync
select select
"assets".*, "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 select
count(*) as "stackedAssetsCount" "asset_stack".*,
(
select
count(*) as "assetCount"
where
"asset_stack"."id" = "assets"."stackId"
) as "assetCount"
from from
"asset_stack" "asset_stack"
where where
"asset_stack"."id" = "assets"."stackId" "assets"."stackId" = "asset_stack"."id"
) as "stackedAssetsCount" ) as "stacked_assets" on true
from
"assets"
where where
"ownerId" = any ($1::uuid []) "assets"."ownerId" = any ($1::uuid [])
and "isVisible" = $2 and "isVisible" = $2
and "updatedAt" > $3 and "updatedAt" > $3
limit limit

View File

@ -164,8 +164,8 @@ export class AssetRepository implements IAssetRepository {
.$if(!!files, (qb) => qb.select(withFiles)) .$if(!!files, (qb) => qb.select(withFiles))
.$if(!!library, (qb) => qb.select(withLibrary)) .$if(!!library, (qb) => qb.select(withLibrary))
.$if(!!owner, (qb) => qb.select(withOwner)) .$if(!!owner, (qb) => qb.select(withOwner))
.$if(!!smartSearch, (qb) => withSmartSearch(qb)) .$if(!!smartSearch, withSmartSearch)
.$if(!!stack, (qb) => withStack(qb, { assets: stack!.assets })) .$if(!!stack, (qb) => withStack(qb, { assets: !!stack!.assets, count: false }))
.$if(!!tags, (qb) => qb.select(withTags)) .$if(!!tags, (qb) => qb.select(withTags))
.execute(); .execute();
@ -181,7 +181,7 @@ export class AssetRepository implements IAssetRepository {
.select(withFacesAndPeople) .select(withFacesAndPeople)
.select(withTags) .select(withTags)
.$call(withExif) .$call(withExif)
.$call((qb) => withStack(qb, { assets: true })) .$call((qb) => withStack(qb, { assets: true, count: false }))
.where('assets.id', '=', anyUuid(ids)) .where('assets.id', '=', anyUuid(ids))
.execute() as any as Promise<AssetEntity[]>; .execute() as any as Promise<AssetEntity[]>;
} }
@ -290,7 +290,7 @@ export class AssetRepository implements IAssetRepository {
.$if(!!library, (qb) => qb.select(withLibrary)) .$if(!!library, (qb) => qb.select(withLibrary))
.$if(!!owner, (qb) => qb.select(withOwner)) .$if(!!owner, (qb) => qb.select(withOwner))
.$if(!!smartSearch, withSmartSearch) .$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(!!files, (qb) => qb.select(withFiles))
.$if(!!tags, (qb) => qb.select(withTags)) .$if(!!tags, (qb) => qb.select(withTags))
.limit(1) .limit(1)
@ -455,7 +455,7 @@ export class AssetRepository implements IAssetRepository {
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.limit(pagination.take + 1) .limit(pagination.take + 1)
.offset(pagination.skip ?? 0) .offset(pagination.skip ?? 0)
.orderBy('createdAt', 'asc') .orderBy('createdAt')
.execute(); .execute();
return paginationHelper(items as any as AssetEntity[], pagination.take); 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.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!)) .$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) .$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.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
.$if(options.isDuplicate !== undefined, (qb) => .$if(options.isDuplicate !== undefined, (qb) =>
qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null), qb.where('assets.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
@ -687,12 +687,12 @@ export class AssetRepository implements IAssetRepository {
.selectFrom('assets') .selectFrom('assets')
.selectAll('assets') .selectAll('assets')
.$call(withExif) .$call(withExif)
.$call((qb) => withStack(qb, { assets: false })) .$call((qb) => withStack(qb, { assets: false, count: true }))
.where('ownerId', '=', asUuid(ownerId)) .where('assets.ownerId', '=', asUuid(ownerId))
.where('isVisible', '=', true) .where('isVisible', '=', true)
.where('updatedAt', '<=', updatedUntil) .where('updatedAt', '<=', updatedUntil)
.$if(!!lastId, (qb) => qb.where('id', '>', lastId!)) .$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
.orderBy('id', 'asc') .orderBy('assets.id')
.limit(limit) .limit(limit)
.execute() as any as Promise<AssetEntity[]>; .execute() as any as Promise<AssetEntity[]>;
} }
@ -703,8 +703,8 @@ export class AssetRepository implements IAssetRepository {
.selectFrom('assets') .selectFrom('assets')
.selectAll('assets') .selectAll('assets')
.$call(withExif) .$call(withExif)
.$call((qb) => withStack(qb, { assets: false })) .$call((qb) => withStack(qb, { assets: false, count: true }))
.where('ownerId', '=', anyUuid(options.userIds)) .where('assets.ownerId', '=', anyUuid(options.userIds))
.where('isVisible', '=', true) .where('isVisible', '=', true)
.where('updatedAt', '>', options.updatedAfter) .where('updatedAt', '>', options.updatedAfter)
.limit(options.limit) .limit(options.limit)