Files
egutierrez 474c2822bc feat(viz): graph_sources lector operations.db + streaming (issue 0049g)
- graph_load_from_operations: SQLite read-only, schema-detect (type_ref/type,
  from_entity/source, to_entity/target, name/type, weight, updated_at).
- 16-color indigo palette por hash FNV1a32 del nombre de tipo. user_data
  por nodo es FNV1a64(entity.id) — deterministico entre cargas.
- Label pool interno: metadata.name (JSON simple) > entities.name > id.
- graph_free libera nodes/edges/types/rel_types/labels/strdup'd names via
  arena_map (GraphData* -> arena).
- Streaming pull-based con tiebreak (updated_at, id) y crecimiento x2 de
  capacidad. Tipos nuevos descubiertos en stream se anaden a types.
- Tests: fixture in-memory (3 entity types, 2 rel types, 10 entities,
  15 relations) + smoke contra apps/script_navegador/operations.db.
- Issue movido a completed/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:12:31 +02:00

121 lines
6.3 KiB
Markdown

---
name: graph_sources
kind: function
lang: cpp
domain: viz
version: "1.0.0"
purity: impure
signature: "bool graph_load_from_operations(const char* db_path, GraphData* out, GraphLoadStats* stats)"
description: "Lectores de grafos para GraphData con firma uniforme GraphLoadFn. Primera implementacion: operations.db (entities + relations) con variante streaming pull-based"
tags: [graph, sources, sqlite, operations, streaming, loader, viz]
uses_functions: []
uses_types: ["GraphData_cpp_viz", "EntityType_cpp_viz", "RelationType_cpp_viz"]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: true
tests: ["test_graph_sources"]
test_file_path: "cpp/tests/test_graph_sources.cpp"
file_path: "cpp/functions/viz/graph_sources.cpp"
framework: "imgui"
params:
- name: db_path
desc: "Ruta a un fichero SQLite operations.db (schema con entities + relations). Solo se abre en modo READONLY"
- name: out
desc: "GraphData destino. La funcion aloca nodes/edges/types/rel_types y un string pool interno; el caller debe liberar con graph_free()"
- name: stats
desc: "GraphLoadStats opcional con conteos y un buffer error_msg de 256 bytes; si la carga falla, errors > 0 y error_msg describe el motivo (BD ausente, tabla faltante, columna desconocida)"
output: "true si la carga fue correcta. false con stats->errors > 0 y error_msg poblado en error. Tras un retorno true: out->nodes/edges apuntan a memoria interna; out->types/rel_types listan los tipos descubiertos con color por hash del nombre y SHAPE_CIRCLE/EDGE_SOLID por defecto. user_data por nodo es FNV1a64 del entity.id (deterministico). label_idx apunta a metadata.name si existe, else entity.name, else entity.id. Streaming: graph_stream_operations_open captura MAX(updated_at, id) actuales; graph_stream_pull devuelve cuantas filas append y crece capacity en x2 si hace falta"
---
# graph_sources
Lectores de grafos para `GraphData` (issue 0049g). Disenado como un set de funciones con la misma firma `GraphLoadFn` para que anadir un backend nuevo (JSON, JSONL, GraphML, Neo4j export, etc.) sea declarar otra funcion compatible — el resto del codigo (apps, viewport, force layout, renderer) no cambia.
## API
```cpp
typedef bool (*GraphLoadFn)(const char* uri, GraphData* out, GraphLoadStats* stats);
bool graph_load_from_operations(const char* db_path, GraphData* out, GraphLoadStats* stats);
void graph_free(GraphData* graph);
const char* graph_label(const GraphData* graph, uint32_t label_idx);
// Streaming pull-based (poll cada N ms desde el caller).
GraphStreamSource* graph_stream_operations_open(const char* db_path, int poll_ms);
int graph_stream_pull(GraphStreamSource*, GraphData*);
void graph_stream_close(GraphStreamSource*);
```
## Mapeo operations.db → GraphData
`operations.db` es la BD de cada app del registry. La funcion detecta el schema via `PRAGMA table_info`:
| Concepto | Columna preferida | Fallback |
|----------|-------------------|----------|
| Tipo de entidad | `entities.type_ref` | `entities.type` |
| Source / target de relacion | `relations.from_entity` / `relations.to_entity` | `relations.source` / `relations.target` |
| Tipo de relacion | `relations.type` | `relations.name` |
| Etiqueta de nodo | `metadata.name` (JSON) | `entities.name` o `entities.id` |
| Tiebreak streaming | `(updated_at, id)` | `id` solo |
Cada valor distinto de `type_ref` produce un `EntityType` con color tomado de un palette de 16 (FNV1a32 sobre el nombre → `palette[h & 0xF]`), `shape = SHAPE_CIRCLE`, `default_size = 6.0`, `icon_id = 0`. La app consumidora puede sobreescribir esa apariencia via `types.yaml` (issue 0049k).
`user_data` por nodo es `FNV1a64(entity.id)` — deterministico entre cargas y util como handle estable para joins con metadata externa.
## Errores
La funcion no usa excepciones. En error:
- Retorna `false`.
- `stats->errors >= 1`.
- `stats->error_msg` contiene un texto corto (`"open: ..."`, `"missing table: entities"`, `"entities: missing type_ref/type column"`, ...).
Relaciones con `from_entity` / `to_entity` que apunten a entities inexistentes se cuentan en `stats->errors` y se descartan — el grafo resultante es siempre consistente.
## Streaming
El streaming es pull-based: el caller decide la cadencia y llama `graph_stream_pull` cuando quiere chequear cambios. La fuente guarda `(MAX(updated_at), MAX(id))` al abrir y avanza el cursor con tiebreak `(updated_at, id)`:
```sql
WHERE (updated_at > ?) OR (updated_at = ? AND id > ?)
ORDER BY updated_at, id
```
`graph_stream_pull` crece la capacidad de `nodes`/`edges` en x2 si hace falta — el caller no necesita pre-allocar. Tipos nuevos descubiertos durante el stream se anaden al final de `types`. Operaciones inversas (deletes) no se propagan — para reset, el caller debe `graph_free` y recargar.
## Memoria
`graph_load_from_operations` aloca toda la memoria que cuelga del `GraphData` devuelto (incluido el string pool de labels y los nombres de tipos via `strdup`). El caller libera todo con `graph_free(graph)`. La asociacion `GraphData* → arena` se mantiene en un mapa estatico interno; `graph_label` y `graph_stream_pull` lo consultan para acceder al pool.
## Ejemplo
```cpp
GraphData g{};
graph::GraphLoadStats s{};
if (!graph::graph_load_from_operations("apps/script_navegador/operations.db", &g, &s)) {
fprintf(stderr, "load failed: %s\n", s.error_msg);
return 1;
}
printf("nodes=%d edges=%d types=%d rel_types=%d\n",
s.nodes_loaded, s.edges_loaded, s.types_discovered, s.rel_types_discovered);
// Render con graph_renderer + force layout via graph_force_layout.
// ...
// Watcher en otro hilo:
auto* src = graph::graph_stream_operations_open("apps/script_navegador/operations.db", 500);
while (running) {
sleep_ms(500);
int n = graph::graph_stream_pull(src, &g);
if (n > 0) printf("appended %d new rows\n", n);
}
graph::graph_stream_close(src);
graph::graph_free(&g);
```
## Notas
- **v1.0** (2026-04-29, issue 0049g): primera version. Lector sincrono + streaming poll-based. Tests con fixture in-memory: 10 entities + 15 relations, 3 entity types, 2 relation types. Determinismo del `user_data` y resolucion de aristas verificados.
- La firma `GraphLoadFn` esta diseniada para futuros backends. Anadir uno (ej. `graph_load_from_jsonl`) consiste en declarar una funcion con la misma firma; nada en el resto del pipeline (renderer, layout, viewport) cambia.