54cee13e8e
- 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>
121 lines
6.3 KiB
Markdown
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.
|