--- id: "0071g" title: "Extraer `app_db_init` a cpp/functions/core/ (sub-issue de 0071, Tier 4)" status: pendiente type: feature domain: - registry-quality scope: registry-only priority: media depends: [] blocks: [] related: [] created: 2026-05-10 updated: 2026-05-17 tags: [] --- ## 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: `/local_files/.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 ```cpp namespace fn_core { struct AppDbConfig { std::string path; // path absoluto al .db std::vector> 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> 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>` 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.