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 @@
|
|||||||
|
-- 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";
|
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