---
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.
