/** * Aplica las migraciones pendientes. * * npm run db:migrate # aplica lo que falte * npm run db:migrate -- --ver # solo dice qué falta * * ## Por qué no `drizzle-kit migrate` * * Porque miente. Su troceador de SQL no entiende los cuerpos `$$` de las * funciones en plpgsql —y este esquema tiene dos, la que comprueba que el * reparto de autoría suma cien y la que impide ciclos en el linaje de * géneros—, así que a partir de esa migración deja de aplicar nada **y termina * sin error**. Se descubrió porque una columna que se había borrado en una * migración seguía existiendo dos migraciones después. * * Esto trocea por el mismo separador que escribe Drizzle, aplica en orden, * anota en su misma tabla y con su mismo hash —el SHA-256 del archivo—, y si * algo falla dice **en qué sentencia** y deshace. */ import { createHash } from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import pg from 'pg'; const CARPETA = 'drizzle'; const SOLO_VER = process.argv.includes('--ver'); /** Trocea por el separador de Drizzle y descarta lo que solo son comentarios. */ function sentenciasDe(sql) { return ( sql .split('--> statement-breakpoint') .map((s) => s.trim()) // Línea a línea y sin expresión regular: `(--[^\n]*\n?)+` tarda catorce // segundos en un comentario de quinientos caracteres, porque cuando no // encaja puede repartir el mismo texto de exponencialmente muchas formas. .filter((s) => s && s.split('\n').some((l) => l.trim() && !l.trim().startsWith('--'))) ); } const archivos = fs .readdirSync(CARPETA) .filter((n) => n.endsWith('.sql')) .sort(); const c = new pg.Client({ connectionString: process.env.DATABASE_URL, query_timeout: 120_000 }); await c.connect(); await c.query('CREATE SCHEMA IF NOT EXISTS drizzle'); await c.query(`CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations ( id SERIAL PRIMARY KEY, hash text NOT NULL, created_at bigint)`); const { rows: hechas } = await c.query('SELECT hash FROM drizzle.__drizzle_migrations'); const yaEstan = new Set(hechas.map((f) => f.hash)); const pendientes = archivos .map((nombre) => { const sql = fs.readFileSync(path.join(CARPETA, nombre), 'utf8'); return { nombre, sql, hash: createHash('sha256').update(sql).digest('hex') }; }) .filter((m) => !yaEstan.has(m.hash)); if (pendientes.length === 0) { console.log(`Al día: ${archivos.length} migraciones, ninguna pendiente.`); await c.end(); process.exit(0); } console.log(`Pendientes ${pendientes.length} de ${archivos.length}:`); for (const m of pendientes) console.log(` ${m.nombre}`); if (SOLO_VER) { console.log('\n(solo mirando: no se ha tocado nada)'); await c.end(); process.exit(0); } for (const m of pendientes) { const sentencias = sentenciasDe(m.sql); process.stdout.write(`\n${m.nombre} · ${sentencias.length} sentencias `); await c.query('BEGIN'); try { for (const [i, sentencia] of sentencias.entries()) { try { await c.query(sentencia); } catch (fallo) { const primera = sentencia.split('\n').find((l) => l.trim() && !l.trim().startsWith('--')) ?? ''; console.log(`\n\n Falla la sentencia ${i + 1}:\n ${primera.trim().slice(0, 90)}`); console.log(` → ${fallo.message}`); throw fallo; } } await c.query('INSERT INTO drizzle.__drizzle_migrations (hash, created_at) VALUES ($1, $2)', [ m.hash, String(Date.now()) ]); await c.query('COMMIT'); console.log('· aplicada'); } catch { await c.query('ROLLBACK').catch(() => {}); console.log('\nSe deshizo. Las anteriores siguen aplicadas.'); await c.end(); process.exit(1); } } console.log('\nAl día.'); await c.end();