2024-10-02 10:54:35 -04:00
import { Injectable } from '@nestjs/common' ;
2024-05-20 20:31:36 -04:00
import semver from 'semver' ;
2025-02-11 17:15:56 -05:00
import { EXTENSION_NAMES } from 'src/constants' ;
2024-09-30 10:35:11 -04:00
import { OnEvent } from 'src/decorators' ;
2025-02-11 17:15:56 -05:00
import { BootstrapEventPriority , DatabaseExtension , DatabaseLock , VectorIndex } from 'src/enum' ;
2024-10-02 10:54:35 -04:00
import { BaseService } from 'src/services/base.service' ;
2025-02-11 17:15:56 -05:00
import { VectorExtension } from 'src/types' ;
2024-05-20 20:31:36 -04:00
2025-05-20 09:36:43 -04:00
type CreateFailedArgs = { name : string ; extension : string ; otherExtensions : string [ ] } ;
2024-05-20 20:31:36 -04:00
type UpdateFailedArgs = { name : string ; extension : string ; availableVersion : string } ;
type RestartRequiredArgs = { name : string ; availableVersion : string } ;
type NightlyVersionArgs = { name : string ; extension : string ; version : string } ;
type OutOfRangeArgs = { name : string ; extension : string ; version : string ; range : string } ;
2024-08-05 21:00:25 -04:00
type InvalidDowngradeArgs = { name : string ; extension : string ; installedVersion : string ; availableVersion : string } ;
2024-05-20 20:31:36 -04:00
const messages = {
2024-08-05 21:00:25 -04:00
notInstalled : ( name : string ) = >
` The ${ name } extension is not available in this Postgres instance.
If using a container image , ensure the image has the extension installed . ` ,
2024-05-20 20:31:36 -04:00
nightlyVersion : ( { name , extension , version } : NightlyVersionArgs ) = > `
2024-08-05 21:00:25 -04:00
The $ { name } extension version is $ { version } , which means it is a nightly release .
Please run 'DROP EXTENSION IF EXISTS ${extension}' and switch to a release version .
See https : //immich.app/docs/guides/database-queries for how to query the database.`,
outOfRange : ( { name , version , range } : OutOfRangeArgs ) = >
` The ${ name } extension version is ${ version } , but Immich only supports ${ range } .
Please change $ { name } to a compatible version in the Postgres instance . ` ,
2025-05-20 09:36:43 -04:00
createFailed : ( { name , extension , otherExtensions } : CreateFailedArgs ) = >
2024-08-05 21:00:25 -04:00
` Failed to activate ${ name } extension.
Please ensure the Postgres instance has $ { name } installed .
If the Postgres instance already has $ { name } installed , Immich may not have the necessary permissions to activate it .
2025-05-20 09:36:43 -04:00
In this case , please run 'CREATE EXTENSION IF NOT EXISTS ${extension} CASCADE' manually as a superuser .
2024-08-05 21:00:25 -04:00
See https : //immich.app/docs/guides/database-queries for how to query the database.
2025-05-20 09:36:43 -04:00
Alternatively , if your Postgres instance has any of $ { otherExtensions . join ( ', ' ) } , you may use one of them instead by setting the environment variable 'DB_VECTOR_EXTENSION=<extension name>' . ` ,
2024-08-05 21:00:25 -04:00
updateFailed : ( { name , extension , availableVersion } : UpdateFailedArgs ) = >
` The ${ name } extension can be updated to ${ availableVersion } .
Immich attempted to update the extension , but failed to do so .
This may be because Immich does not have the necessary permissions to update the extension .
Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser .
See https : //immich.app/docs/guides/database-queries for how to query the database.`,
restartRequired : ( { name , availableVersion } : RestartRequiredArgs ) = >
` The ${ name } extension has been updated to ${ availableVersion } .
Please restart the Postgres instance to complete the update . ` ,
invalidDowngrade : ( { name , installedVersion , availableVersion } : InvalidDowngradeArgs ) = >
` The database currently has ${ name } ${ installedVersion } activated, but the Postgres instance only has ${ availableVersion } available.
This most likely means the extension was downgraded .
If $ { name } $ { installedVersion } is compatible with Immich , please ensure the Postgres instance has this available . ` ,
2024-05-20 20:31:36 -04:00
} ;
2023-12-21 11:06:26 -05:00
@Injectable ( )
2024-10-02 10:54:35 -04:00
export class DatabaseService extends BaseService {
2024-11-05 16:30:56 +00:00
@OnEvent ( { name : 'app.bootstrap' , priority : BootstrapEventPriority.DatabaseService } )
2024-08-15 16:12:41 -04:00
async onBootstrap() {
2024-05-20 20:31:36 -04:00
const version = await this . databaseRepository . getPostgresVersion ( ) ;
const current = semver . coerce ( version ) ;
2024-08-05 21:00:25 -04:00
const postgresRange = this . databaseRepository . getPostgresVersionRange ( ) ;
if ( ! current || ! semver . satisfies ( current , postgresRange ) ) {
2024-05-20 20:31:36 -04:00
throw new Error (
2024-08-05 21:00:25 -04:00
` Invalid PostgreSQL version. Found ${ version } , but needed ${ postgresRange } . Please use a supported version. ` ,
2024-05-20 20:31:36 -04:00
) ;
}
2024-02-06 21:46:38 -05:00
await this . databaseRepository . withLock ( DatabaseLock . Migrations , async ( ) = > {
2025-05-20 09:36:43 -04:00
const extension = await this . databaseRepository . getVectorExtension ( ) ;
2024-05-20 20:31:36 -04:00
const name = EXTENSION_NAMES [ extension ] ;
2024-08-05 21:00:25 -04:00
const extensionRange = this . databaseRepository . getExtensionVersionRange ( extension ) ;
2024-05-20 20:31:36 -04:00
2024-08-05 21:00:25 -04:00
const { availableVersion , installedVersion } = await this . databaseRepository . getExtensionVersion ( extension ) ;
if ( ! availableVersion ) {
throw new Error ( messages . notInstalled ( name ) ) ;
2024-05-20 20:31:36 -04:00
}
2024-08-05 21:00:25 -04:00
if ( [ availableVersion , installedVersion ] . some ( ( version ) = > version && semver . eq ( version , '0.0.0' ) ) ) {
throw new Error ( messages . nightlyVersion ( { name , extension , version : '0.0.0' } ) ) ;
2024-05-20 20:31:36 -04:00
}
2024-08-05 21:00:25 -04:00
if ( ! semver . satisfies ( availableVersion , extensionRange ) ) {
throw new Error ( messages . outOfRange ( { name , extension , version : availableVersion , range : extensionRange } ) ) ;
2024-05-20 20:31:36 -04:00
}
2024-08-05 21:00:25 -04:00
if ( ! installedVersion ) {
await this . createExtension ( extension ) ;
2024-05-20 20:31:36 -04:00
}
2024-08-05 21:00:25 -04:00
if ( installedVersion && semver . gt ( availableVersion , installedVersion ) ) {
await this . updateExtension ( extension , availableVersion ) ;
} else if ( installedVersion && ! semver . satisfies ( installedVersion , extensionRange ) ) {
throw new Error ( messages . outOfRange ( { name , extension , version : installedVersion , range : extensionRange } ) ) ;
} else if ( installedVersion && semver . lt ( availableVersion , installedVersion ) ) {
throw new Error ( messages . invalidDowngrade ( { name , extension , availableVersion , installedVersion } ) ) ;
2024-05-20 20:31:36 -04:00
}
2023-12-21 11:06:26 -05:00
2025-05-20 09:36:43 -04:00
try {
await this . databaseRepository . reindexVectorsIfNeeded ( [ VectorIndex . CLIP , VectorIndex . FACE ] ) ;
} catch ( error ) {
this . logger . warn (
'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance. If you are upgrading directly from a version below 1.107.2, please upgrade to 1.107.2 first.' ,
) ;
throw error ;
}
2023-12-21 11:06:26 -05:00
2024-09-27 10:28:56 -04:00
const { database } = this . configRepository . getEnv ( ) ;
if ( ! database . skipMigrations ) {
2024-04-24 22:52:38 -04:00
await this . databaseRepository . runMigrations ( ) ;
}
2025-05-20 09:36:43 -04:00
await Promise . all ( [
this . databaseRepository . prewarm ( VectorIndex . CLIP ) ,
this . databaseRepository . prewarm ( VectorIndex . FACE ) ,
] ) ;
2024-02-06 21:46:38 -05:00
} ) ;
}
2024-08-05 21:00:25 -04:00
private async createExtension ( extension : DatabaseExtension ) {
try {
await this . databaseRepository . createExtension ( extension ) ;
} catch ( error ) {
2025-05-20 09:36:43 -04:00
const otherExtensions = [
DatabaseExtension . VECTOR ,
DatabaseExtension . VECTORS ,
DatabaseExtension . VECTORCHORD ,
] . filter ( ( ext ) = > ext !== extension ) ;
2024-08-05 21:00:25 -04:00
const name = EXTENSION_NAMES [ extension ] ;
2025-05-20 09:36:43 -04:00
this . logger . fatal ( messages . createFailed ( { name , extension , otherExtensions } ) ) ;
2024-08-05 21:00:25 -04:00
throw error ;
}
}
private async updateExtension ( extension : VectorExtension , availableVersion : string ) {
this . logger . log ( ` Updating ${ EXTENSION_NAMES [ extension ] } extension to ${ availableVersion } ` ) ;
try {
const { restartRequired } = await this . databaseRepository . updateVectorExtension ( extension , availableVersion ) ;
if ( restartRequired ) {
this . logger . warn ( messages . restartRequired ( { name : EXTENSION_NAMES [ extension ] , availableVersion } ) ) ;
}
} catch ( error ) {
this . logger . warn ( messages . updateFailed ( { name : EXTENSION_NAMES [ extension ] , extension , availableVersion } ) ) ;
throw error ;
}
}
2023-12-21 11:06:26 -05:00
}