Su troceador de SQL no entiende los cuerpos `$$` de plpgsql, y este esquema tiene dos funciones —la que comprueba que el reparto de autoría suma cien y la que impide ciclos en el linaje de géneros—. A partir de esa migración deja de aplicar nada y termina con éxito. Se descubrió porque `cancion.nota` seguía existiendo dos migraciones después de borrarla. Es decir: durante un rato la base y el esquema dijeron cosas distintas y nada avisó. `scripts/db/migrar.mjs` trocea por el mismo separador que escribe Drizzle, aplica en orden, anota en su misma tabla y con su mismo hash, y cuando algo falla dice en qué sentencia y deshace. De paso: - 0004 rehace `cancion_principal` alrededor del `DROP COLUMN`. Un `CREATE VIEW ... SELECT *` expande las columnas al crearse, así que la vista impedía borrar la columna. - 0005 le da procedencia a `persona`. Las figuras de un género las escribe un modelo igual que las fichas, con fechas que hay que verificar; sin esto no se distingue a quien firma una canción del catálogo de una semblanza importada sin revisar. - `marked`, para componer en el servidor el Markdown que ahora vive en una columna. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>master
parent
be28a29693
commit
8272f3c195
@ -1 +1,18 @@
|
||||
ALTER TABLE "cancion" DROP COLUMN "nota";
|
||||
-- La columna `nota` sobraba: nació de leer al revés los archivos de canción.
|
||||
-- El cuerpo del Markdown NO es un comentario, es la letra; el campo `letra:`
|
||||
-- del frontmatter —que aparece en un solo archivo— son los créditos de quién
|
||||
-- la firma, y esos van a `cancion_persona`.
|
||||
|
||||
-- La vista se rehace alrededor del cambio.
|
||||
--
|
||||
-- `CREATE VIEW ... SELECT *` expande las columnas al crearse, así que la vista
|
||||
-- queda dependiendo de cada una: ni recoge las que se añadan después ni deja
|
||||
-- borrar las que hay. Lo peor de las dos opciones. Mientras siga siendo `*`,
|
||||
-- toda migración que toque las columnas de `cancion` tiene que quitarla y
|
||||
-- volver a ponerla, como aquí.
|
||||
DROP VIEW "cancion_principal";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "cancion" DROP COLUMN "nota";
|
||||
--> statement-breakpoint
|
||||
CREATE VIEW "cancion_principal" AS
|
||||
SELECT * FROM "cancion" WHERE "version_de_id" IS NULL;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "persona" ADD COLUMN "origen" text DEFAULT 'manual' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "persona" ADD COLUMN "modelo_origen" text;--> statement-breakpoint
|
||||
ALTER TABLE "persona" ADD COLUMN "importado_en" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "persona" ADD COLUMN "revisado_por" text;--> statement-breakpoint
|
||||
ALTER TABLE "persona" ADD COLUMN "revisado_en" timestamp with time zone;
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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();
|
||||
@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Ejecuta la migración en una transacción y la deshace.
|
||||
*
|
||||
* `drizzle-kit migrate` se queda colgado sin decir por qué, así que hay que
|
||||
* preguntarle directamente a Postgres.
|
||||
*
|
||||
* El progreso se escribe a disco de forma SÍNCRONA. La salida normal de Node
|
||||
* se acumula en un buffer cuando va a un pipe o a un archivo, y si el proceso
|
||||
* se mata por tiempo ese buffer se pierde entero: es justo lo que hacía que
|
||||
* pareciera que no llegaba a arrancar.
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import pg from 'pg';
|
||||
|
||||
const ARCHIVO = 'drizzle/0002_catalogo_en_la_base.sql';
|
||||
const DIARIO = '.diario-migracion.txt';
|
||||
const APLICAR = process.argv.includes('--aplicar');
|
||||
|
||||
fs.writeFileSync(DIARIO, '');
|
||||
const apuntar = (linea) => fs.appendFileSync(DIARIO, linea + '\n');
|
||||
|
||||
const sql = fs.readFileSync(ARCHIVO, 'utf8');
|
||||
const sentencias = sql
|
||||
.split('--> statement-breakpoint')
|
||||
.map((s) => s.trim())
|
||||
// Sin expresión regular. `(--[^\n]*\n?)+` tarda catorce segundos en un
|
||||
// comentario de quinientos caracteres: cuando no encaja, puede repartir el
|
||||
// mismo texto de exponencialmente muchas formas antes de rendirse. Línea a
|
||||
// línea es lineal, y además se lee.
|
||||
.filter((s) => s && s.split('\n').some((l) => l.trim() && !l.trim().startsWith('--')));
|
||||
|
||||
apuntar(`${sentencias.length} sentencias`);
|
||||
|
||||
const cliente = new pg.Client({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
// Que no se quede esperando eternamente si el servidor no contesta.
|
||||
connectionTimeoutMillis: 10_000,
|
||||
query_timeout: 30_000
|
||||
});
|
||||
|
||||
apuntar('conectando...');
|
||||
await cliente.connect();
|
||||
apuntar('conectado');
|
||||
|
||||
await cliente.query('BEGIN');
|
||||
// Que falle rápido en vez de colgarse esperando un bloqueo que no llega.
|
||||
await cliente.query("SET LOCAL lock_timeout = '5s'");
|
||||
await cliente.query("SET LOCAL statement_timeout = '30s'");
|
||||
apuntar('transacción abierta');
|
||||
|
||||
let hechas = 0;
|
||||
let rota = null;
|
||||
|
||||
for (const [i, sentencia] of sentencias.entries()) {
|
||||
const resumen = sentencia.split('\n').find((l) => l.trim() && !l.trim().startsWith('--')) ?? '';
|
||||
apuntar(`[${i + 1}/${sentencias.length}] ${resumen.trim().slice(0, 80)}`);
|
||||
try {
|
||||
await cliente.query(sentencia);
|
||||
hechas++;
|
||||
} catch (fallo) {
|
||||
rota = { i, sentencia, fallo };
|
||||
apuntar(`\n FALLA: ${fallo.message}`);
|
||||
if (fallo.hint) apuntar(` pista: ${fallo.hint}`);
|
||||
if (fallo.position) apuntar(` posición: ${fallo.position}`);
|
||||
apuntar('\n' + sentencia.slice(0, 600));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const completa = !rota && hechas === sentencias.length;
|
||||
await cliente.query(APLICAR && completa ? 'COMMIT' : 'ROLLBACK');
|
||||
apuntar(completa ? `\nLas ${hechas} sentencias pasan.` : `\nParó en la ${hechas + 1}.`);
|
||||
apuntar(APLICAR && completa ? 'APLICADA.' : '(deshecha)');
|
||||
await cliente.end();
|
||||
Loading…
Reference in new issue