You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
senzapaura_es/scripts/db/migrar.mjs

112 lines
3.6 KiB

/**
* 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();

Powered by TurnKey Linux.