From e25ec4ec17cb26833a9b063f41c8d112911a7f03 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:59:52 +0100 Subject: [PATCH] chore: migrate migration scripts to sql tools (#26537) --- pnpm-lock.yaml | 18 ++- server/package.json | 14 +-- server/src/bin/migrations.ts | 213 ----------------------------------- 3 files changed, 20 insertions(+), 225 deletions(-) delete mode 100644 server/src/bin/migrations.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7568e0436..c02d46c975 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/server/package.json b/server/package.json index 9b1acc91fb..5578f242ae 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts deleted file mode 100644 index bfa0f1733c..0000000000 --- a/server/src/bin/migrations.ts +++ /dev/null @@ -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 - node dist/bin/migrations.js generate - 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(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): Promise { -${upSql} -} - -export async function down(db: Kysely): Promise { -${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); - });