mirror of
https://github.com/immich-app/immich.git
synced 2025-06-06 21:38:26 +02:00
fix(server): drop old extension (#18400)
This commit is contained in:
parent
98e998e814
commit
a02fe89ec9
7 changed files with 211 additions and 80 deletions
|
@ -77,10 +77,11 @@ The easiest option is to have both extensions installed during the migration:
|
|||
3. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed
|
||||
4. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;` using psql or your choice of database client
|
||||
5. Start Immich and wait for the logs `Reindexed face_index` and `Reindexed clip_index` to be output
|
||||
6. Remove the `vectors.so` entry from the `shared_preload_libraries` setting
|
||||
7. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate)
|
||||
6. If Immich does not have superuser permissions, run the SQL command `DROP EXTENSION vectors;`
|
||||
7. Remove the `vectors.so` entry from the `shared_preload_libraries` setting
|
||||
8. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate)
|
||||
|
||||
If it is not possible to have both VectorChord and pgvector.s installed at the same time, you can perform the migration with more manual steps:
|
||||
If it is not possible to have both VectorChord and pgvecto.rs installed at the same time, you can perform the migration with more manual steps:
|
||||
|
||||
1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- DatabaseRepository.getExtensionVersion
|
||||
-- DatabaseRepository.getExtensionVersions
|
||||
SELECT
|
||||
name,
|
||||
default_version as "availableVersion",
|
||||
installed_version as "installedVersion"
|
||||
FROM
|
||||
pg_available_extensions
|
||||
WHERE
|
||||
name = $1
|
||||
name in ($1)
|
||||
|
||||
-- DatabaseRepository.getPostgresVersion
|
||||
SHOW server_version
|
||||
|
|
|
@ -77,14 +77,14 @@ export class DatabaseRepository {
|
|||
return getVectorExtension(this.db);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DatabaseExtension.VECTORS] })
|
||||
async getExtensionVersion(extension: DatabaseExtension): Promise<ExtensionVersion> {
|
||||
@GenerateSql({ params: [[DatabaseExtension.VECTORS]] })
|
||||
async getExtensionVersions(extensions: readonly DatabaseExtension[]): Promise<ExtensionVersion[]> {
|
||||
const { rows } = await sql<ExtensionVersion>`
|
||||
SELECT default_version as "availableVersion", installed_version as "installedVersion"
|
||||
SELECT name, default_version as "availableVersion", installed_version as "installedVersion"
|
||||
FROM pg_available_extensions
|
||||
WHERE name = ${extension}
|
||||
WHERE name in (${sql.join(extensions)})
|
||||
`.execute(this.db);
|
||||
return rows[0] ?? { availableVersion: null, installedVersion: null };
|
||||
return rows;
|
||||
}
|
||||
|
||||
getExtensionVersionRange(extension: VectorExtension): string {
|
||||
|
@ -115,6 +115,7 @@ export class DatabaseRepository {
|
|||
}
|
||||
|
||||
async createExtension(extension: DatabaseExtension): Promise<void> {
|
||||
this.logger.log(`Creating ${EXTENSION_NAMES[extension]} extension`);
|
||||
await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)} CASCADE`.execute(this.db);
|
||||
if (extension === DatabaseExtension.VECTORCHORD) {
|
||||
const dbName = sql.table(await this.getDatabaseName());
|
||||
|
@ -125,8 +126,13 @@ export class DatabaseRepository {
|
|||
}
|
||||
}
|
||||
|
||||
async dropExtension(extension: DatabaseExtension): Promise<void> {
|
||||
this.logger.log(`Dropping ${EXTENSION_NAMES[extension]} extension`);
|
||||
await sql`DROP EXTENSION IF EXISTS ${sql.raw(extension)}`.execute(this.db);
|
||||
}
|
||||
|
||||
async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise<VectorUpdateResult> {
|
||||
const { availableVersion, installedVersion } = await this.getExtensionVersion(extension);
|
||||
const [{ availableVersion, installedVersion }] = await this.getExtensionVersions([extension]);
|
||||
if (!installedVersion) {
|
||||
throw new Error(`${EXTENSION_NAMES[extension]} extension is not installed`);
|
||||
}
|
||||
|
|
|
@ -19,16 +19,20 @@ describe(DatabaseService.name, () => {
|
|||
({ sut, mocks } = newTestService(DatabaseService));
|
||||
|
||||
extensionRange = '0.2.x';
|
||||
mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VECTORCHORD);
|
||||
mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange);
|
||||
|
||||
versionBelowRange = '0.1.0';
|
||||
minVersionInRange = '0.2.0';
|
||||
updateInRange = '0.2.1';
|
||||
versionAboveRange = '0.3.0';
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: minVersionInRange,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: DatabaseExtension.VECTORCHORD,
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
|
@ -50,6 +54,13 @@ describe(DatabaseService.name, () => {
|
|||
{ extension: DatabaseExtension.VECTORCHORD, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORCHORD] },
|
||||
])('should work with $extensionName', ({ extension, extensionName }) => {
|
||||
beforeEach(() => {
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
installedVersion: minVersionInRange,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
mocks.database.getVectorExtension.mockResolvedValue(extension);
|
||||
mocks.config.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
|
@ -71,23 +82,26 @@ describe(DatabaseService.name, () => {
|
|||
|
||||
it(`should start up successfully with ${extension}`, async () => {
|
||||
mocks.database.getPostgresVersion.mockResolvedValue('14.0.0');
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
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.getExtensionVersions).toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should throw an error if the ${extension} extension is not installed`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null });
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([]);
|
||||
const message = `The ${extensionName} extension is not available in this Postgres instance.
|
||||
If using a container image, ensure the image has the extension installed.`;
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(message);
|
||||
|
@ -97,10 +111,13 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: versionBelowRange,
|
||||
availableVersion: versionBelowRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
installedVersion: versionBelowRange,
|
||||
availableVersion: versionBelowRange,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`,
|
||||
|
@ -110,7 +127,13 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should throw an error if ${extension} extension version is a nightly`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' });
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
installedVersion: '0.0.0',
|
||||
availableVersion: '0.0.0',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`,
|
||||
|
@ -122,26 +145,32 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should do in-range update for ${extension} extension`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(mocks.database.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange);
|
||||
expect(mocks.database.updateVectorExtension).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.getExtensionVersion).toHaveBeenCalled();
|
||||
expect(mocks.database.getExtensionVersions).toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should not upgrade ${extension} if same version`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
|
@ -151,10 +180,13 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should throw error if ${extension} available version is below range`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: versionBelowRange,
|
||||
installedVersion: null,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
availableVersion: versionBelowRange,
|
||||
installedVersion: null,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||
|
||||
|
@ -165,10 +197,13 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should throw error if ${extension} available version is above range`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: versionAboveRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
availableVersion: versionAboveRange,
|
||||
installedVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow();
|
||||
|
||||
|
@ -179,10 +214,13 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it('should throw error if available version is below installed version', async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: updateInRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: updateInRange,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`,
|
||||
|
@ -194,10 +232,13 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it('should throw error if installed version is not in version range', async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: versionAboveRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
availableVersion: minVersionInRange,
|
||||
installedVersion: versionAboveRange,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow(
|
||||
`The ${extensionName} extension version is ${versionAboveRange}, but Immich only supports`,
|
||||
|
@ -209,10 +250,13 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should raise error if ${extension} extension upgrade failed`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
mocks.database.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension');
|
||||
|
@ -226,10 +270,13 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should warn if ${extension} extension update requires restart`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
});
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: extension,
|
||||
availableVersion: updateInRange,
|
||||
installedVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: true });
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
@ -294,22 +341,79 @@ describe(DatabaseService.name, () => {
|
|||
});
|
||||
|
||||
it(`should throw error if extension could not be created`, async () => {
|
||||
mocks.database.getExtensionVersion.mockResolvedValue({
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
});
|
||||
mocks.database.updateVectorExtension.mockResolvedValue({ restartRequired: false });
|
||||
mocks.database.createExtension.mockRejectedValue(new Error('Failed to create extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension');
|
||||
|
||||
expect(mocks.logger.fatal).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.fatal.mock.calls[0][0]).toContain(
|
||||
`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>'`,
|
||||
);
|
||||
expect(mocks.logger.fatal.mock.calls[0][0]).toContain('CREATE EXTENSION IF NOT EXISTS vchord CASCADE');
|
||||
expect(mocks.database.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.database.updateVectorExtension).not.toHaveBeenCalled();
|
||||
expect(mocks.database.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`should drop unused extension`, async () => {
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: DatabaseExtension.VECTORS,
|
||||
installedVersion: minVersionInRange,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
{
|
||||
name: DatabaseExtension.VECTORCHORD,
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORCHORD);
|
||||
expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORS);
|
||||
});
|
||||
|
||||
it(`should warn if unused extension could not be dropped`, async () => {
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: DatabaseExtension.VECTORS,
|
||||
installedVersion: minVersionInRange,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
{
|
||||
name: DatabaseExtension.VECTORCHORD,
|
||||
installedVersion: null,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
mocks.database.dropExtension.mockRejectedValue(new Error('Failed to drop extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORCHORD);
|
||||
expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORS);
|
||||
expect(mocks.logger.warn).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.logger.warn.mock.calls[0][0]).toContain('DROP EXTENSION vectors');
|
||||
});
|
||||
|
||||
it(`should not try to drop pgvector when using vectorchord`, async () => {
|
||||
mocks.database.getExtensionVersions.mockResolvedValue([
|
||||
{
|
||||
name: DatabaseExtension.VECTOR,
|
||||
installedVersion: minVersionInRange,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
{
|
||||
name: DatabaseExtension.VECTORCHORD,
|
||||
installedVersion: minVersionInRange,
|
||||
availableVersion: minVersionInRange,
|
||||
},
|
||||
]);
|
||||
mocks.database.dropExtension.mockRejectedValue(new Error('Failed to drop extension'));
|
||||
|
||||
await expect(sut.onBootstrap()).resolves.toBeUndefined();
|
||||
|
||||
expect(mocks.database.dropExtension).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import semver from 'semver';
|
||||
import { EXTENSION_NAMES } from 'src/constants';
|
||||
import { EXTENSION_NAMES, VECTOR_EXTENSIONS } from 'src/constants';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { BootstrapEventPriority, DatabaseExtension, DatabaseLock, VectorIndex } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { VectorExtension } from 'src/types';
|
||||
|
||||
type CreateFailedArgs = { name: string; extension: string; otherExtensions: string[] };
|
||||
type CreateFailedArgs = { name: string; extension: string };
|
||||
type UpdateFailedArgs = { name: string; extension: string; availableVersion: string };
|
||||
type DropFailedArgs = { name: string; extension: 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 };
|
||||
|
@ -25,15 +26,13 @@ const messages = {
|
|||
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.`,
|
||||
createFailed: ({ name, extension, otherExtensions }: CreateFailedArgs) =>
|
||||
createFailed: ({ name, extension }: CreateFailedArgs) =>
|
||||
`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.
|
||||
In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension} CASCADE' manually as a superuser.
|
||||
See https://immich.app/docs/guides/database-queries for how to query the database.
|
||||
|
||||
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>'.`,
|
||||
See https://immich.app/docs/guides/database-queries for how to query the database.`,
|
||||
updateFailed: ({ name, extension, availableVersion }: UpdateFailedArgs) =>
|
||||
`The ${name} extension can be updated to ${availableVersion}.
|
||||
Immich attempted to update the extension, but failed to do so.
|
||||
|
@ -41,6 +40,12 @@ const messages = {
|
|||
|
||||
Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser.
|
||||
See https://immich.app/docs/guides/database-queries for how to query the database.`,
|
||||
dropFailed: ({ name, extension }: DropFailedArgs) =>
|
||||
`The ${name} extension is no longer needed, but could not be dropped.
|
||||
This may be because Immich does not have the necessary permissions to drop the extension.
|
||||
|
||||
Please run 'DROP EXTENSION ${extension};' 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.`,
|
||||
|
@ -68,7 +73,8 @@ export class DatabaseService extends BaseService {
|
|||
const name = EXTENSION_NAMES[extension];
|
||||
const extensionRange = this.databaseRepository.getExtensionVersionRange(extension);
|
||||
|
||||
const { availableVersion, installedVersion } = await this.databaseRepository.getExtensionVersion(extension);
|
||||
const extensionVersions = await this.databaseRepository.getExtensionVersions(VECTOR_EXTENSIONS);
|
||||
const { installedVersion, availableVersion } = extensionVersions.find((v) => v.name === extension) ?? {};
|
||||
if (!availableVersion) {
|
||||
throw new Error(messages.notInstalled(name));
|
||||
}
|
||||
|
@ -102,6 +108,13 @@ export class DatabaseService extends BaseService {
|
|||
throw error;
|
||||
}
|
||||
|
||||
for (const { name: dbName, installedVersion } of extensionVersions) {
|
||||
const isDepended = dbName === DatabaseExtension.VECTOR && extension === DatabaseExtension.VECTORCHORD;
|
||||
if (dbName !== extension && installedVersion && !isDepended) {
|
||||
await this.dropExtension(dbName);
|
||||
}
|
||||
}
|
||||
|
||||
const { database } = this.configRepository.getEnv();
|
||||
if (!database.skipMigrations) {
|
||||
await this.databaseRepository.runMigrations();
|
||||
|
@ -117,13 +130,8 @@ export class DatabaseService extends BaseService {
|
|||
try {
|
||||
await this.databaseRepository.createExtension(extension);
|
||||
} catch (error) {
|
||||
const otherExtensions = [
|
||||
DatabaseExtension.VECTOR,
|
||||
DatabaseExtension.VECTORS,
|
||||
DatabaseExtension.VECTORCHORD,
|
||||
].filter((ext) => ext !== extension);
|
||||
const name = EXTENSION_NAMES[extension];
|
||||
this.logger.fatal(messages.createFailed({ name, extension, otherExtensions }));
|
||||
this.logger.fatal(messages.createFailed({ name, extension }));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -140,4 +148,13 @@ export class DatabaseService extends BaseService {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async dropExtension(extension: DatabaseExtension) {
|
||||
try {
|
||||
await this.databaseRepository.dropExtension(extension);
|
||||
} catch (error) {
|
||||
const name = EXTENSION_NAMES[extension];
|
||||
this.logger.warn(messages.dropFailed({ name, extension }), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -383,6 +383,7 @@ export type DatabaseConnectionParts = {
|
|||
export type DatabaseConnectionParams = DatabaseConnectionURL | DatabaseConnectionParts;
|
||||
|
||||
export interface ExtensionVersion {
|
||||
name: VectorExtension;
|
||||
availableVersion: string | null;
|
||||
installedVersion: string | null;
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import { Mocked, vitest } from 'vitest';
|
|||
export const newDatabaseRepositoryMock = (): Mocked<RepositoryInterface<DatabaseRepository>> => {
|
||||
return {
|
||||
shutdown: vitest.fn(),
|
||||
getExtensionVersion: vitest.fn(),
|
||||
getExtensionVersions: vitest.fn(),
|
||||
getVectorExtension: vitest.fn(),
|
||||
getExtensionVersionRange: vitest.fn(),
|
||||
getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'),
|
||||
getPostgresVersionRange: vitest.fn().mockReturnValue('>=14.0.0'),
|
||||
createExtension: vitest.fn().mockResolvedValue(void 0),
|
||||
dropExtension: vitest.fn(),
|
||||
updateVectorExtension: vitest.fn(),
|
||||
reindexVectorsIfNeeded: vitest.fn(),
|
||||
getDimensionSize: vitest.fn(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue