From e414d43eee9e36d977c7064a7b12d84cc9c008e4 Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:56:30 -0500 Subject: [PATCH] single statement --- server/src/repositories/asset.repository.ts | 160 ++++++++---------- ...66946512-AddLockedPropertiesToAssetExif.ts | 9 - server/src/schema/tables/asset-exif.table.ts | 9 +- 3 files changed, 76 insertions(+), 102 deletions(-) delete mode 100644 server/src/schema/migrations/1764866946512-AddLockedPropertiesToAssetExif.ts diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 3f7568b572..5ccfede432 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely'; -import { intersection, isEmpty, isUndefined, omit, omitBy, union } from 'lodash'; +import { ExpressionBuilder, Insertable, Kysely, NotNull, Selectable, sql, Updateable, UpdateResult } from 'kysely'; +import { isEmpty, isUndefined, omitBy } from 'lodash'; import { InjectKysely } from 'nestjs-kysely'; import { Stack } from 'src/database'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileType, AssetMetadataKey, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; -import { AssetExifTable, lockableProperties, LockableProperty } from 'src/schema/tables/asset-exif.table'; +import { AssetExifTable, LockableProperty } from 'src/schema/tables/asset-exif.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; import { AssetTable } from 'src/schema/tables/asset.table'; @@ -113,6 +113,9 @@ interface GetByIdsRelations { tags?: boolean; } +const distinctLocked = (eb: ExpressionBuilder, columns: T) => + sql`nullif(array(select distinct unnest(${eb.ref('asset_exif.lockedProperties')} || ${columns})), '{}')`; + @Injectable() export class AssetRepository { constructor(@InjectKysely() private db: Kysely) {} @@ -121,79 +124,60 @@ export class AssetRepository { exif: Insertable, { lockedPropertiesBehavior }: { lockedPropertiesBehavior: 'none' | 'update' | 'skip' }, ): Promise { - await this.db.transaction().execute(async (tx) => { - const lockedProperties = await tx - .selectFrom('asset_exif') - .select('asset_exif.lockedProperties') - .where('asset_exif.assetId', '=', exif.assetId) - .executeTakeFirst() - .then((result) => result?.lockedProperties ?? []); - - let value = { ...exif, assetId: asUuid(exif.assetId) }; - - switch (lockedPropertiesBehavior) { - case 'skip': { - value = omit(value, [...lockedProperties, 'lockedProperties']); - break; - } - - case 'update': { - const updatedLockableProperties = intersection(lockableProperties, Object.keys(exif)) as LockableProperty[]; - value = { - ...value, - lockedProperties: union(updatedLockableProperties, lockedProperties), - }; - break; - } - } - - if (Object.keys(value).length <= 1) { - return; - } - - return tx - .insertInto('asset_exif') - .values(value) - .onConflict((oc) => - oc.column('assetId').doUpdateSet((eb) => - removeUndefinedKeys( - { - description: eb.ref('excluded.description'), - exifImageWidth: eb.ref('excluded.exifImageWidth'), - exifImageHeight: eb.ref('excluded.exifImageHeight'), - fileSizeInByte: eb.ref('excluded.fileSizeInByte'), - orientation: eb.ref('excluded.orientation'), - dateTimeOriginal: eb.ref('excluded.dateTimeOriginal'), - modifyDate: eb.ref('excluded.modifyDate'), - timeZone: eb.ref('excluded.timeZone'), - latitude: eb.ref('excluded.latitude'), - longitude: eb.ref('excluded.longitude'), - projectionType: eb.ref('excluded.projectionType'), - city: eb.ref('excluded.city'), - livePhotoCID: eb.ref('excluded.livePhotoCID'), - autoStackId: eb.ref('excluded.autoStackId'), - state: eb.ref('excluded.state'), - country: eb.ref('excluded.country'), - make: eb.ref('excluded.make'), - model: eb.ref('excluded.model'), - lensModel: eb.ref('excluded.lensModel'), - fNumber: eb.ref('excluded.fNumber'), - focalLength: eb.ref('excluded.focalLength'), - iso: eb.ref('excluded.iso'), - exposureTime: eb.ref('excluded.exposureTime'), - profileDescription: eb.ref('excluded.profileDescription'), - colorspace: eb.ref('excluded.colorspace'), - bitsPerSample: eb.ref('excluded.bitsPerSample'), - rating: eb.ref('excluded.rating'), - fps: eb.ref('excluded.fps'), - lockedProperties: eb.ref('excluded.lockedProperties'), - }, - value, - ), - ), - ) - .execute(); - }); + await this.db + .insertInto('asset_exif') + .values(exif) + .onConflict((oc) => + oc.column('assetId').doUpdateSet((eb) => { + const updateLocked = (col: T) => eb.ref(`excluded.${col}`); + const skipLocked = (col: T) => + eb + .case() + .when(sql`${col}`, '=', eb.fn.any('asset_exif.lockedProperties')) + .then(eb.ref(`asset_exif.${col}`)) + .else(eb.ref(`excluded.${col}`)) + .end(); + const ref = lockedPropertiesBehavior === 'update' ? updateLocked : skipLocked; + return removeUndefinedKeys( + { + description: ref('description'), + exifImageWidth: ref('exifImageWidth'), + exifImageHeight: ref('exifImageHeight'), + fileSizeInByte: ref('fileSizeInByte'), + orientation: ref('orientation'), + dateTimeOriginal: ref('dateTimeOriginal'), + modifyDate: ref('modifyDate'), + timeZone: ref('timeZone'), + latitude: ref('latitude'), + longitude: ref('longitude'), + projectionType: ref('projectionType'), + city: ref('city'), + livePhotoCID: ref('livePhotoCID'), + autoStackId: ref('autoStackId'), + state: ref('state'), + country: ref('country'), + make: ref('make'), + model: ref('model'), + lensModel: ref('lensModel'), + fNumber: ref('fNumber'), + focalLength: eb.ref('excluded.focalLength'), + iso: ref('iso'), + exposureTime: ref('exposureTime'), + profileDescription: ref('profileDescription'), + colorspace: ref('colorspace'), + bitsPerSample: ref('bitsPerSample'), + rating: ref('rating'), + fps: ref('fps'), + lockedProperties: + exif.lockedProperties === undefined || lockedPropertiesBehavior === 'none' + ? undefined + : distinctLocked(eb, exif.lockedProperties), + }, + exif, + ); + }), + ) + .execute(); } @GenerateSql({ params: [[DummyValue.UUID], { model: DummyValue.STRING }] }) @@ -207,11 +191,7 @@ export class AssetRepository { .updateTable('asset_exif') .set((eb) => ({ ...options, - lockedProperties: eb - .fn< - LockableProperty[] - >('array', [sql`select distinct unnest(${eb.fn('array_cat', ['lockedProperties', eb.val(Object.keys(options))])})`]) - .as('lockedProperties').expression, + lockedProperties: distinctLocked(eb, Object.keys(options) as LockableProperty[]), })) .where('assetId', 'in', ids) .execute(); @@ -219,21 +199,17 @@ export class AssetRepository { @GenerateSql({ params: [[DummyValue.UUID], DummyValue.NUMBER, DummyValue.STRING] }) @Chunked() - async updateDateTimeOriginal( - ids: string[], - delta?: number, - timeZone?: string, - ): Promise<{ assetId: string; dateTimeOriginal: Date | null; timeZone: string | null }[]> { - return await this.db + updateDateTimeOriginal(ids: string[], delta?: number, timeZone?: string) { + if (ids.length === 0) { + return; + } + + return this.db .updateTable('asset_exif') .set((eb) => ({ dateTimeOriginal: sql`"dateTimeOriginal" + ${(delta ?? 0) + ' minute'}::interval`, timeZone, - lockedProperties: eb - .fn< - LockableProperty[] - >('array', [sql`select distinct unnest(${eb.fn('array_cat', ['lockedProperties', eb.val(['dateTimeOriginal', 'timeZone'])])})`]) - .as('lockedProperties').expression, + lockedProperties: distinctLocked(eb, ['dateTimeOriginal', 'timeZone']), })) .where('assetId', 'in', ids) .returning(['assetId', 'dateTimeOriginal', 'timeZone']) diff --git a/server/src/schema/migrations/1764866946512-AddLockedPropertiesToAssetExif.ts b/server/src/schema/migrations/1764866946512-AddLockedPropertiesToAssetExif.ts deleted file mode 100644 index 5a9f652b8c..0000000000 --- a/server/src/schema/migrations/1764866946512-AddLockedPropertiesToAssetExif.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Kysely, sql } from 'kysely'; - -export async function up(db: Kysely): Promise { - await sql`ALTER TABLE "asset_exif" ADD "lockedProperties" character varying[] NOT NULL DEFAULT '{}';`.execute(db); -} - -export async function down(db: Kysely): Promise { - await sql`ALTER TABLE "asset_exif" DROP COLUMN "lockedProperties";`.execute(db); -} diff --git a/server/src/schema/tables/asset-exif.table.ts b/server/src/schema/tables/asset-exif.table.ts index e25131193a..833e9f9385 100644 --- a/server/src/schema/tables/asset-exif.table.ts +++ b/server/src/schema/tables/asset-exif.table.ts @@ -3,7 +3,14 @@ import { AssetTable } from 'src/schema/tables/asset.table'; import { Column, ForeignKeyColumn, Generated, Int8, Table, Timestamp, UpdateDateColumn } from 'src/sql-tools'; export type LockableProperty = (typeof lockableProperties)[number]; -export const lockableProperties = ['description', 'dateTimeOriginal', 'latitude', 'longitude', 'rating'] as const; +export const lockableProperties = [ + 'description', + 'dateTimeOriginal', + 'latitude', + 'longitude', + 'rating', + 'timeZone', +] as const; @Table('asset_exif') @UpdatedAtTrigger('asset_exif_updatedAt')