chore: migrate migration scripts to sql tools (#26537)

This commit is contained in:
Daniel Dietzler 2026-02-26 17:59:52 +01:00 committed by GitHub
parent dd9046508d
commit e25ec4ec17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 20 additions and 225 deletions

18
pnpm-lock.yaml generated
View File

@ -344,8 +344,8 @@ importers:
specifier: 2.0.0-rc13
version: 2.0.0-rc13
'@immich/sql-tools':
specifier: ^0.2.0
version: 0.2.0
specifier: ^0.3.2
version: 0.3.2
'@nestjs/bullmq':
specifier: ^11.0.1
version: 11.0.4(@nestjs/common@11.1.14(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.14)(bullmq@5.69.3)
@ -3017,8 +3017,9 @@ packages:
'@immich/justified-layout-wasm@0.4.3':
resolution: {integrity: sha512-fpcQ7zPhP3Cp1bEXhONVYSUeIANa2uzaQFGKufUZQo5FO7aFT77szTVChhlCy4XaVy5R4ZvgSkA/1TJmeORz7Q==}
'@immich/sql-tools@0.2.0':
resolution: {integrity: sha512-AH0GRIUYrckNKuid5uO33vgRbGaznhRtArdQ91K310A1oUFjaoNzOaZyZhXwEmft3WYeC1bx4fdgUeois2QH5A==}
'@immich/sql-tools@0.3.2':
resolution: {integrity: sha512-UWhy/+Lf8C1dJip5wPfFytI3Vq/9UyDKQE1ROjXwVhT6E/CPgBkRLwHPetjYGPJ4o1JVVpRLnEEJCXdvzqVpGw==}
hasBin: true
'@immich/svelte-markdown-preprocess@0.2.1':
resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==}
@ -6058,6 +6059,10 @@ packages:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
commander@14.0.3:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@ -14855,8 +14860,9 @@ snapshots:
'@immich/justified-layout-wasm@0.4.3': {}
'@immich/sql-tools@0.2.0':
'@immich/sql-tools@0.3.2':
dependencies:
commander: 14.0.3
kysely: 0.28.11
kysely-postgres-js: 3.0.0(kysely@0.28.11)(postgres@3.4.8)
pg-connection-string: 2.11.0
@ -18224,6 +18230,8 @@ snapshots:
commander@13.1.0: {}
commander@14.0.3: {}
commander@2.20.3: {}
commander@4.1.1: {}

View File

@ -22,12 +22,12 @@
"test:cov": "vitest --config test/vitest.config.mjs --coverage",
"test:medium": "vitest --config test/vitest.config.medium.mjs",
"typeorm": "typeorm",
"migrations:debug": "node ./dist/bin/migrations.js debug",
"migrations:generate": "node ./dist/bin/migrations.js generate",
"migrations:create": "node ./dist/bin/migrations.js create",
"migrations:run": "node ./dist/bin/migrations.js run",
"migrations:revert": "node ./dist/bin/migrations.js revert",
"schema:drop": "node ./dist/bin/migrations.js query 'DROP schema public cascade; CREATE schema public;'",
"migrations:debug": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate --debug",
"migrations:generate": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate",
"migrations:create": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate",
"migrations:run": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations run",
"migrations:revert": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations revert",
"schema:drop": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} query 'DROP schema public cascade; CREATE schema public;'",
"schema:reset": "pnpm run schema:drop && pnpm run migrations:run",
"sync:open-api": "node ./dist/bin/sync-open-api.js",
"sync:sql": "node ./dist/bin/sync-sql.js",
@ -35,7 +35,7 @@
},
"dependencies": {
"@extism/extism": "2.0.0-rc13",
"@immich/sql-tools": "^0.2.0",
"@immich/sql-tools": "^0.3.2",
"@nestjs/bullmq": "^11.0.1",
"@nestjs/common": "^11.0.4",
"@nestjs/core": "^11.0.4",

View File

@ -1,213 +0,0 @@
#!/usr/bin/env node
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
import { schemaDiff, schemaFromCode, schemaFromDatabase } from '@immich/sql-tools';
import { Kysely, sql } from 'kysely';
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { basename, dirname, extname, join } from 'node:path';
import { ConfigRepository } from 'src/repositories/config.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import 'src/schema';
import { getKyselyConfig } from 'src/utils/database';
const main = async () => {
const command = process.argv[2];
const path = process.argv[3] || 'src/Migration';
switch (command) {
case 'debug': {
await debug();
return;
}
case 'run': {
await runMigrations();
return;
}
case 'revert': {
await revert();
return;
}
case 'query': {
const query = process.argv[3];
await runQuery(query);
return;
}
case 'create': {
create(path, [], []);
return;
}
case 'generate': {
await generate(path);
return;
}
default: {
console.log(`Usage:
node dist/bin/migrations.js create <name>
node dist/bin/migrations.js generate <name>
node dist/bin/migrations.js run
node dist/bin/migrations.js revert
`);
}
}
};
const getDatabaseClient = () => {
const configRepository = new ConfigRepository();
const { database } = configRepository.getEnv();
return new Kysely<any>(getKyselyConfig(database.config));
};
const runQuery = async (query: string) => {
const db = getDatabaseClient();
await sql.raw(query).execute(db);
await db.destroy();
};
const runMigrations = async () => {
const configRepository = new ConfigRepository();
const logger = LoggingRepository.create();
const db = getDatabaseClient();
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
await databaseRepository.runMigrations();
await db.destroy();
};
const revert = async () => {
const configRepository = new ConfigRepository();
const logger = LoggingRepository.create();
const db = getDatabaseClient();
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
try {
const migrationName = await databaseRepository.revertLastMigration();
if (!migrationName) {
console.log('No migrations to revert');
return;
}
markMigrationAsReverted(migrationName);
} finally {
await db.destroy();
}
};
const debug = async () => {
const { up } = await compare();
const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n');
// const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n');
writeFileSync('./migrations.sql', upSql + '\n\n');
console.log('Wrote migrations.sql');
};
const generate = async (path: string) => {
const { up, down } = await compare();
if (up.items.length === 0) {
console.log('No changes detected');
return;
}
create(path, up.asSql(), down.asSql());
};
const create = (path: string, up: string[], down: string[]) => {
const timestamp = Date.now();
const name = basename(path, extname(path));
const filename = `${timestamp}-${name}.ts`;
const folder = dirname(path);
const fullPath = join(folder, filename);
mkdirSync(folder, { recursive: true });
writeFileSync(fullPath, asMigration({ up, down }));
console.log(`Wrote ${fullPath}`);
};
const compare = async () => {
const configRepository = new ConfigRepository();
const { database } = configRepository.getEnv();
const source = schemaFromCode({ overrides: true, namingStrategy: 'default' });
const target = await schemaFromDatabase({ connection: database.config });
console.log(source.warnings.join('\n'));
const up = schemaDiff(source, target, {
tables: { ignoreExtra: true },
functions: { ignoreExtra: false },
parameters: { ignoreExtra: true },
});
const down = schemaDiff(target, source, {
tables: { ignoreExtra: false, ignoreMissing: true },
functions: { ignoreExtra: false },
extensions: { ignoreMissing: true },
parameters: { ignoreMissing: true },
});
return { up, down };
};
type MigrationProps = {
up: string[];
down: string[];
};
const asMigration = ({ up, down }: MigrationProps) => {
const upSql = up.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
const downSql = down.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
return `import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
${upSql}
}
export async function down(db: Kysely<any>): Promise<void> {
${downSql}
}
`;
};
const markMigrationAsReverted = (migrationName: string) => {
// eslint-disable-next-line unicorn/prefer-module
const distRoot = join(__dirname, '..');
const projectRoot = join(distRoot, '..');
const sourceFolder = join(projectRoot, 'src', 'schema', 'migrations');
const distFolder = join(distRoot, 'schema', 'migrations');
const sourcePath = join(sourceFolder, `${migrationName}.ts`);
const revertedFolder = join(sourceFolder, 'reverted');
const revertedPath = join(revertedFolder, `${migrationName}.ts`);
if (existsSync(revertedPath)) {
console.log(`Migration ${migrationName} is already marked as reverted`);
} else if (existsSync(sourcePath)) {
mkdirSync(revertedFolder, { recursive: true });
renameSync(sourcePath, revertedPath);
console.log(`Moved ${sourcePath} to ${revertedPath}`);
} else {
console.warn(`Source migration file not found for ${migrationName}`);
}
const distBase = join(distFolder, migrationName);
for (const extension of ['.js', '.js.map', '.d.ts']) {
const filePath = `${distBase}${extension}`;
if (existsSync(filePath)) {
rmSync(filePath, { force: true });
console.log(`Removed ${filePath}`);
}
}
};
main()
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error(error);
console.log('Something went wrong');
process.exit(1);
});