diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index a2f3f64442..2a0cb16403 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -28,21 +28,15 @@ 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 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( + or "person"."assetCount" >= COALESCE( ( SELECT value -> 'people' ->> 'minimumFaces' @@ -59,7 +53,7 @@ order by "person"."isHidden" asc, "person"."isFavorite" desc, NULLIF(person.name, '') is null asc, - count("asset_face"."assetId") desc, + "person"."assetCount" desc, NULLIF(person.name, '') asc nulls last, "person"."createdAt" limit @@ -262,24 +256,7 @@ select from "person" where - exists ( - select - from - "asset_face" - where - "asset_face"."personId" = "person"."id" - and "asset_face"."deletedAt" is null - and "asset_face"."isVisible" = $2 - and exists ( - select - from - "asset" - where - "asset"."id" = "asset_face"."assetId" - and "asset"."visibility" = 'timeline' - and "asset"."deletedAt" is null - ) - ) + "person"."assetCount" > $2 and "person"."ownerId" = $3 -- PersonRepository.refreshFaces diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 0db03a18c7..ca3ae0532f 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -152,23 +152,14 @@ export class PersonRepository { const items = await this.db .selectFrom('person') .selectAll('person') - .innerJoin('asset_face', 'asset_face.personId', 'person.id') - .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) - .where('asset_face.deletedAt', 'is', null) - .where('asset_face.isVisible', 'is', true) .orderBy('person.isHidden', 'asc') .orderBy('person.isFavorite', 'desc') .having((eb) => eb.or([ eb('person.name', '!=', ''), eb( - (innerEb) => innerEb.fn.count('asset_face.assetId'), + 'person.assetCount', '>=', sql`COALESCE( (SELECT value -> 'people' ->> 'minimumFaces' @@ -201,7 +192,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('person.assetCount', 'desc') .orderBy(sql`NULLIF(person.name, '')`, (om) => om.asc().nullsLast()) .orderBy('person.createdAt'), ) @@ -369,24 +360,7 @@ export class PersonRepository { const zero = sql.lit(0); return this.db .selectFrom('person') - .where((eb) => - eb.exists((eb) => - eb - .selectFrom('asset_face') - .whereRef('asset_face.personId', '=', 'person.id') - .where('asset_face.deletedAt', 'is', null) - .where('asset_face.isVisible', '=', true) - .where((eb) => - eb.exists((eb) => - eb - .selectFrom('asset') - .whereRef('asset.id', '=', 'asset_face.assetId') - .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) - .where('asset.deletedAt', 'is', null), - ), - ), - ), - ) + .where('person.assetCount', '>', 0) .where('person.ownerId', '=', userId) .select((eb) => eb.fn.coalesce(eb.fn.countAll(), zero).as('total')) .select((eb) => eb.fn.coalesce(eb.fn.countAll().filterWhere('isHidden', '=', true), zero).as('hidden')) diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 2ee61d82dd..f16be0c7c5 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -219,6 +219,66 @@ export const person_delete_audit = registerFunction({ END`, }); +export const person_increment_asset_count = registerFunction({ + name: 'person_increment_asset_count', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + IF NEW."deletedAt" IS NULL AND NEW."isVisible" IS true THEN + UPDATE person SET "assetCount" = "assetCount" + 1 WHERE id = NEW."personId"; + END IF; + RETURN NULL; + END`, +}); + +export const person_update_asset_count = registerFunction({ + name: 'person_update_asset_count', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + IF OLD."personId" != NEW."personId" AND OLD."deletedAt" IS NULL AND OLD."isVisible" IS true THEN + UPDATE person SET "assetCount" = "assetCount" - 1 WHERE id = OLD."personId"; + END IF; + + IF OLD."personId" != NEW."personId" AND NEW."deletedAt" IS NULL AND OLD."isVisible" IS true THEN + UPDATE person SET "assetCount" = "assetCount" + 1 WHERE id = NEW."personId"; + END IF; + + IF + OLD."personId" = NEW."personId" AND + (OLD."deletedAt" IS NULL AND NEW."deletedAt" IS NOT NULL) OR + (OLD."isVisible" IS true AND NEW."isVisible" IS false) + THEN + UPDATE person SET "assetCount" = "assetCount" - 1 WHERE id = NEW."personId"; + END IF; + + IF + OLD."personId" = NEW."personId" AND + (OLD."deletedAt" IS NOT NULL AND NEW."deletedAt" IS NULL) OR + (OLD."isVisible" IS false AND NEW."isVisible" IS true) + THEN + UPDATE person SET "assetCount" = "assetCount" + 1 WHERE id = NEW."personId"; + END IF; + + RETURN NULL; + END`, +}); + +export const person_decrement_asset_count = registerFunction({ + name: 'person_decrement_asset_count', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + IF OLD."deletedAt" IS NULL AND OLD."isVisible" IS true THEN + UPDATE person SET "assetCount" = "assetCount" - 1 WHERE id = OLD."personId"; + END IF; + RETURN NULL; + END`, +}); + export const user_metadata_audit = registerFunction({ name: 'user_metadata_audit', returnType: 'TRIGGER', diff --git a/server/src/schema/migrations/1782147568849-AddPersonAssetCountColumn.ts b/server/src/schema/migrations/1782147568849-AddPersonAssetCountColumn.ts new file mode 100644 index 0000000000..6025edb5b4 --- /dev/null +++ b/server/src/schema/migrations/1782147568849-AddPersonAssetCountColumn.ts @@ -0,0 +1,100 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "person" ADD "assetCount" integer NOT NULL DEFAULT 0;`.execute(db); + await sql`UPDATE "person" SET "assetCount" = ( + SELECT COUNT("assetId") + FROM "asset_face" + WHERE "personId" = "person".id AND "deletedAt" IS NULL AND "isVisible" IS true + )`.execute(db); + await sql`CREATE INDEX "idx_person_assetCount" ON "person" ("assetCount");`.execute(db); + await sql`CREATE OR REPLACE FUNCTION person_increment_asset_count() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + IF NEW."deletedAt" IS NULL AND NEW."isVisible" IS true THEN + UPDATE person SET "assetCount" = "assetCount" + 1 WHERE id = NEW."personId"; + END IF; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION person_update_asset_count() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + IF OLD."personId" != NEW."personId" AND OLD."deletedAt" IS NULL AND OLD."isVisible" IS true THEN + UPDATE person SET "assetCount" = "assetCount" - 1 WHERE id = OLD."personId"; + END IF; + + IF OLD."personId" != NEW."personId" AND NEW."deletedAt" IS NULL AND OLD."isVisible" IS true THEN + UPDATE person SET "assetCount" = "assetCount" + 1 WHERE id = NEW."personId"; + END IF; + + IF + OLD."personId" = NEW."personId" AND + (OLD."deletedAt" IS NULL AND NEW."deletedAt" IS NOT NULL) OR + (OLD."isVisible" IS true AND NEW."isVisible" IS false) + THEN + UPDATE person SET "assetCount" = "assetCount" - 1 WHERE id = NEW."personId"; + END IF; + + IF + OLD."personId" = NEW."personId" AND + (OLD."deletedAt" IS NOT NULL AND NEW."deletedAt" IS NULL) OR + (OLD."isVisible" IS false AND NEW."isVisible" IS true) + THEN + UPDATE person SET "assetCount" = "assetCount" + 1 WHERE id = NEW."personId"; + END IF; + + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE FUNCTION person_decrement_asset_count() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + IF OLD."deletedAt" IS NULL AND OLD."isVisible" IS true THEN + UPDATE person SET "assetCount" = "assetCount" - 1 WHERE id = OLD."personId"; + END IF; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_face_delete_decrement_person_asset_count_trigger" + AFTER DELETE ON "asset_face" + FOR EACH ROW + EXECUTE FUNCTION person_decrement_asset_count();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_face_update_increment_person_asset_count_trigger" + AFTER UPDATE ON "asset_face" + FOR EACH ROW + EXECUTE FUNCTION person_update_asset_count();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "asset_face_insert_increment_person_asset_count_trigger" + AFTER INSERT ON "asset_face" + FOR EACH ROW + EXECUTE FUNCTION person_increment_asset_count();`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_person_increment_asset_count', '{"type":"function","name":"person_increment_asset_count","sql":"CREATE OR REPLACE FUNCTION person_increment_asset_count()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n IF NEW.\\"deletedAt\\" IS NULL AND NEW.\\"isVisible\\" IS true THEN\\n UPDATE person SET \\"assetCount\\" = \\"assetCount\\" + 1 WHERE id = NEW.\\"personId\\";\\n END IF;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_person_update_asset_count', '{"type":"function","name":"person_update_asset_count","sql":"CREATE OR REPLACE FUNCTION person_update_asset_count()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n IF OLD.\\"personId\\" != NEW.\\"personId\\" AND OLD.\\"deletedAt\\" IS NULL AND OLD.\\"isVisible\\" IS true THEN\\n UPDATE person SET \\"assetCount\\" = \\"assetCount\\" - 1 WHERE id = OLD.\\"personId\\";\\n END IF;\\n\\n IF OLD.\\"personId\\" != NEW.\\"personId\\" AND NEW.\\"deletedAt\\" IS NULL AND OLD.\\"isVisible\\" IS true THEN\\n UPDATE person SET \\"assetCount\\" = \\"assetCount\\" + 1 WHERE id = NEW.\\"personId\\";\\n END IF;\\n\\n IF\\n OLD.\\"personId\\" = NEW.\\"personId\\" AND\\n (OLD.\\"deletedAt\\" IS NULL AND NEW.\\"deletedAt\\" IS NOT NULL) OR\\n (OLD.\\"isVisible\\" IS true AND NEW.\\"isVisible\\" IS false)\\n THEN\\n UPDATE person SET \\"assetCount\\" = \\"assetCount\\" - 1 WHERE id = NEW.\\"personId\\";\\n END IF;\\n\\n IF\\n OLD.\\"personId\\" = NEW.\\"personId\\" AND\\n (OLD.\\"deletedAt\\" IS NOT NULL AND NEW.\\"deletedAt\\" IS NULL) OR\\n (OLD.\\"isVisible\\" IS false AND NEW.\\"isVisible\\" IS true)\\n THEN\\n UPDATE person SET \\"assetCount\\" = \\"assetCount\\" + 1 WHERE id = NEW.\\"personId\\";\\n END IF;\\n\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_person_decrement_asset_count', '{"type":"function","name":"person_decrement_asset_count","sql":"CREATE OR REPLACE FUNCTION person_decrement_asset_count()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n IF OLD.\\"deletedAt\\" IS NULL AND OLD.\\"isVisible\\" IS true THEN\\n UPDATE person SET \\"assetCount\\" = \\"assetCount\\" - 1 WHERE id = OLD.\\"personId\\";\\n END IF;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_face_delete_decrement_person_asset_count_trigger', '{"type":"trigger","name":"asset_face_delete_decrement_person_asset_count_trigger","sql":"CREATE OR REPLACE TRIGGER \\"asset_face_delete_decrement_person_asset_count_trigger\\"\\n AFTER DELETE ON \\"asset_face\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION person_decrement_asset_count();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_face_update_increment_person_asset_count_trigger', '{"type":"trigger","name":"asset_face_update_increment_person_asset_count_trigger","sql":"CREATE OR REPLACE TRIGGER \\"asset_face_update_increment_person_asset_count_trigger\\"\\n AFTER UPDATE ON \\"asset_face\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION person_update_asset_count();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_face_insert_increment_person_asset_count_trigger', '{"type":"trigger","name":"asset_face_insert_increment_person_asset_count_trigger","sql":"CREATE OR REPLACE TRIGGER \\"asset_face_insert_increment_person_asset_count_trigger\\"\\n AFTER INSERT ON \\"asset_face\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION person_increment_asset_count();"}'::jsonb);`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP TRIGGER "asset_face_insert_increment_person_asset_count_trigger" ON "asset_face";`.execute(db); + await sql`DROP FUNCTION person_increment_asset_count;`.execute(db); + await sql`DROP TRIGGER "asset_face_update_increment_person_asset_count_trigger" ON "asset_face";`.execute(db); + await sql`DROP FUNCTION person_update_asset_count;`.execute(db); + await sql`DROP TRIGGER "asset_face_delete_decrement_person_asset_count_trigger" ON "asset_face";`.execute(db); + await sql`DROP FUNCTION person_decrement_asset_count;`.execute(db); + await sql`DROP INDEX "idx_person_assetCount";`.execute(db); + await sql`ALTER TABLE "person" DROP COLUMN "assetCount";`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_person_increment_asset_count';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_person_update_asset_count';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_person_decrement_asset_count';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_face_delete_decrement_person_asset_count_trigger';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_face_update_increment_person_asset_count_trigger';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_face_insert_increment_person_asset_count_trigger';`.execute(db); +} diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts index b67e5e5dac..3e6252226d 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -1,5 +1,7 @@ import { AfterDeleteTrigger, + AfterInsertTrigger, + AfterUpdateTrigger, Column, DeleteDateColumn, ForeignKeyColumn, @@ -13,7 +15,12 @@ import { import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { SourceType } from 'src/enum'; import { asset_face_source_type } from 'src/schema/enums'; -import { asset_face_audit } from 'src/schema/functions'; +import { + asset_face_audit, + person_decrement_asset_count, + person_increment_asset_count, + person_update_asset_count, +} from 'src/schema/functions'; import { AssetTable } from 'src/schema/tables/asset.table'; import { PersonTable } from 'src/schema/tables/person.table'; @@ -25,6 +32,21 @@ import { PersonTable } from 'src/schema/tables/person.table'; referencingOldTableAs: 'old', when: 'pg_trigger_depth() = 0', }) +@AfterInsertTrigger({ + name: 'asset_face_insert_increment_person_asset_count_trigger', + scope: 'row', + function: person_increment_asset_count, +}) +@AfterUpdateTrigger({ + name: 'asset_face_update_increment_person_asset_count_trigger', + scope: 'row', + function: person_update_asset_count, +}) +@AfterDeleteTrigger({ + name: 'asset_face_delete_decrement_person_asset_count_trigger', + scope: 'row', + function: person_decrement_asset_count, +}) // schemaFromDatabase does not preserve column order @Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] }) @Index({ diff --git a/server/src/schema/tables/person.table.ts b/server/src/schema/tables/person.table.ts index 35447acfd0..a72840ea13 100644 --- a/server/src/schema/tables/person.table.ts +++ b/server/src/schema/tables/person.table.ts @@ -22,6 +22,10 @@ import { UserTable } from 'src/schema/tables/user.table'; using: 'gin', expression: 'f_unaccent("name") gin_trgm_ops', }) +@Index({ + name: 'idx_person_assetCount', + columns: ['assetCount'], +}) @UpdatedAtTrigger('person_updatedAt') @AfterDeleteTrigger({ scope: 'statement', @@ -66,4 +70,7 @@ export class PersonTable { @UpdateIdColumn({ index: true }) updateId!: Generated; + + @Column({ type: 'integer', default: 0 }) + assetCount!: Generated; } diff --git a/server/test/factories/person.factory.ts b/server/test/factories/person.factory.ts index 8e016e5398..6c5e0245f4 100644 --- a/server/test/factories/person.factory.ts +++ b/server/test/factories/person.factory.ts @@ -12,6 +12,7 @@ export class PersonFactory { static from(dto: PersonLike = {}) { return new PersonFactory({ + assetCount: 0, birthDate: null, color: null, createdAt: newDate(),