Files
fn_registry/dev/issues/0071g-extract-app-db-init.md

4.8 KiB

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0071g Extraer `app_db_init` a cpp/functions/core/ (sub-issue de 0071, Tier 4) pendiente feature
registry-quality
registry-only media
2026-05-10 2026-05-17

Contexto

Sub-issue derivado de 0071. Primitiva Tier 4 — sirve a TODOS los paneles de cualquier app que use SQLite. Bajo riesgo, alta reusabilidad.

Cada app C++ del registry repite el mismo patron al abrir su SQLite local:

  1. Resolver path: <exe_dir>/local_files/<app>.db via fn::local_path().
  2. Abrir con sqlite3_open_v2(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE).
  3. PRAGMA journal_mode=WAL.
  4. PRAGMA foreign_keys=ON.
  5. PRAGMA busy_timeout=5000.
  6. Aplicar migrations desde embed.FS o array de strings (migrations/001_*.sql, 002_*.sql, ...).
  7. Catch "duplicate column" / "already exists" para idempotencia.
  8. Verificar version con tabla _migrations o assumir orden archivos.

Sitios actuales con esta logica embebida (~300 LoC duplicadas):

  • projects/osint_graph/apps/graph_explorer/project_manager.cpp:37-100 (graph_explorer.db)
  • projects/osint_graph/apps/graph_explorer/jobs.cpp (jobs persistence)
  • projects/osint_graph/apps/graph_explorer/layout_store.cpp (layout positions)
  • projects/osint_graph/apps/graph_explorer/node_groups.cpp (groups)
  • apps/kanban/db.go::ensureColumns (Go, fuera de scope C++ pero patron equivalente)
  • Proxima navegator_dashboard/local_api.cpp (history, tabs)

5+ duplicaciones C++ confirmadas. Rule of three superada con creces.

Objetivo

Funcion app_db_init en cpp/functions/core/ que recibe un path + lista de migraciones SQL y devuelve un sqlite3* listo, con todos los pragmas + migraciones aplicadas.

API propuesta

namespace fn_core {

struct AppDbConfig {
    std::string path;                          // path absoluto al .db
    std::vector<std::pair<std::string, std::string>> migrations;
    // ^ pares (nombre_archivo, sql). Aplicados en orden de aparicion.
    //   Ej: {"001_init.sql", "CREATE TABLE ..."}, {"002_add_x.sql", "ALTER ..."}
    bool wal = true;
    bool foreign_keys = true;
    int busy_timeout_ms = 5000;
};

struct AppDbResult {
    sqlite3* conn;             // null si error
    std::string error;         // descripcion del error si conn==null
    int migrations_applied;    // cuantas se ejecutaron sin error de duplicado
    int migrations_skipped;    // cuantas eran idempotentes ya aplicadas
};

AppDbResult app_db_init(const AppDbConfig& cfg);

// Helper para apps que tienen migrations en disco (no embebidas):
std::vector<std::pair<std::string, std::string>>
  app_db_load_migrations_from_dir(const std::string& dir_path);

}  // namespace fn_core

Behavior

  • Crea el directorio del path si no existe.
  • Aplica cada migracion en transaccion individual.
  • Captura errores de tipo "duplicate column" / "table already exists" → no fail, suma a skipped.
  • Cualquier otro error de SQL → cierra conn, devuelve null + mensaje.
  • Logging via fn_log::log_debug con nombre del archivo aplicado.

Migracion

  1. Crear cpp/functions/core/app_db_init.{h,cpp,md} + tests.
  2. Tests en primitives_gallery:
    • DB nueva con 1 migracion → tabla creada, 1 applied.
    • DB existente con la misma migracion → 0 applied, 1 skipped.
    • DB con 2 migraciones, segunda falla por SQL invalido → conn null, error claro.
    • DB con migracion que añade columna existente → catch "duplicate column", continua.
  3. Migrar consumidores uno a uno:
    • graph_explorer: project_manager.cpp primero (mas codigo).
    • graph_explorer: jobs, layout_store, node_groups.
    • navegator_dashboard cuando empiece a tocar SQLite serio.
  4. Cada migracion en commit propio.

Definicion de hecho

  • app_db_init.{h,cpp,md} registrado.
  • Tests pasan (>= 4 casos).
  • 4 consumidores migrados en graph_explorer.
  • LoC eliminadas: ~300 (los 4 sitios reducidos a llamadas de ~5 LoC).
  • app.md de cada app actualizado.

Riesgos

  • Bajo. La operacion es read+exec SQL. No hay concurrencia compleja. Falla detectable inmediato.
  • Riesgo unico: si la app espera un schema custom no via migrations (ej. CREATE TABLE IF NOT EXISTS inline), hay que mover ese SQL a un string migrations/001_init.sql antes.

Anti-patrones

  • Auto-detectar formato (YAML/JSON) de migrations — recibir vector<pair<name, sql>> directo. KISS.
  • Versioning con tabla _migrations — para apps pequeñas basta el archivo numerado + idempotencia. Si una app crece, ella misma migra al patron tabla.
  • Embedded embed.FS baked-in — la funcion recibe pares ya cargados; cada app decide si los embebe con #embed (C++23) o los carga del filesystem.
  • Wrapper RAII sobre sqlite3* — fuera de scope. Devuelve raw pointer, la app lo maneja con su propia clase.