2025-02-11 17:15:56 -05:00
import { EXTENSION_NAMES } from 'src/constants' ;
2025-05-20 09:36:43 -04:00
import { DatabaseExtension , VectorIndex } from 'src/enum' ;
2024-03-21 00:07:30 +01:00
import { DatabaseService } from 'src/services/database.service' ;
2025-02-11 17:15:56 -05:00
import { VectorExtension } from 'src/types' ;
2024-10-02 10:54:35 -04:00
import { mockEnvData } from 'test/repositories/config.repository.mock' ;
2025-02-10 18:47:42 -05:00
import { newTestService , ServiceMocks } from 'test/utils' ;
2023-12-21 11:06:26 -05:00
describe ( DatabaseService . name , ( ) = > {
let sut : DatabaseService ;
2025-02-10 18:47:42 -05:00
let mocks : ServiceMocks ;
2024-10-02 10:54:35 -04:00
2024-08-05 21:00:25 -04:00
let extensionRange : string ;
let versionBelowRange : string ;
let minVersionInRange : string ;
let updateInRange : string ;
let versionAboveRange : string ;
2023-12-21 11:06:26 -05:00
2024-03-05 23:23:06 +01:00
beforeEach ( ( ) = > {
2025-02-10 18:47:42 -05:00
( { sut , mocks } = newTestService ( DatabaseService ) ) ;
2024-05-20 20:31:36 -04:00
2024-08-05 21:00:25 -04:00
extensionRange = '0.2.x' ;
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersionRange . mockReturnValue ( extensionRange ) ;
2024-08-05 21:00:25 -04:00
versionBelowRange = '0.1.0' ;
minVersionInRange = '0.2.0' ;
updateInRange = '0.2.1' ;
versionAboveRange = '0.3.0' ;
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-08-05 21:00:25 -04:00
installedVersion : minVersionInRange ,
availableVersion : minVersionInRange ,
} ) ;
} ) ;
2023-12-21 11:06:26 -05:00
it ( 'should work' , ( ) = > {
expect ( sut ) . toBeDefined ( ) ;
} ) ;
2024-10-10 15:07:37 +02:00
describe ( 'onBootstrap' , ( ) = > {
it ( 'should throw an error if PostgreSQL version is below minimum supported version' , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getPostgresVersion . mockResolvedValueOnce ( '13.10.0' ) ;
2024-02-06 21:46:38 -05:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow ( 'Invalid PostgreSQL version. Found 13.10.0' ) ;
2024-02-06 21:46:38 -05:00
2025-02-10 18:47:42 -05:00
expect ( mocks . database . getPostgresVersion ) . toHaveBeenCalledTimes ( 1 ) ;
2024-08-05 21:00:25 -04:00
} ) ;
2024-01-07 00:24:09 +00:00
2024-10-10 15:07:37 +02:00
describe . each ( < Array < { extension : VectorExtension ; extensionName : string } > > [
{ extension : DatabaseExtension.VECTOR , extensionName : EXTENSION_NAMES [ DatabaseExtension . VECTOR ] } ,
{ extension : DatabaseExtension.VECTORS , extensionName : EXTENSION_NAMES [ DatabaseExtension . VECTORS ] } ,
2025-05-20 09:36:43 -04:00
{ extension : DatabaseExtension.VECTORCHORD , extensionName : EXTENSION_NAMES [ DatabaseExtension . VECTORCHORD ] } ,
2024-10-10 15:07:37 +02:00
] ) ( 'should work with $extensionName' , ( { extension , extensionName } ) = > {
beforeEach ( ( ) = > {
2025-05-20 09:36:43 -04:00
mocks . database . getVectorExtension . mockResolvedValue ( extension ) ;
2025-02-10 18:47:42 -05:00
mocks . config . getEnv . mockReturnValue (
2024-10-10 15:07:37 +02:00
mockEnvData ( {
database : {
2024-10-24 17:12:25 -04:00
config : {
2025-04-24 12:58:29 -04:00
connectionType : 'parts' ,
host : 'database' ,
port : 5432 ,
username : 'postgres' ,
password : 'postgres' ,
database : 'immich' ,
2024-10-24 17:12:25 -04:00
} ,
2024-10-10 15:07:37 +02:00
skipMigrations : false ,
vectorExtension : extension ,
} ,
} ) ,
) ;
2024-08-05 21:00:25 -04:00
} ) ;
2024-01-07 00:24:09 +00:00
2024-10-10 15:07:37 +02:00
it ( ` should start up successfully with ${ extension } ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getPostgresVersion . mockResolvedValue ( '14.0.0' ) ;
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
installedVersion : null ,
availableVersion : minVersionInRange ,
} ) ;
await expect ( sut . onBootstrap ( ) ) . resolves . toBeUndefined ( ) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . database . getPostgresVersion ) . toHaveBeenCalled ( ) ;
expect ( mocks . database . createExtension ) . toHaveBeenCalledWith ( extension ) ;
expect ( mocks . database . createExtension ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . database . getExtensionVersion ) . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-10-10 15:07:37 +02:00
} ) ;
2024-01-07 00:24:09 +00:00
2024-10-10 15:07:37 +02:00
it ( ` should throw an error if the ${ extension } extension is not installed ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( { installedVersion : null , availableVersion : null } ) ;
2024-10-10 15:07:37 +02:00
const message = ` The ${ extensionName } extension is not available in this Postgres instance.
2024-08-05 21:00:25 -04:00
If using a container image , ensure the image has the extension installed . ` ;
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow ( message ) ;
2024-01-07 00:24:09 +00:00
2025-02-10 18:47:42 -05:00
expect ( mocks . database . createExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
2024-10-10 15:07:37 +02:00
} ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
it ( ` should throw an error if the ${ extension } extension version is below minimum supported version ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
installedVersion : versionBelowRange ,
availableVersion : versionBelowRange ,
} ) ;
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow (
` The ${ extensionName } extension version is ${ versionBelowRange } , but Immich only supports ${ extensionRange } ` ,
) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
2024-08-05 21:00:25 -04:00
} ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
it ( ` should throw an error if ${ extension } extension version is a nightly ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( { installedVersion : '0.0.0' , availableVersion : '0.0.0' } ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow (
` The ${ extensionName } extension version is 0.0.0, which means it is a nightly release. ` ,
) ;
2023-12-21 11:06:26 -05:00
2025-02-10 18:47:42 -05:00
expect ( mocks . database . createExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . updateVectorExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
2024-10-10 15:07:37 +02:00
} ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
it ( ` should do in-range update for ${ extension } extension ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
availableVersion : updateInRange ,
installedVersion : minVersionInRange ,
} ) ;
2025-02-10 18:47:42 -05:00
mocks . database . updateVectorExtension . mockResolvedValue ( { restartRequired : false } ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . resolves . toBeUndefined ( ) ;
2023-12-21 11:06:26 -05:00
2025-02-10 18:47:42 -05:00
expect ( mocks . database . updateVectorExtension ) . toHaveBeenCalledWith ( extension , updateInRange ) ;
expect ( mocks . database . updateVectorExtension ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . database . getExtensionVersion ) . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-08-05 21:00:25 -04:00
} ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
it ( ` should not upgrade ${ extension } if same version ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
availableVersion : minVersionInRange ,
installedVersion : minVersionInRange ,
} ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . resolves . toBeUndefined ( ) ;
2023-12-21 11:06:26 -05:00
2025-02-10 18:47:42 -05:00
expect ( mocks . database . updateVectorExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-08-05 21:00:25 -04:00
} ) ;
2024-02-06 21:46:38 -05:00
2024-10-10 15:07:37 +02:00
it ( ` should throw error if ${ extension } available version is below range ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
availableVersion : versionBelowRange ,
installedVersion : null ,
} ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow ( ) ;
2023-12-21 11:06:26 -05:00
2025-02-10 18:47:42 -05:00
expect ( mocks . database . createExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . updateVectorExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-08-05 21:00:25 -04:00
} ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
it ( ` should throw error if ${ extension } available version is above range ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
availableVersion : versionAboveRange ,
installedVersion : minVersionInRange ,
} ) ;
2024-05-20 20:31:36 -04:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow ( ) ;
2024-05-20 20:31:36 -04:00
2025-02-10 18:47:42 -05:00
expect ( mocks . database . createExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . updateVectorExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-08-05 21:00:25 -04:00
} ) ;
2024-05-20 20:31:36 -04:00
2024-10-10 15:07:37 +02:00
it ( 'should throw error if available version is below installed version' , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
availableVersion : minVersionInRange ,
installedVersion : updateInRange ,
} ) ;
2024-05-20 20:31:36 -04:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow (
` The database currently has ${ extensionName } ${ updateInRange } activated, but the Postgres instance only has ${ minVersionInRange } available. ` ,
) ;
2024-05-20 20:31:36 -04:00
2025-02-10 18:47:42 -05:00
expect ( mocks . database . updateVectorExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-10-10 15:07:37 +02:00
} ) ;
it ( 'should throw error if installed version is not in version range' , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
availableVersion : minVersionInRange ,
installedVersion : versionAboveRange ,
} ) ;
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow (
` The ${ extensionName } extension version is ${ versionAboveRange } , but Immich only supports ` ,
) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . database . updateVectorExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-10-10 15:07:37 +02:00
} ) ;
it ( ` should raise error if ${ extension } extension upgrade failed ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
availableVersion : updateInRange ,
installedVersion : minVersionInRange ,
} ) ;
2025-02-10 18:47:42 -05:00
mocks . database . updateVectorExtension . mockRejectedValue ( new Error ( 'Failed to update extension' ) ) ;
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow ( 'Failed to update extension' ) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . logger . warn . mock . calls [ 0 ] [ 0 ] ) . toContain (
2024-10-10 15:07:37 +02:00
` The ${ extensionName } extension can be updated to ${ updateInRange } . ` ,
) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . updateVectorExtension ) . toHaveBeenCalledWith ( extension , updateInRange ) ;
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
2024-10-10 15:07:37 +02:00
} ) ;
it ( ` should warn if ${ extension } extension update requires restart ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
availableVersion : updateInRange ,
installedVersion : minVersionInRange ,
} ) ;
2025-02-10 18:47:42 -05:00
mocks . database . updateVectorExtension . mockResolvedValue ( { restartRequired : true } ) ;
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . resolves . toBeUndefined ( ) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . logger . warn ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . logger . warn . mock . calls [ 0 ] [ 0 ] ) . toContain ( extensionName ) ;
expect ( mocks . database . updateVectorExtension ) . toHaveBeenCalledWith ( extension , updateInRange ) ;
expect ( mocks . database . runMigrations ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-08-05 21:00:25 -04:00
} ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
it ( ` should reindex ${ extension } indices if needed ` , async ( ) = > {
await expect ( sut . onBootstrap ( ) ) . resolves . toBeUndefined ( ) ;
2025-05-20 09:36:43 -04:00
expect ( mocks . database . reindexVectorsIfNeeded ) . toHaveBeenCalledExactlyOnceWith ( [
VectorIndex . CLIP ,
VectorIndex . FACE ,
] ) ;
expect ( mocks . database . reindexVectorsIfNeeded ) . toHaveBeenCalledTimes ( 1 ) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . database . runMigrations ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
2024-10-10 15:07:37 +02:00
} ) ;
it ( ` should throw an error if reindexing fails ` , async ( ) = > {
2025-05-20 09:36:43 -04:00
mocks . database . reindexVectorsIfNeeded . mockRejectedValue ( new Error ( 'Error reindexing' ) ) ;
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toBeDefined ( ) ;
2025-05-20 09:36:43 -04:00
expect ( mocks . database . reindexVectorsIfNeeded ) . toHaveBeenCalledExactlyOnceWith ( [
VectorIndex . CLIP ,
VectorIndex . FACE ,
] ) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . logger . fatal ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . logger . warn ) . toHaveBeenCalledWith (
2024-10-10 15:07:37 +02:00
expect . stringContaining ( 'Could not run vector reindexing checks.' ) ,
) ;
} ) ;
} ) ;
it ( 'should skip migrations if DB_SKIP_MIGRATIONS=true' , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . config . getEnv . mockReturnValue (
2024-10-10 15:07:37 +02:00
mockEnvData ( {
database : {
2024-10-24 17:12:25 -04:00
config : {
2025-04-24 12:58:29 -04:00
connectionType : 'parts' ,
host : 'database' ,
port : 5432 ,
username : 'postgres' ,
password : 'postgres' ,
database : 'immich' ,
2024-10-24 17:12:25 -04:00
} ,
2024-10-10 15:07:37 +02:00
skipMigrations : true ,
vectorExtension : DatabaseExtension.VECTORS ,
} ,
} ) ,
2024-08-05 21:00:25 -04:00
) ;
2024-05-20 20:31:36 -04:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . resolves . toBeUndefined ( ) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
2024-08-05 21:00:25 -04:00
} ) ;
2024-05-20 20:31:36 -04:00
2025-05-20 09:36:43 -04:00
it ( ` should throw error if extension could not be created ` , async ( ) = > {
2025-02-10 18:47:42 -05:00
mocks . database . getExtensionVersion . mockResolvedValue ( {
2024-10-10 15:07:37 +02:00
installedVersion : null ,
availableVersion : minVersionInRange ,
2024-08-05 21:00:25 -04:00
} ) ;
2025-02-10 18:47:42 -05:00
mocks . database . updateVectorExtension . mockResolvedValue ( { restartRequired : false } ) ;
mocks . database . createExtension . mockRejectedValue ( new Error ( 'Failed to create extension' ) ) ;
2023-12-21 11:06:26 -05:00
2024-10-10 15:07:37 +02:00
await expect ( sut . onBootstrap ( ) ) . rejects . toThrow ( 'Failed to create extension' ) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . logger . fatal ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . logger . fatal . mock . calls [ 0 ] [ 0 ] ) . toContain (
2025-05-20 09:36:43 -04:00
` Alternatively, if your Postgres instance has any of vector, vectors, vchord, you may use one of them instead by setting the environment variable 'DB_VECTOR_EXTENSION=<extension name>' ` ,
2024-10-10 15:07:37 +02:00
) ;
2025-02-10 18:47:42 -05:00
expect ( mocks . database . createExtension ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( mocks . database . updateVectorExtension ) . not . toHaveBeenCalled ( ) ;
expect ( mocks . database . runMigrations ) . not . toHaveBeenCalled ( ) ;
2024-10-10 15:07:37 +02:00
} ) ;
} ) ;
2023-12-21 11:06:26 -05:00
} ) ;