Files
fn_registry/.claude/rules/db_migrations.md
T
egutierrez cd50e790ca docs(rules): db migrations obligatorias retroactivas y siempre
- 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>
2026-05-09 00:44:29 +02:00

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):

  1. Crear nueva columna/tabla con la forma deseada (migration N).
  2. App escribe en ambas (migration N+1, codigo).
  3. Backfill de datos viejos (migration N+2, script).
  4. App lee solo de la nueva (migration N+3, codigo).
  5. 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 .db para "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.