2026-02-23 15:50:16 +01:00
import { createPostgres , DatabaseConnectionParams } from '@immich/sql-tools' ;
2025-04-15 13:26:56 -04:00
import {
2026-01-09 17:59:52 -05:00
AliasedRawBuilder ,
2025-04-18 23:39:56 +02:00
DeduplicateJoinsPlugin ,
2025-04-15 13:26:56 -04:00
Expression ,
ExpressionBuilder ,
2025-04-18 23:39:56 +02:00
Kysely ,
2025-04-15 13:26:56 -04:00
KyselyConfig ,
2026-03-11 16:17:31 +01:00
NotNull ,
2025-04-15 13:26:56 -04:00
Selectable ,
2025-04-18 23:39:56 +02:00
SelectQueryBuilder ,
2026-03-11 16:17:31 +01:00
ShallowDehydrateObject ,
2025-04-15 13:26:56 -04:00
sql ,
} from 'kysely' ;
import { PostgresJSDialect } from 'kysely-postgres-js' ;
2025-04-18 23:39:56 +02:00
import { jsonArrayFrom , jsonObjectFrom } from 'kysely/helpers/postgres' ;
2026-02-23 15:50:16 +01:00
import { Notice , PostgresError } from 'postgres' ;
2026-03-11 16:17:31 +01:00
import { columns , lockableProperties , LockableProperty , Person } from 'src/database' ;
2026-01-09 17:59:52 -05:00
import { AssetEditActionItem } from 'src/dtos/editing.dto' ;
2026-05-11 14:35:10 -07:00
import { AssetFileType , AssetOrderBy , AssetVisibility , DatabaseExtension , ExifOrientation } from 'src/enum' ;
2025-04-18 23:39:56 +02:00
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository' ;
2025-06-30 13:19:16 -04:00
import { DB } from 'src/schema' ;
2026-03-11 16:17:31 +01:00
import { AssetExifTable } from 'src/schema/tables/asset-exif.table' ;
2026-05-04 09:04:29 -04:00
import { AudioStreamInfo , VectorExtension , VideoFormat , VideoPacketInfo , VideoStreamInfo } from 'src/types' ;
2025-04-15 13:26:56 -04:00
2026-02-23 15:50:16 +01:00
export const getKyselyConfig = ( connection : DatabaseConnectionParams ) : KyselyConfig = > {
2025-04-15 13:26:56 -04:00
return {
dialect : new PostgresJSDialect ( {
2026-02-23 15:50:16 +01:00
postgres : createPostgres ( {
connection ,
onNotice : ( notice : Notice ) = > {
2025-04-15 13:26:56 -04:00
if ( notice [ 'severity' ] !== 'NOTICE' ) {
console . warn ( 'Postgres notice:' , notice ) ;
}
} ,
} ) ,
} ) ,
log ( event ) {
if ( event . level === 'error' ) {
2026-02-10 15:43:55 -05:00
if ( isAssetChecksumConstraint ( event . error ) ) {
return ;
}
2025-04-15 13:26:56 -04:00
console . error ( 'Query failed :' , {
durationMs : event.queryDurationMillis ,
error : event.error ,
sql : event.query.sql ,
params : event.query.parameters ,
} ) ;
}
} ,
} ;
} ;
2023-12-08 11:15:46 -05:00
2025-01-09 11:15:41 -05:00
export const asUuid = ( id : string | Expression < string > ) = > sql < string > ` ${ id } ::uuid ` ;
2024-02-17 11:00:55 -06:00
2025-01-09 11:15:41 -05:00
export const anyUuid = ( ids : string [ ] ) = > sql < string > ` any( ${ ` { ${ ids } } ` } ::uuid[]) ` ;
2024-02-12 20:50:47 -05:00
2025-01-21 19:12:28 +01:00
export const asVector = ( embedding : number [ ] ) = > sql < string > ` ${ ` [ ${ embedding } ] ` } ::vector ` ;
2024-02-12 20:50:47 -05:00
2025-03-06 16:00:18 +01:00
export const unnest = ( array : string [ ] ) = > sql < Record < string , string > > ` unnest(array[ ${ sql . join ( array ) } ]::text[]) ` ;
2025-03-06 13:33:24 -05:00
export const removeUndefinedKeys = < T extends object > ( update : T , template : unknown ) = > {
for ( const key in update ) {
if ( ( template as T ) [ key ] === undefined ) {
delete update [ key ] ;
}
}
return update ;
} ;
2025-04-11 14:44:45 -04:00
2025-04-18 23:39:56 +02:00
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum' ;
2026-06-01 14:52:29 -04:00
export const VIDEO_STREAM_SESSION_PK_CONSTRAINT = 'video_stream_session_pkey' ;
2025-04-18 23:39:56 +02:00
2026-06-01 14:52:29 -04:00
export const isAssetChecksumConstraint = ( error : unknown ) = >
( error as PostgresError ) ? . constraint_name === ASSET_CHECKSUM_CONSTRAINT ;
export const isVideoStreamSessionPkConstraint = ( error : unknown ) = >
( error as PostgresError ) ? . constraint_name === VIDEO_STREAM_SESSION_PK_CONSTRAINT ;
2025-08-27 15:10:55 -04:00
2025-07-14 10:13:06 -04:00
export function withDefaultVisibility < O > ( qb : SelectQueryBuilder < DB , 'asset' , O > ) {
2025-07-15 14:50:13 -04:00
return qb . where ( 'asset.visibility' , 'in' , [ sql . lit ( AssetVisibility . Archive ) , sql . lit ( AssetVisibility . Timeline ) ] ) ;
2025-05-06 12:12:48 -05:00
}
2025-06-02 10:33:08 -04:00
// TODO come up with a better query that only selects the fields we need
2025-07-14 10:13:06 -04:00
export function withExif < O > ( qb : SelectQueryBuilder < DB , 'asset' , O > ) {
2025-04-18 23:39:56 +02:00
return qb
2025-07-14 10:13:06 -04:00
. leftJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
2026-03-11 16:17:31 +01:00
. select ( ( eb ) = >
eb . fn
. toJson ( eb . table ( 'asset_exif' ) )
. $castTo < ShallowDehydrateObject < Selectable < AssetExifTable > > | null > ( )
. as ( 'exifInfo' ) ,
) ;
2025-04-18 23:39:56 +02:00
}
2025-07-14 10:13:06 -04:00
export function withExifInner < O > ( qb : SelectQueryBuilder < DB , 'asset' , O > ) {
2025-04-18 23:39:56 +02:00
return qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
2026-03-11 16:17:31 +01:00
. select ( ( eb ) = > eb . fn . toJson ( eb . table ( 'asset_exif' ) ) . as ( 'exifInfo' ) )
. $narrowType < { exifInfo : NotNull } > ( ) ;
2025-04-18 23:39:56 +02:00
}
2026-05-01 13:03:49 -04:00
export const dummy = sql ` (select 1) ` . as ( 'dummy' ) ;
export function withAudioStream ( eb : ExpressionBuilder < DB , 'asset_exif' | 'asset_audio' > ) {
return jsonObjectFrom (
eb
. selectFrom ( dummy )
. select ( [ 'asset_audio.index' , 'asset_audio.codecName' , 'asset_audio.profile' , 'asset_audio.bitrate' ] )
. where ( 'asset_audio.assetId' , 'is not' , sql . lit ( null ) )
. $castTo < AudioStreamInfo | null > ( ) ,
) ;
}
export function withVideoStream ( eb : ExpressionBuilder < DB , 'asset_exif' | 'asset_video' > ) {
return jsonObjectFrom (
eb
. selectFrom ( dummy )
. select ( ( eb ) = > [
'asset_video.index' ,
'asset_video.codecName' ,
'asset_video.profile' ,
'asset_video.level' ,
'asset_video.bitrate' ,
'asset_exif.exifImageWidth as width' ,
'asset_exif.exifImageHeight as height' ,
'asset_video.pixelFormat' ,
'asset_video.frameCount' ,
'asset_exif.fps as frameRate' ,
'asset_video.timeBase' ,
eb
. case ( )
. when ( 'asset_exif.orientation' , '=' , sql . lit ( ExifOrientation . Rotate90CW . toString ( ) ) )
. then ( sql . lit ( - 90 ) )
. when ( 'asset_exif.orientation' , '=' , sql . lit ( ExifOrientation . Rotate270CW . toString ( ) ) )
. then ( sql . lit ( 90 ) )
. when ( 'asset_exif.orientation' , '=' , sql . lit ( ExifOrientation . Rotate180 . toString ( ) ) )
. then ( sql . lit ( 180 ) )
. else ( 0 )
. end ( )
. as ( 'rotation' ) ,
'asset_video.colorPrimaries' ,
'asset_video.colorMatrix' ,
'asset_video.colorTransfer' ,
'asset_video.dvProfile' ,
'asset_video.dvLevel' ,
'asset_video.dvBlSignalCompatibilityId' ,
] )
. where ( 'asset_video.assetId' , 'is not' , sql . lit ( null ) ) ,
2026-05-04 09:04:29 -04:00
) . $castTo < ( VideoStreamInfo & { timeBase : number } ) | null > ( ) ;
2026-05-01 13:03:49 -04:00
}
export function withVideoFormat ( eb : ExpressionBuilder < DB , 'asset' | 'asset_video' > ) {
return jsonObjectFrom (
eb
. selectFrom ( dummy )
. select ( [ 'asset_video.formatName' , 'asset_video.formatLongName' , 'asset.duration' , 'asset_video.bitrate' ] )
. where ( 'asset_video.assetId' , 'is not' , sql . lit ( null ) ) ,
) . $castTo < VideoFormat | null > ( ) ;
}
2026-05-04 09:04:29 -04:00
export function withVideoPackets ( eb : ExpressionBuilder < DB , 'asset' | 'asset_keyframe' > ) {
return jsonObjectFrom (
eb
. selectFrom ( dummy )
. where ( 'asset_keyframe.assetId' , 'is not' , sql . lit ( null ) )
. select ( [
'asset_keyframe.pts as keyframePts' ,
'asset_keyframe.accDuration as keyframeAccDuration' ,
'asset_keyframe.ownDuration as keyframeOwnDuration' ,
'asset_keyframe.totalDuration' ,
'asset_keyframe.packetCount' ,
'asset_keyframe.outputFrames' ,
] ) ,
) . $castTo < VideoPacketInfo | null > ( ) ;
}
2025-07-14 10:13:06 -04:00
export function withSmartSearch < O > ( qb : SelectQueryBuilder < DB , 'asset' , O > ) {
2025-04-18 23:39:56 +02:00
return qb
2025-07-14 10:13:06 -04:00
. leftJoin ( 'smart_search' , 'asset.id' , 'smart_search.assetId' )
2026-03-11 16:17:31 +01:00
. select ( ( eb ) = > jsonObjectFrom ( eb . table ( 'smart_search' ) ) . as ( 'smartSearch' ) ) ;
2025-04-18 23:39:56 +02:00
}
2026-01-09 17:59:52 -05:00
export function withFaces ( eb : ExpressionBuilder < DB , 'asset' > , withHidden? : boolean , withDeletedFace? : boolean ) {
2025-04-18 23:39:56 +02:00
return jsonArrayFrom (
eb
2025-07-14 10:13:06 -04:00
. selectFrom ( 'asset_face' )
. selectAll ( 'asset_face' )
. whereRef ( 'asset_face.assetId' , '=' , 'asset.id' )
2026-01-09 17:59:52 -05:00
. $if ( ! withDeletedFace , ( qb ) = > qb . where ( 'asset_face.deletedAt' , 'is' , null ) )
. $if ( ! withHidden , ( qb ) = > qb . where ( 'asset_face.isVisible' , '=' , true ) ) ,
2025-04-18 23:39:56 +02:00
) . as ( 'faces' ) ;
}
2025-07-14 10:13:06 -04:00
export function withFiles ( eb : ExpressionBuilder < DB , 'asset' > , type ? : AssetFileType ) {
2025-04-18 23:39:56 +02:00
return jsonArrayFrom (
eb
2025-07-14 10:13:06 -04:00
. selectFrom ( 'asset_file' )
2025-04-18 23:39:56 +02:00
. select ( columns . assetFiles )
2025-07-14 10:13:06 -04:00
. whereRef ( 'asset_file.assetId' , '=' , 'asset.id' )
. $if ( ! ! type , ( qb ) = > qb . where ( 'asset_file.type' , '=' , type ! ) ) ,
2025-04-18 23:39:56 +02:00
) . as ( 'files' ) ;
}
2026-04-01 07:19:38 -05:00
export function withFilePath ( eb : ExpressionBuilder < DB , 'asset' > , type : AssetFileType , isEdited = false ) {
2025-10-27 22:09:55 +08:00
return eb
. selectFrom ( 'asset_file' )
. select ( 'asset_file.path' )
. whereRef ( 'asset_file.assetId' , '=' , 'asset.id' )
2026-04-01 07:19:38 -05:00
. where ( 'asset_file.type' , '=' , sql . lit ( type ) )
. where ( 'asset_file.isEdited' , '=' , sql . lit ( isEdited ) ) ;
2025-10-27 22:09:55 +08:00
}
2026-01-09 17:59:52 -05:00
export function withFacesAndPeople (
eb : ExpressionBuilder < DB , 'asset' > ,
withHidden? : boolean ,
withDeletedFace? : boolean ,
) {
2025-04-18 23:39:56 +02:00
return jsonArrayFrom (
eb
2025-07-14 10:13:06 -04:00
. selectFrom ( 'asset_face' )
2025-04-18 23:39:56 +02:00
. leftJoinLateral (
( eb ) = >
2025-07-14 10:13:06 -04:00
eb . selectFrom ( 'person' ) . selectAll ( 'person' ) . whereRef ( 'asset_face.personId' , '=' , 'person.id' ) . as ( 'person' ) ,
2025-04-18 23:39:56 +02:00
( join ) = > join . onTrue ( ) ,
)
2025-07-14 10:13:06 -04:00
. selectAll ( 'asset_face' )
2026-03-11 16:17:31 +01:00
. select ( ( eb ) = > eb . table ( 'person' ) . $castTo < ShallowDehydrateObject < Person > > ( ) . as ( 'person' ) )
2025-07-14 10:13:06 -04:00
. whereRef ( 'asset_face.assetId' , '=' , 'asset.id' )
2026-01-09 17:59:52 -05:00
. $if ( ! withDeletedFace , ( qb ) = > qb . where ( 'asset_face.deletedAt' , 'is' , null ) )
. $if ( ! withHidden , ( qb ) = > qb . where ( 'asset_face.isVisible' , 'is' , true ) ) ,
2025-04-18 23:39:56 +02:00
) . as ( 'faces' ) ;
}
2025-07-14 10:13:06 -04:00
export function hasPeople < O > ( qb : SelectQueryBuilder < DB , 'asset' , O > , personIds : string [ ] ) {
2025-04-18 23:39:56 +02:00
return qb . innerJoin (
( eb ) = >
eb
2025-07-14 10:13:06 -04:00
. selectFrom ( 'asset_face' )
2025-04-18 23:39:56 +02:00
. select ( 'assetId' )
. where ( 'personId' , '=' , anyUuid ( personIds ! ) )
. where ( 'deletedAt' , 'is' , null )
2026-01-09 17:59:52 -05:00
. where ( 'isVisible' , 'is' , true )
2025-04-18 23:39:56 +02:00
. groupBy ( 'assetId' )
. having ( ( eb ) = > eb . fn . count ( 'personId' ) . distinct ( ) , '=' , personIds . length )
. as ( 'has_people' ) ,
2025-07-14 10:13:06 -04:00
( join ) = > join . onRef ( 'has_people.assetId' , '=' , 'asset.id' ) ,
2025-04-18 23:39:56 +02:00
) ;
}
2025-07-14 10:13:06 -04:00
export function inAlbums < O > ( qb : SelectQueryBuilder < DB , 'asset' , O > , albumIds : string [ ] ) {
2025-06-09 11:11:43 -04:00
return qb . innerJoin (
( eb ) = >
eb
2025-07-14 10:13:06 -04:00
. selectFrom ( 'album_asset' )
2025-11-04 16:03:21 -05:00
. select ( 'assetId' )
. where ( 'albumId' , '=' , anyUuid ( albumIds ! ) )
. groupBy ( 'assetId' )
. having ( ( eb ) = > eb . fn . count ( 'albumId' ) . distinct ( ) , '=' , albumIds . length )
2025-06-09 11:11:43 -04:00
. as ( 'has_album' ) ,
2025-11-04 16:03:21 -05:00
( join ) = > join . onRef ( 'has_album.assetId' , '=' , 'asset.id' ) ,
2025-06-09 11:11:43 -04:00
) ;
}
2025-07-14 10:13:06 -04:00
export function hasTags < O > ( qb : SelectQueryBuilder < DB , 'asset' , O > , tagIds : string [ ] ) {
2025-04-18 23:39:56 +02:00
return qb . innerJoin (
( eb ) = >
eb
. selectFrom ( 'tag_asset' )
2025-11-04 16:03:21 -05:00
. select ( 'assetId' )
. innerJoin ( 'tag_closure' , 'tag_asset.tagId' , 'tag_closure.id_descendant' )
2025-07-14 10:13:06 -04:00
. where ( 'tag_closure.id_ancestor' , '=' , anyUuid ( tagIds ) )
2025-11-04 16:03:21 -05:00
. groupBy ( 'assetId' )
2025-07-14 10:13:06 -04:00
. having ( ( eb ) = > eb . fn . count ( 'tag_closure.id_ancestor' ) . distinct ( ) , '>=' , tagIds . length )
2025-04-18 23:39:56 +02:00
. as ( 'has_tags' ) ,
2025-11-04 16:03:21 -05:00
( join ) = > join . onRef ( 'has_tags.assetId' , '=' , 'asset.id' ) ,
2025-04-18 23:39:56 +02:00
) ;
}
2025-07-14 10:13:06 -04:00
export function withOwner ( eb : ExpressionBuilder < DB , 'asset' > ) {
return jsonObjectFrom ( eb . selectFrom ( 'user' ) . select ( columns . user ) . whereRef ( 'user.id' , '=' , 'asset.ownerId' ) ) . as (
2025-04-18 23:39:56 +02:00
'owner' ,
) ;
}
2025-07-14 10:13:06 -04:00
export function withLibrary ( eb : ExpressionBuilder < DB , 'asset' > ) {
2025-04-18 23:39:56 +02:00
return jsonObjectFrom (
2025-07-14 10:13:06 -04:00
eb . selectFrom ( 'library' ) . selectAll ( 'library' ) . whereRef ( 'library.id' , '=' , 'asset.libraryId' ) ,
2025-04-18 23:39:56 +02:00
) . as ( 'library' ) ;
}
2025-07-14 10:13:06 -04:00
export function withTags ( eb : ExpressionBuilder < DB , 'asset' > ) {
2025-04-18 23:39:56 +02:00
return jsonArrayFrom (
eb
2025-07-14 10:13:06 -04:00
. selectFrom ( 'tag' )
2025-04-18 23:39:56 +02:00
. select ( columns . tag )
2025-11-04 16:03:21 -05:00
. innerJoin ( 'tag_asset' , 'tag.id' , 'tag_asset.tagId' )
. whereRef ( 'asset.id' , '=' , 'tag_asset.assetId' ) ,
2025-04-18 23:39:56 +02:00
) . as ( 'tags' ) ;
}
2026-05-11 14:35:10 -07:00
export function truncatedDate < O > ( order : AssetOrderBy = AssetOrderBy . TakenAt ) {
return sql < O > ` date_trunc( ${ sql . lit ( 'MONTH' ) } , ${ sql . ref ( order === AssetOrderBy . CreatedAt ? 'asset.createdAt' : 'localDateTime' ) } AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' ` ;
2025-04-18 23:39:56 +02:00
}
2025-07-14 10:13:06 -04:00
export function withTagId < O > ( qb : SelectQueryBuilder < DB , 'asset' , O > , tagId : string ) {
2025-04-18 23:39:56 +02:00
return qb . where ( ( eb ) = >
eb . exists (
eb
2025-07-14 10:13:06 -04:00
. selectFrom ( 'tag_closure' )
2025-11-04 16:03:21 -05:00
. innerJoin ( 'tag_asset' , 'tag_asset.tagId' , 'tag_closure.id_descendant' )
. whereRef ( 'tag_asset.assetId' , '=' , 'asset.id' )
2025-07-14 10:13:06 -04:00
. where ( 'tag_closure.id_ancestor' , '=' , tagId ) ,
2025-04-18 23:39:56 +02:00
) ,
) ;
}
2025-05-19 17:40:48 -04:00
2025-12-01 12:24:37 -05:00
const isCJK = ( c : number ) : boolean = >
( c >= 0x4e _00 && c <= 0x9f _ff ) ||
( c >= 0xac _00 && c <= 0xd7 _af ) ||
( c >= 0x30 _40 && c <= 0x30 _9f ) ||
( c >= 0x30 _a0 && c <= 0x30 _ff ) ||
( c >= 0x34 _00 && c <= 0x4d _bf ) ;
export const tokenizeForSearch = ( text : string ) : string [ ] = > {
/* eslint-disable unicorn/prefer-code-point */
const tokens : string [ ] = [ ] ;
let i = 0 ;
while ( i < text . length ) {
const c = text . charCodeAt ( i ) ;
if ( c <= 32 ) {
i ++ ;
continue ;
}
const start = i ;
if ( isCJK ( c ) ) {
while ( i < text . length && isCJK ( text . charCodeAt ( i ) ) ) {
i ++ ;
}
if ( i - start === 1 ) {
tokens . push ( text [ start ] ) ;
} else {
for ( let k = start ; k < i - 1 ; k ++ ) {
tokens . push ( text [ k ] + text [ k + 1 ] ) ;
}
}
} else {
while ( i < text . length && text . charCodeAt ( i ) > 32 && ! isCJK ( text . charCodeAt ( i ) ) ) {
i ++ ;
}
tokens . push ( text . slice ( start , i ) ) ;
}
}
return tokens ;
} ;
2026-01-09 17:59:52 -05:00
// needed to properly type the return with the EditActionItem discriminated union type
type AliasedEditActions = AliasedRawBuilder < AssetEditActionItem [ ] , 'edits' > ;
export function withEdits ( eb : ExpressionBuilder < DB , 'asset' > ) : AliasedEditActions {
return jsonArrayFrom (
eb
. selectFrom ( 'asset_edit' )
. select ( [ 'asset_edit.action' , 'asset_edit.parameters' ] )
. whereRef ( 'asset_edit.assetId' , '=' , 'asset.id' ) ,
) . as ( 'edits' ) as AliasedEditActions ;
}
2025-04-18 23:39:56 +02:00
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin ( ) ;
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
export function searchAssetBuilder ( kysely : Kysely < DB > , options : AssetSearchBuilderOptions ) {
options . withDeleted || = ! ! ( options . trashedAfter || options . trashedBefore || options . isOffline ) ;
2025-07-15 14:50:13 -04:00
const visibility = options . visibility == null ? AssetVisibility.Timeline : options.visibility ;
2025-05-06 12:12:48 -05:00
2025-04-18 23:39:56 +02:00
return kysely
. withPlugin ( joinDeduplicationPlugin )
2025-07-14 10:13:06 -04:00
. selectFrom ( 'asset' )
. where ( 'asset.visibility' , '=' , visibility )
2025-06-09 11:11:43 -04:00
. $if ( ! ! options . albumIds && options . albumIds . length > 0 , ( qb ) = > inAlbums ( qb , options . albumIds ! ) )
2025-04-18 23:39:56 +02:00
. $if ( ! ! options . tagIds && options . tagIds . length > 0 , ( qb ) = > hasTags ( qb , options . tagIds ! ) )
2025-07-10 16:28:20 +02:00
. $if ( options . tagIds === null , ( qb ) = >
2025-11-04 16:03:21 -05:00
qb . where ( ( eb ) = > eb . not ( eb . exists ( ( eb ) = > eb . selectFrom ( 'tag_asset' ) . whereRef ( 'assetId' , '=' , 'asset.id' ) ) ) ) ,
2025-07-10 16:28:20 +02:00
)
2025-04-18 23:39:56 +02:00
. $if ( ! ! options . personIds && options . personIds . length > 0 , ( qb ) = > hasPeople ( qb , options . personIds ! ) )
2025-07-14 10:13:06 -04:00
. $if ( ! ! options . createdBefore , ( qb ) = > qb . where ( 'asset.createdAt' , '<=' , options . createdBefore ! ) )
. $if ( ! ! options . createdAfter , ( qb ) = > qb . where ( 'asset.createdAt' , '>=' , options . createdAfter ! ) )
. $if ( ! ! options . updatedBefore , ( qb ) = > qb . where ( 'asset.updatedAt' , '<=' , options . updatedBefore ! ) )
. $if ( ! ! options . updatedAfter , ( qb ) = > qb . where ( 'asset.updatedAt' , '>=' , options . updatedAfter ! ) )
. $if ( ! ! options . trashedBefore , ( qb ) = > qb . where ( 'asset.deletedAt' , '<=' , options . trashedBefore ! ) )
. $if ( ! ! options . trashedAfter , ( qb ) = > qb . where ( 'asset.deletedAt' , '>=' , options . trashedAfter ! ) )
. $if ( ! ! options . takenBefore , ( qb ) = > qb . where ( 'asset.fileCreatedAt' , '<=' , options . takenBefore ! ) )
. $if ( ! ! options . takenAfter , ( qb ) = > qb . where ( 'asset.fileCreatedAt' , '>=' , options . takenAfter ! ) )
2025-04-18 23:39:56 +02:00
. $if ( options . city !== undefined , ( qb ) = >
qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
. where ( 'asset_exif.city' , options . city === null ? 'is' : '=' , options . city ! ) ,
2025-04-18 23:39:56 +02:00
)
. $if ( options . state !== undefined , ( qb ) = >
qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
. where ( 'asset_exif.state' , options . state === null ? 'is' : '=' , options . state ! ) ,
2025-04-18 23:39:56 +02:00
)
. $if ( options . country !== undefined , ( qb ) = >
qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
. where ( 'asset_exif.country' , options . country === null ? 'is' : '=' , options . country ! ) ,
2025-04-18 23:39:56 +02:00
)
. $if ( options . make !== undefined , ( qb ) = >
qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
. where ( 'asset_exif.make' , options . make === null ? 'is' : '=' , options . make ! ) ,
2025-04-18 23:39:56 +02:00
)
. $if ( options . model !== undefined , ( qb ) = >
qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
. where ( 'asset_exif.model' , options . model === null ? 'is' : '=' , options . model ! ) ,
2025-04-18 23:39:56 +02:00
)
. $if ( options . lensModel !== undefined , ( qb ) = >
qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
. where ( 'asset_exif.lensModel' , options . lensModel === null ? 'is' : '=' , options . lensModel ! ) ,
2025-04-18 23:39:56 +02:00
)
. $if ( options . rating !== undefined , ( qb ) = >
qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
. where ( 'asset_exif.rating' , options . rating === null ? 'is' : '=' , options . rating ! ) ,
2025-04-18 23:39:56 +02:00
)
2025-07-14 10:13:06 -04:00
. $if ( ! ! options . checksum , ( qb ) = > qb . where ( 'asset.checksum' , '=' , options . checksum ! ) )
. $if ( ! ! options . id , ( qb ) = > qb . where ( 'asset.id' , '=' , asUuid ( options . id ! ) ) )
. $if ( ! ! options . libraryId , ( qb ) = > qb . where ( 'asset.libraryId' , '=' , asUuid ( options . libraryId ! ) ) )
. $if ( ! ! options . userIds , ( qb ) = > qb . where ( 'asset.ownerId' , '=' , anyUuid ( options . userIds ! ) ) )
2026-03-12 15:15:21 -05:00
. $if ( ! ! options . encodedVideoPath , ( qb ) = >
qb
. innerJoin ( 'asset_file' , ( join ) = >
join
. onRef ( 'asset.id' , '=' , 'asset_file.assetId' )
. on ( 'asset_file.type' , '=' , AssetFileType . EncodedVideo )
. on ( 'asset_file.isEdited' , '=' , false ) ,
)
. where ( 'asset_file.path' , '=' , options . encodedVideoPath ! ) ,
)
2025-04-18 23:39:56 +02:00
. $if ( ! ! options . originalPath , ( qb ) = >
2025-07-14 10:13:06 -04:00
qb . where ( sql ` f_unaccent(asset."originalPath") ` , 'ilike' , sql ` '%' || f_unaccent( ${ options . originalPath } ) || '%' ` ) ,
2025-04-18 23:39:56 +02:00
)
. $if ( ! ! options . originalFileName , ( qb ) = >
qb . where (
2025-07-14 10:13:06 -04:00
sql ` f_unaccent(asset."originalFileName") ` ,
2025-04-18 23:39:56 +02:00
'ilike' ,
sql ` '%' || f_unaccent( ${ options . originalFileName } ) || '%' ` ,
) ,
)
. $if ( ! ! options . description , ( qb ) = >
qb
2025-07-14 10:13:06 -04:00
. innerJoin ( 'asset_exif' , 'asset.id' , 'asset_exif.assetId' )
. where ( sql ` f_unaccent(asset_exif.description) ` , 'ilike' , sql ` '%' || f_unaccent( ${ options . description } ) || '%' ` ) ,
2025-04-18 23:39:56 +02:00
)
2025-10-27 22:09:55 +08:00
. $if ( ! ! options . ocr , ( qb ) = >
qb
. innerJoin ( 'ocr_search' , 'asset.id' , 'ocr_search.assetId' )
2025-12-01 12:24:37 -05:00
. where ( ( ) = > sql ` f_unaccent(ocr_search.text) %>> f_unaccent( ${ tokenizeForSearch ( options . ocr ! ) . join ( ' ' ) } ) ` ) ,
2025-10-27 22:09:55 +08:00
)
2025-07-14 10:13:06 -04:00
. $if ( ! ! options . type , ( qb ) = > qb . where ( 'asset.type' , '=' , options . type ! ) )
. $if ( options . isFavorite !== undefined , ( qb ) = > qb . where ( 'asset.isFavorite' , '=' , options . isFavorite ! ) )
. $if ( options . isOffline !== undefined , ( qb ) = > qb . where ( 'asset.isOffline' , '=' , options . isOffline ! ) )
2025-04-18 23:39:56 +02:00
. $if ( options . isEncoded !== undefined , ( qb ) = >
2026-03-12 15:15:21 -05:00
qb . where ( ( eb ) = > {
const exists = eb . exists ( ( eb ) = >
eb
. selectFrom ( 'asset_file' )
. whereRef ( 'assetId' , '=' , 'asset.id' )
. where ( 'type' , '=' , AssetFileType . EncodedVideo ) ,
) ;
return options . isEncoded ? exists : eb.not ( exists ) ;
} ) ,
2025-04-18 23:39:56 +02:00
)
. $if ( options . isMotion !== undefined , ( qb ) = >
2025-07-14 10:13:06 -04:00
qb . where ( 'asset.livePhotoVideoId' , options . isMotion ? 'is not' : 'is' , null ) ,
2025-04-18 23:39:56 +02:00
)
2025-06-09 11:11:43 -04:00
. $if ( ! ! options . isNotInAlbum && ( ! options . albumIds || options . albumIds . length === 0 ) , ( qb ) = >
2025-11-04 16:03:21 -05:00
qb . where ( ( eb ) = > eb . not ( eb . exists ( ( eb ) = > eb . selectFrom ( 'album_asset' ) . whereRef ( 'assetId' , '=' , 'asset.id' ) ) ) ) ,
2025-04-18 23:39:56 +02:00
)
2026-03-03 06:41:29 -05:00
. $if ( options . withStacked === false , ( qb ) = > qb . where ( 'asset.stackId' , 'is' , null ) )
2025-04-18 23:39:56 +02:00
. $if ( ! ! options . withExif , withExifInner )
2026-01-06 21:54:12 +08:00
. $if ( ! ! ( options . withFaces || options . withPeople ) , ( qb ) = > qb . select ( withFacesAndPeople ) )
2025-07-14 10:13:06 -04:00
. $if ( ! options . withDeleted , ( qb ) = > qb . where ( 'asset.deletedAt' , 'is' , null ) ) ;
2025-04-18 23:39:56 +02:00
}
2025-04-29 19:23:01 +01:00
2025-05-20 09:36:43 -04:00
export type ReindexVectorIndexOptions = { indexName : string ; lists? : number } ;
2025-04-29 19:23:01 +01:00
2025-05-20 09:36:43 -04:00
type VectorIndexQueryOptions = { table : string ; vectorExtension : VectorExtension } & ReindexVectorIndexOptions ;
export function vectorIndexQuery ( { vectorExtension , table , indexName , lists } : VectorIndexQueryOptions ) : string {
2025-04-29 19:23:01 +01:00
switch ( vectorExtension ) {
2025-07-15 14:50:13 -04:00
case DatabaseExtension . VectorChord : {
2025-05-20 09:36:43 -04:00
return `
CREATE INDEX IF NOT EXISTS $ { indexName } ON $ { table } USING vchordrq ( embedding vector_cosine_ops ) WITH ( options = $ $
residual_quantization = false
[ build . internal ]
lists = [ $ { lists ? ? 1 } ]
spherical_centroids = true
build_threads = 4
sampling_factor = 1024
$ $ ) ` ;
}
2025-07-15 14:50:13 -04:00
case DatabaseExtension . Vector : {
2025-04-29 19:23:01 +01:00
return `
CREATE INDEX IF NOT EXISTS $ { indexName } ON $ { table }
USING hnsw ( embedding vector_cosine_ops )
WITH ( ef_construction = 300 , m = 16 ) ` ;
}
default : {
throw new Error ( ` Unsupported vector extension: ' ${ vectorExtension } ' ` ) ;
}
}
}
2025-12-17 09:23:13 -06:00
export const updateLockedColumns = < T extends Record < string , unknown > & { lockedProperties ? : LockableProperty [ ] } > (
exif : T ,
) = > {
exif . lockedProperties = lockableProperties . filter ( ( property ) = > property in exif ) ;
return exif ;
} ;