- db_migrations.md (nuevo): doctrina archivos numerados, aditivo, idempotente, embed.FS pattern, branch-by-abstraction para destructivos, anti-patrones, inventario retroactivo del ecosistema - INDEX: entrada 25 - CLAUDE.md: nota en cabecera Aplicado retroactivamente en commit paralelo: kanban (003-005), deploy_server (001-002), agents_and_robots/memory (001). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.9 KiB
Migraciones de BBDD: nunca perder datos
Regla absoluta: todo cambio de schema en SQLite (apps con kanban.db, operations.db propia, registry.db, etc.) DEBE ir en un archivo de migración versionado. Nunca borrar/recrear tablas, nunca cambiar tipos sin proceso seguro, nunca confiar en "borra el .db y vuelve a empezar".
Por que
- Las apps almacenan datos vivos (cards, entities, executions, assertions, columns, sessions).
- Borrar = perder horas/dias/semanas de trabajo del usuario.
- Lo que es trivial en dev (
rm operations.db) es destructivo en produccion (deploys + sync entre PCs). - Sync entre PCs (
fn sync,/full-git-pull) trae bases de datos de otros equipos: si tu schema asume tabla recreada, los datos del otro PC desaparecen.
Patrones obligatorios
1. Archivos numerados en migrations/
Cada cambio de schema = un archivo nuevo migrations/NNN_<accion>.sql. Numeracion zero-padded de 3 digitos. Nombre descriptivo.
apps/<app>/migrations/
001_init.sql # CREATE TABLE inicial (no se modifica nunca)
002_add_stickers.sql # ALTER TABLE cards ADD COLUMN stickers
003_add_assignees.sql # ALTER TABLE cards ADD COLUMN assignee_id
004_create_lock_history.sql # CREATE TABLE card_lock_history
...
2. Solo operaciones aditivas seguras
| Operacion | Seguro | Notas |
|---|---|---|
CREATE TABLE IF NOT EXISTS |
si | idempotente |
CREATE INDEX IF NOT EXISTS |
si | idempotente |
ALTER TABLE ... ADD COLUMN |
si | aditivo, default obligatorio |
INSERT INTO ... ON CONFLICT IGNORE |
si | seed data idempotente |
DROP TABLE |
NO | destructivo |
DROP COLUMN |
NO | destructivo (SQLite < 3.35 ni siquiera lo soporta) |
ALTER TABLE ... RENAME COLUMN |
precaucion | rompe codigo viejo si rollback |
ALTER TABLE ... DROP/ALTER constraint |
NO sin backup | requiere recreate-and-copy |
Si necesitas cambiar tipo, eliminar columna, o cambiar PK: hacer migracion en pasos (Branch by Abstraction):
- Crear nueva columna/tabla con la forma deseada (migration N).
- App escribe en ambas (migration N+1, codigo).
- Backfill de datos viejos (migration N+2, script).
- App lee solo de la nueva (migration N+3, codigo).
- Eliminar la vieja (migration N+4, despues de tener backups verificados).
Cada paso = una rama TBD corta + commit + verificacion. Nunca un solo PR que rompa lectores.
3. Aplicacion idempotente al arrancar
La app aplica todas las migraciones en orden al iniciar. Patron canonico (Go):
//go:embed migrations/*.sql
var migrationsFS embed.FS
func applyMigrations(conn *sql.DB) error {
files, err := fs.Glob(migrationsFS, "migrations/*.sql")
if err != nil { return err }
sort.Strings(files)
for _, f := range files {
b, err := migrationsFS.ReadFile(f)
if err != nil { return err }
if _, err := conn.Exec(string(b)); err != nil {
// SQLite ALTER TABLE ADD COLUMN no es idempotente nativamente.
// Si ya existe, ignorar el error de "duplicate column".
if !strings.Contains(err.Error(), "duplicate column") &&
!strings.Contains(err.Error(), "already exists") {
return fmt.Errorf("%s: %w", f, err)
}
}
}
return nil
}
Alternativa: tabla _migrations con las versiones aplicadas (mas robusta para schemas grandes). Para apps pequeñas (kanban, operations.db), bastan los archivos numerados + IF NOT EXISTS / catch de "duplicate column".
4. Migracion + cambios en codigo en el mismo commit
Cuando añades una columna:
migrations/NNN_<accion>.sql(nueva)db.go(lee/escribe la columna)types.ts(frontend type)- Tests
Todo en el mismo commit/rama. Si solo mergeas la migracion pero no el codigo, otros PCs aplican la migracion al sync y luego el codigo viejo no la usa. OK. Si mergeas el codigo sin la migracion, la app peta al arrancar en otros PCs. Mal. Migracion antes que codigo en el orden de archivos (no de tiempo).
5. Tests sobre la migracion
Cada migracion debe tener test que:
- Arranca con DB vacia → aplica todas → verifica schema.
- Arranca con DB en estado N-1 (datos previos) → aplica migracion N → verifica que los datos se conservan.
Esto detecta migraciones destructivas antes de mergear.
Que NO hacer
| Anti-patron | Consecuencia |
|---|---|
Borrar *.db durante dev y commitear "schema actualizado" |
Otros PCs pierden datos al sync. |
Modificar 001_init.sql para añadir columnas |
Las DBs ya creadas no se actualizan. Datos divergentes. |
DROP TABLE x; CREATE TABLE x ... |
Borra todo lo que el usuario tenga. |
Usar ensureColumns sin archivo SQL paralelo |
El cambio de schema vive solo en codigo Go, no auditable, no migrable manualmente. |
| Cambiar tipo de columna in-place | SQLite necesita recreate-and-copy. Asume que pierde datos si no se hace bien. |
| "fn index" como solucion para regenerar registry.db | OK para registry.db (regenerable). NUNCA para operations.db, kanban.db, etc. |
Casos especiales
registry.db (raiz del fn_registry)
registry.db SE PUEDE regenerar con fn index desde los .go y .md. Para cambios de schema del registry: actualizar registry/migrations.go o el codigo de creacion + fn index. NO hace falta archivo de migracion porque la fuente de verdad son los .md/.go. Excepcion: tablas con datos vivos (proposals, pc_locations) — esas SI requieren migracion preservando datos.
operations.db (por app)
Cada app tiene su operations.db con entities/relations/executions. Schema definido en fn_operations/. Cambios al schema → archivo de migracion en fn_operations/migrations/ aplicado al abrir la BD. Idempotente.
apps con BD propia (kanban, etc.)
Mismo patron: apps/<app>/migrations/NNN_*.sql, embebido y aplicado al arrancar.
Comandos utiles
# Ver schema actual
sqlite3 apps/kanban/operations.db ".schema"
# Ver columnas de una tabla
sqlite3 apps/kanban/operations.db "PRAGMA table_info(cards);"
# Backup antes de migracion arriesgada
sqlite3 apps/kanban/operations.db ".backup apps/kanban/operations.db.bak.$(date +%Y%m%d)"
# Aplicar una migracion manual (si la app no esta corriendo)
sqlite3 apps/kanban/operations.db < apps/kanban/migrations/00X_<accion>.sql
# Listar archivos de migracion en orden
ls apps/kanban/migrations/*.sql | sort
Resumen
- Cada cambio de schema = archivo numerado nuevo en
migrations/. - Aditivo siempre que se pueda. Destructivo solo en pasos verificados con backup.
- App aplica migraciones al arrancar, idempotente.
- Migracion + codigo + tests en el mismo commit.
- Nunca borrar
.dbpara "arreglar" schema. Nunca modificar migraciones existentes.
Estado retroactivo (2026-05-09)
Inventario de BDs del ecosistema y conformidad con la regla:
| Repo / App | BD | migrations/ |
Estado |
|---|---|---|---|
registry/ |
registry.db |
si (11 archivos) | ✓ |
fn_operations/ |
operations.db por app |
si (4 archivos) | ✓ |
apps/kanban/ |
operations.db (kanban) |
si (5 archivos: 001 init, 002 stickers, 003 columns_extras, 004 cards_extras, 005 history_actor) | ✓ |
apps/deploy_server/ |
operations.db (deploys) |
si (2 archivos: 001 init, 002 target_extras) | ✓ |
apps/dag_engine/store/ |
DB del dag_engine | si (001_init) | ✓ |
projects/element_agents/.../shell/memory/ |
memoria del agente | si (001_init) | ✓ |
projects/osint_graph/apps/graph_explorer/ |
DBs C++ inline (project_manager, layout_store, jobs, node_groups) | NO | pendiente — refactor C++ multi-archivo, mover schema inline a migrations/*.sql aplicado al abrir cada DB. |
Las apps marcadas ✓ usan el patron canonico embed.FS + applyMigrations() (Go) o equivalente. La C++ pendiente requiere ronda dedicada — tracker via issue cuando se aborde.
apps/kanban/db.go::ensureColumns se mantiene como backstop idempotente para DBs muy antiguas creadas antes del refactor de migraciones. NO añadir columnas nuevas alli — siempre via archivo SQL.