mirror of
https://github.com/immich-app/immich.git
synced 2025-12-27 01:11:42 +03:00
fix(server): run migrations after database checks (#5832)
* run migrations after checks * optional migrations * only run checks in server and e2e * re-add migrations for microservices * refactor * move e2e init * remove assert from migration * update providers * update microservices app service * fixed logging * refactored version check, added unit tests * more version tests * don't use mocks for sut * refactor tests * suggest image only if postgres is 14, 15 or 16 * review suggestions * fixed regexp escape * fix typing * update migration
This commit is contained in:
134
server/src/domain/database/database.service.spec.ts
Normal file
134
server/src/domain/database/database.service.spec.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { ImmichLogger } from '@app/infra/logger';
|
||||
import { newDatabaseRepositoryMock } from '@test';
|
||||
import { Version } from '../domain.constant';
|
||||
import { DatabaseExtension, IDatabaseRepository } from '../repositories';
|
||||
import { DatabaseService } from './database.service';
|
||||
|
||||
describe(DatabaseService.name, () => {
|
||||
let sut: DatabaseService;
|
||||
let databaseMock: jest.Mocked<IDatabaseRepository>;
|
||||
let fatalLog: jest.SpyInstance;
|
||||
|
||||
beforeEach(async () => {
|
||||
databaseMock = newDatabaseRepositoryMock();
|
||||
fatalLog = jest.spyOn(ImmichLogger.prototype, 'fatal');
|
||||
|
||||
sut = new DatabaseService(databaseMock);
|
||||
|
||||
sut.minVectorsVersion = new Version(0, 1, 1);
|
||||
sut.maxVectorsVersion = new Version(0, 1, 11);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fatalLog.mockRestore();
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should return if minimum supported vectors version is installed', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 1));
|
||||
|
||||
await sut.init();
|
||||
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(fatalLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return if maximum supported vectors version is installed', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 11));
|
||||
|
||||
await sut.init();
|
||||
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(fatalLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if vectors version is not installed even after createVectors', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValueOnce(null);
|
||||
|
||||
await expect(sut.init()).rejects.toThrow('Unexpected: The pgvecto.rs extension is not installed.');
|
||||
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if vectors version is below minimum supported version', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 0, 1));
|
||||
|
||||
await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0.1.11')/s);
|
||||
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if vectors version is above maximum supported version', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 1, 12));
|
||||
|
||||
await expect(sut.init()).rejects.toThrow(
|
||||
/('DROP EXTENSION IF EXISTS vectors').*('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s,
|
||||
);
|
||||
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if vectors version is a nightly', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValueOnce(new Version(0, 0, 0));
|
||||
|
||||
await expect(sut.init()).rejects.toThrow(
|
||||
/(nightly).*('DROP EXTENSION IF EXISTS vectors').*('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s,
|
||||
);
|
||||
|
||||
expect(databaseMock.getExtensionVersion).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error if vectors extension could not be created', async () => {
|
||||
databaseMock.createExtension.mockRejectedValueOnce(new Error('Failed to create extension'));
|
||||
|
||||
await expect(sut.init()).rejects.toThrow('Failed to create extension');
|
||||
|
||||
expect(fatalLog).toHaveBeenCalledTimes(1);
|
||||
expect(fatalLog.mock.calls[0][0]).toMatch(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.11').*(v1\.91\.0)/s);
|
||||
expect(databaseMock.createExtension).toHaveBeenCalledTimes(1);
|
||||
expect(databaseMock.runMigrations).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
for (const major of [14, 15, 16]) {
|
||||
it(`should suggest image with postgres ${major} if database is ${major}`, async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 1));
|
||||
databaseMock.getPostgresVersion.mockResolvedValueOnce(new Version(major, 0, 0));
|
||||
|
||||
await expect(sut.init()).rejects.toThrow(new RegExp(`tensorchord\/pgvecto-rs:pg${major}-v0\\.1\\.11`, 's'));
|
||||
});
|
||||
}
|
||||
|
||||
it('should not suggest image if postgres version is not in 14, 15 or 16', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 1));
|
||||
[13, 17].forEach((major) => databaseMock.getPostgresVersion.mockResolvedValueOnce(new Version(major, 0, 0)));
|
||||
|
||||
await expect(sut.init()).rejects.toThrow(/^(?:(?!tensorchord\/pgvecto-rs).)*$/s);
|
||||
await expect(sut.init()).rejects.toThrow(/^(?:(?!tensorchord\/pgvecto-rs).)*$/s);
|
||||
});
|
||||
|
||||
it('should set the image to the maximum supported version', async () => {
|
||||
databaseMock.getExtensionVersion.mockResolvedValue(new Version(0, 0, 1));
|
||||
|
||||
await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.11')/s);
|
||||
|
||||
sut.maxVectorsVersion = new Version(0, 1, 12);
|
||||
await expect(sut.init()).rejects.toThrow(/('tensorchord\/pgvecto-rs:pg14-v0\.1\.12')/s);
|
||||
});
|
||||
});
|
||||
});
|
||||
69
server/src/domain/database/database.service.ts
Normal file
69
server/src/domain/database/database.service.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { ImmichLogger } from '@app/infra/logger';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
import { Version } from '../domain.constant';
|
||||
import { DatabaseExtension, IDatabaseRepository } from '../repositories';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseService {
|
||||
private logger = new ImmichLogger(DatabaseService.name);
|
||||
minVectorsVersion = new Version(0, 1, 1);
|
||||
maxVectorsVersion = new Version(0, 1, 11);
|
||||
|
||||
constructor(@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository) {}
|
||||
|
||||
async init() {
|
||||
await this.createVectors();
|
||||
await this.assertVectors();
|
||||
await this.databaseRepository.runMigrations();
|
||||
}
|
||||
|
||||
private async assertVectors() {
|
||||
const version = await this.databaseRepository.getExtensionVersion(DatabaseExtension.VECTORS);
|
||||
if (version == null) {
|
||||
throw new Error('Unexpected: The pgvecto.rs extension is not installed.');
|
||||
}
|
||||
|
||||
const image = await this.getVectorsImage();
|
||||
const suggestion = image ? `, such as with the docker image '${image}'` : '';
|
||||
|
||||
if (version.isEqual(new Version(0, 0, 0))) {
|
||||
throw new Error(
|
||||
`The pgvecto.rs extension version is ${version}, which means it is a nightly release.` +
|
||||
`Please run 'DROP EXTENSION IF EXISTS vectors' and switch to a release version${suggestion}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (version.isNewerThan(this.maxVectorsVersion)) {
|
||||
throw new Error(`
|
||||
The pgvecto.rs extension version is ${version} instead of ${this.maxVectorsVersion}.
|
||||
Please run 'DROP EXTENSION IF EXISTS vectors' and switch to ${this.maxVectorsVersion}${suggestion}.`);
|
||||
}
|
||||
|
||||
if (version.isOlderThan(this.minVectorsVersion)) {
|
||||
throw new Error(`
|
||||
The pgvecto.rs extension version is ${version}, which is older than the minimum supported version ${this.minVectorsVersion}.
|
||||
Please upgrade to this version or later${suggestion}.`);
|
||||
}
|
||||
}
|
||||
|
||||
private async createVectors() {
|
||||
await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (err: QueryFailedError) => {
|
||||
const image = await this.getVectorsImage();
|
||||
this.logger.fatal(`
|
||||
Failed to create pgvecto.rs extension.
|
||||
If you have not updated your Postgres instance to a docker image that supports pgvecto.rs (such as '${image}'), please do so.
|
||||
See the v1.91.0 release notes for more info: https://github.com/immich-app/immich/releases/tag/v1.91.0'
|
||||
`);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
private async getVectorsImage() {
|
||||
const { major } = await this.databaseRepository.getPostgresVersion();
|
||||
if (![14, 15, 16].includes(major)) {
|
||||
return null;
|
||||
}
|
||||
return `tensorchord/pgvecto-rs:pg${major}-v${this.maxVectorsVersion}`;
|
||||
}
|
||||
}
|
||||
1
server/src/domain/database/index.ts
Normal file
1
server/src/domain/database/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './database.service';
|
||||
Reference in New Issue
Block a user