Files
codebase-memory-mcp/src/graph_buffer/graph_buffer.c
T
Martin Vogel 80680ea367 Cross-service communication discovery + RAM-first incremental indexing
AST-based detection of HTTP calls, async dispatch (Pub/Sub, Cloud Tasks,
Kafka, SQS, etc.), and config accesses via resolved qualified names.
Route nodes as cross-service rendezvous points with infra→handler matching.
Constant propagation for module-level string assignments. YAML infrastructure
URL extraction from Cloud Scheduler configs.

RAM-first incremental pipeline: load DB into graph buffer, purge changed
file nodes, extract directly into existing buffer (resolver sees all nodes),
dump back to disk. Zero edge gap on kubernetes/django/meilisearch/neovim.

- service_patterns.c: ~170 library patterns (90 HTTP, 50 async, 30 config)
- pass_route_nodes.c: Route node creation + infra URL matching
- extract_unified.c: string constant collection + string ref classification
- extract_calls.c: first_string_arg + keyword argument extraction
- pipeline_incremental.c: RAM-first load→purge→extract→resolve→dump
- graph_buffer.c: load_from_db, delete_by_file, foreach visitors
- C++ LSP crash fix: NULL guard in cbm_type_substitute
2026-03-28 13:59:42 +01:00

1393 lines
47 KiB
C

/*
* graph_buffer.c — In-memory graph buffer for pipeline indexing.
*
* Uses foundation hash tables for O(1) node lookup by QN and edge dedup.
* Uses dynamic arrays for ordered iteration and secondary indexes.
*
* Memory ownership: each node/edge is individually heap-allocated so that
* pointers stored in hash tables remain stable when the pointer-array grows.
* The buffer frees everything in cbm_gbuf_free().
*/
#include "graph_buffer/graph_buffer.h"
#include "store/store.h"
#include "sqlite_writer.h"
#include "foundation/hash_table.h"
#include "foundation/compat.h"
#include "foundation/log.h"
#include "foundation/dyn_array.h"
#include <sqlite3.h>
#include <stdatomic.h>
#include <stdint.h> // int64_t
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // strdup
#include <time.h>
/* ── Internal types ──────────────────────────────────────────────── */
/* Edge key for dedup hash table — composite key as string "srcID:tgtID:type" */
#define EDGE_KEY_BUF 128
/* Per-type or per-key edge list stored in hash tables as values */
typedef CBM_DYN_ARRAY(const cbm_gbuf_edge_t *) edge_ptr_array_t;
/* Per-label or per-name node list */
typedef CBM_DYN_ARRAY(const cbm_gbuf_node_t *) node_ptr_array_t;
struct cbm_gbuf {
char *project;
char *root_path;
int64_t next_id;
_Atomic int64_t *shared_ids; /* NULL = use next_id, non-NULL = atomic source */
/* Node storage: array of pointers to individually heap-allocated nodes.
* This ensures pointers stored in hash tables remain valid when the
* pointer array reallocs (only the pointer array moves, not the nodes). */
CBM_DYN_ARRAY(cbm_gbuf_node_t *) nodes;
/* Primary index: QN → cbm_gbuf_node_t* */
CBMHashTable *node_by_qn;
/* Primary index: "id" string → cbm_gbuf_node_t* */
CBMHashTable *node_by_id;
/* Secondary node indexes */
CBMHashTable *nodes_by_label; /* key: label, value: (node_ptr_array_t*) */
CBMHashTable *nodes_by_name; /* key: name, value: (node_ptr_array_t*) */
/* Edge storage: array of pointers to individually heap-allocated edges */
CBM_DYN_ARRAY(cbm_gbuf_edge_t *) edges;
/* Edge dedup index: "srcID:tgtID:type" → cbm_gbuf_edge_t* */
CBMHashTable *edge_by_key;
/* Edge secondary indexes: composite keys → edge_ptr_array_t */
CBMHashTable *edges_by_source_type; /* "srcID:type" → edge_ptr_array_t* */
CBMHashTable *edges_by_target_type; /* "tgtID:type" → edge_ptr_array_t* */
CBMHashTable *edges_by_type; /* "type" → edge_ptr_array_t* */
};
/* ── Helpers ─────────────────────────────────────────────────────── */
static char *heap_strdup(const char *s) {
// NOLINTNEXTLINE(misc-include-cleaner) — strdup provided by standard header
return s ? strdup(s) : strdup("{}");
}
static void make_id_key(char *buf, size_t bufsz, int64_t id) {
snprintf(buf, bufsz, "%lld", (long long)id);
}
static void make_edge_key(char *buf, size_t bufsz, int64_t src, int64_t tgt, const char *type) {
snprintf(buf, bufsz, "%lld:%lld:%s", (long long)src, (long long)tgt, type);
}
static void make_src_type_key(char *buf, size_t bufsz, int64_t src, const char *type) {
snprintf(buf, bufsz, "%lld:%s", (long long)src, type);
}
/* Get or create a node_ptr_array_t in a hash table */
static node_ptr_array_t *get_or_create_node_array(CBMHashTable *ht, const char *key) {
node_ptr_array_t *arr = cbm_ht_get(ht, key);
if (!arr) {
arr = calloc(1, sizeof(node_ptr_array_t));
cbm_ht_set(ht, strdup(key), arr);
}
return arr;
}
/* Get or create an edge_ptr_array_t in a hash table */
static edge_ptr_array_t *get_or_create_edge_array(CBMHashTable *ht, const char *key) {
edge_ptr_array_t *arr = cbm_ht_get(ht, key);
if (!arr) {
arr = calloc(1, sizeof(edge_ptr_array_t));
cbm_ht_set(ht, strdup(key), arr);
}
return arr;
}
/* Free a node_ptr_array_t (callback for hash table iteration) */
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
static void free_node_array(const char *key, void *value, void *ud) {
(void)ud;
node_ptr_array_t *arr = value;
if (arr) {
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_free(arr);
free(arr);
}
free((void *)key);
}
/* Free an edge_ptr_array_t (callback) */
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
static void free_edge_array(const char *key, void *value, void *ud) {
(void)ud;
edge_ptr_array_t *arr = value;
if (arr) {
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_free(arr);
free(arr);
}
free((void *)key);
}
/* Free keys only (for node_by_id, edge_by_key) */
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
static void free_key_only(const char *key, void *value, void *ud) {
(void)value;
(void)ud;
free((void *)key);
}
/* Free a single node's owned strings */
static void free_node_strings(cbm_gbuf_node_t *n) {
free(n->label);
free(n->name);
free(n->qualified_name);
free(n->file_path);
free(n->properties_json);
}
/* Free a single edge's owned strings */
static void free_edge_strings(cbm_gbuf_edge_t *e) {
free(e->type);
free(e->properties_json);
}
/* ── Lifecycle ──────────────────────────────────────────────────── */
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
cbm_gbuf_t *cbm_gbuf_new(const char *project, const char *root_path) {
cbm_gbuf_t *gb = calloc(1, sizeof(cbm_gbuf_t));
if (!gb) {
return NULL;
}
gb->project = strdup(project ? project : "");
gb->root_path = strdup(root_path ? root_path : "");
gb->next_id = 1;
gb->shared_ids = NULL;
gb->node_by_qn = cbm_ht_create(256);
gb->node_by_id = cbm_ht_create(256);
gb->nodes_by_label = cbm_ht_create(32);
gb->nodes_by_name = cbm_ht_create(256);
gb->edge_by_key = cbm_ht_create(512);
gb->edges_by_source_type = cbm_ht_create(256);
gb->edges_by_target_type = cbm_ht_create(256);
gb->edges_by_type = cbm_ht_create(32);
return gb;
}
cbm_gbuf_t *cbm_gbuf_new_shared_ids(const char *project, const char *root_path,
_Atomic int64_t *id_source) {
cbm_gbuf_t *gb = cbm_gbuf_new(project, root_path);
if (gb && id_source) {
gb->shared_ids = id_source;
}
return gb;
}
void cbm_gbuf_free(cbm_gbuf_t *gb) {
if (!gb) {
return;
}
/* Free each individually-allocated node */
for (int i = 0; i < gb->nodes.count; i++) {
cbm_gbuf_node_t *n = gb->nodes.items[i];
free_node_strings(n);
free(n);
}
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_free(&gb->nodes);
/* Free each individually-allocated edge */
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
free_edge_strings(e);
free(e);
}
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_free(&gb->edges);
/* Free hash tables — may be NULL if already released by dump_to_sqlite */
if (gb->node_by_qn) {
cbm_ht_free(gb->node_by_qn);
}
if (gb->node_by_id) {
cbm_ht_foreach(gb->node_by_id, free_key_only, NULL);
cbm_ht_free(gb->node_by_id);
}
if (gb->nodes_by_label) {
cbm_ht_foreach(gb->nodes_by_label, free_node_array, NULL);
cbm_ht_free(gb->nodes_by_label);
}
if (gb->nodes_by_name) {
cbm_ht_foreach(gb->nodes_by_name, free_node_array, NULL);
cbm_ht_free(gb->nodes_by_name);
}
if (gb->edge_by_key) {
cbm_ht_foreach(gb->edge_by_key, free_key_only, NULL);
cbm_ht_free(gb->edge_by_key);
}
if (gb->edges_by_source_type) {
cbm_ht_foreach(gb->edges_by_source_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_source_type);
}
if (gb->edges_by_target_type) {
cbm_ht_foreach(gb->edges_by_target_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_target_type);
}
if (gb->edges_by_type) {
cbm_ht_foreach(gb->edges_by_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_type);
}
free(gb->project);
free(gb->root_path);
free(gb);
}
/* ── ID accessors ────────────────────────────────────────────────── */
int64_t cbm_gbuf_next_id(const cbm_gbuf_t *gb) {
if (!gb) {
return 1;
}
if (gb->shared_ids) {
return atomic_load(gb->shared_ids);
}
return gb->next_id;
}
void cbm_gbuf_set_next_id(cbm_gbuf_t *gb, int64_t next_id) {
if (!gb) {
return;
}
gb->next_id = next_id;
}
/* ── Node operations ─────────────────────────────────────────────── */
int64_t cbm_gbuf_upsert_node(cbm_gbuf_t *gb, const char *label, const char *name,
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
const char *qualified_name, const char *file_path, int start_line,
int end_line, const char *properties_json) {
if (!gb || !qualified_name) {
return 0;
}
/* Check if node already exists */
cbm_gbuf_node_t *existing = cbm_ht_get(gb->node_by_qn, qualified_name);
if (existing) {
/* Update in-place. Strdup new values BEFORE freeing old ones,
* because callers may pass existing->label etc. as arguments. */
char *new_label = heap_strdup(label);
char *new_name = heap_strdup(name);
char *new_file = heap_strdup(file_path);
char *new_props = properties_json ? heap_strdup(properties_json) : NULL;
free(existing->label);
existing->label = new_label;
free(existing->name);
existing->name = new_name;
free(existing->file_path);
existing->file_path = new_file;
existing->start_line = start_line;
existing->end_line = end_line;
if (new_props) {
free(existing->properties_json);
existing->properties_json = new_props;
}
return existing->id;
}
/* Heap-allocate a new node (pointer stays stable across array growth) */
cbm_gbuf_node_t *node = calloc(1, sizeof(cbm_gbuf_node_t));
if (!node) {
return 0;
}
int64_t id;
if (gb->shared_ids) {
id = atomic_fetch_add_explicit(gb->shared_ids, 1, memory_order_relaxed);
} else {
id = gb->next_id++;
}
node->id = id;
node->project = gb->project;
node->label = heap_strdup(label);
node->name = heap_strdup(name);
node->qualified_name = heap_strdup(qualified_name);
node->file_path = heap_strdup(file_path);
node->start_line = start_line;
node->end_line = end_line;
node->properties_json = heap_strdup(properties_json);
/* Store pointer in array */
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(&gb->nodes, node);
/* Primary indexes — QN key is borrowed from the node's own string */
cbm_ht_set(gb->node_by_qn, node->qualified_name, node);
char id_buf[32];
make_id_key(id_buf, sizeof(id_buf), id);
/* Check if key exists — if so, reuse old heap key to avoid leak.
* cbm_ht_set replaces key pointer, leaking old strdup'd key. */
const char *existing_key = cbm_ht_get_key(gb->node_by_id, id_buf);
if (existing_key) {
cbm_ht_set(gb->node_by_id, existing_key, node);
} else {
cbm_ht_set(gb->node_by_id, strdup(id_buf), node);
}
/* Secondary indexes */
node_ptr_array_t *by_label = get_or_create_node_array(gb->nodes_by_label, label ? label : "");
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(by_label, (const cbm_gbuf_node_t *)node);
node_ptr_array_t *by_name = get_or_create_node_array(gb->nodes_by_name, name ? name : "");
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(by_name, (const cbm_gbuf_node_t *)node);
return id;
}
const cbm_gbuf_node_t *cbm_gbuf_find_by_qn(const cbm_gbuf_t *gb, const char *qn) {
if (!gb || !qn) {
return NULL;
}
return cbm_ht_get(gb->node_by_qn, qn);
}
const cbm_gbuf_node_t *cbm_gbuf_find_by_id(const cbm_gbuf_t *gb, int64_t id) {
if (!gb) {
return NULL;
}
char key[32];
make_id_key(key, sizeof(key), id);
return cbm_ht_get(gb->node_by_id, key);
}
int cbm_gbuf_find_by_label(const cbm_gbuf_t *gb, const char *label, const cbm_gbuf_node_t ***out,
int *count) {
if (!gb || !out || !count) {
return -1;
}
node_ptr_array_t *arr = cbm_ht_get(gb->nodes_by_label, label ? label : "");
if (arr && arr->count > 0) {
*out = arr->items;
*count = arr->count;
} else {
*out = NULL;
*count = 0;
}
return 0;
}
int cbm_gbuf_find_by_name(const cbm_gbuf_t *gb, const char *name, const cbm_gbuf_node_t ***out,
int *count) {
if (!gb || !out || !count) {
return -1;
}
node_ptr_array_t *arr = cbm_ht_get(gb->nodes_by_name, name ? name : "");
if (arr && arr->count > 0) {
*out = arr->items;
*count = arr->count;
} else {
*out = NULL;
*count = 0;
}
return 0;
}
int cbm_gbuf_node_count(const cbm_gbuf_t *gb) {
/* Use QN hash table count since it's authoritative (handles deletes) */
return gb ? (int)cbm_ht_count(gb->node_by_qn) : 0;
}
int cbm_gbuf_delete_by_label(cbm_gbuf_t *gb, const char *label) {
if (!gb || !label) {
return -1;
}
node_ptr_array_t *arr = cbm_ht_get(gb->nodes_by_label, label);
if (!arr || arr->count == 0) {
return 0;
}
/* Build hash set of deleted node IDs for O(1) lookup */
CBMHashTable *deleted_set = cbm_ht_create(arr->count);
for (int i = 0; i < arr->count; i++) {
const cbm_gbuf_node_t *n = arr->items[i];
char id_buf[32];
make_id_key(id_buf, sizeof(id_buf), n->id);
cbm_ht_set(deleted_set, strdup(id_buf), (void *)1);
/* Remove from primary indexes */
cbm_ht_delete(gb->node_by_qn, n->qualified_name);
/* Free the strdup'd key stored in node_by_id (get before delete) */
const char *stored_key = cbm_ht_get_key(gb->node_by_id, id_buf);
cbm_ht_delete(gb->node_by_id, id_buf);
free((void *)stored_key);
}
/* Clear the label array */
cbm_da_clear(arr);
/* Cascade-delete edges referencing deleted nodes.
* Use hash set for O(1) membership check instead of O(n) scan.
* Incrementally remove from secondary indexes to avoid full rebuild. */
int write_idx = 0;
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
char src_id[32];
char tgt_id[32];
make_id_key(src_id, sizeof(src_id), e->source_id);
make_id_key(tgt_id, sizeof(tgt_id), e->target_id);
if (cbm_ht_get(deleted_set, src_id) || cbm_ht_get(deleted_set, tgt_id)) {
/* Remove from edge dedup index (free the strdup'd key) */
char key[EDGE_KEY_BUF];
make_edge_key(key, sizeof(key), e->source_id, e->target_id, e->type);
const char *ekey = cbm_ht_get_key(gb->edge_by_key, key);
cbm_ht_delete(gb->edge_by_key, key);
free((void *)ekey);
/* Remove from secondary indexes incrementally */
make_src_type_key(key, sizeof(key), e->source_id, e->type);
edge_ptr_array_t *st = cbm_ht_get(gb->edges_by_source_type, key);
if (st) {
for (int j = 0; j < st->count; j++) {
if (st->items[j]->id == e->id) {
st->items[j] = st->items[--st->count];
break;
}
}
}
make_src_type_key(key, sizeof(key), e->target_id, e->type);
edge_ptr_array_t *tt = cbm_ht_get(gb->edges_by_target_type, key);
if (tt) {
for (int j = 0; j < tt->count; j++) {
if (tt->items[j]->id == e->id) {
tt->items[j] = tt->items[--tt->count];
break;
}
}
}
edge_ptr_array_t *bt = cbm_ht_get(gb->edges_by_type, e->type);
if (bt) {
for (int j = 0; j < bt->count; j++) {
if (bt->items[j]->id == e->id) {
bt->items[j] = bt->items[--bt->count];
break;
}
}
}
/* Free the edge */
free_edge_strings(e);
free(e);
} else {
gb->edges.items[write_idx++] = gb->edges.items[i];
}
}
gb->edges.count = write_idx;
cbm_ht_foreach(deleted_set, free_key_only, NULL);
cbm_ht_free(deleted_set);
return 0;
}
int cbm_gbuf_delete_by_file(cbm_gbuf_t *gb, const char *file_path) {
if (!gb || !file_path) {
return -1;
}
/* Collect IDs of nodes in this file */
CBMHashTable *deleted_set = cbm_ht_create(64);
int deleted_count = 0;
int scanned = 0;
for (int i = 0; i < gb->nodes.count; i++) {
cbm_gbuf_node_t *n = gb->nodes.items[i];
scanned++;
if (!n->file_path || strcmp(n->file_path, file_path) != 0) {
continue;
}
if (!n->qualified_name || !cbm_ht_get(gb->node_by_qn, n->qualified_name)) {
continue;
}
char id_buf[32];
make_id_key(id_buf, sizeof(id_buf), n->id);
cbm_ht_set(deleted_set, strdup(id_buf), (void *)1);
/* Remove from label index */
node_ptr_array_t *label_arr = cbm_ht_get(gb->nodes_by_label, n->label);
if (label_arr) {
for (int j = 0; j < label_arr->count; j++) {
if (label_arr->items[j]->id == n->id) {
label_arr->items[j] = label_arr->items[--label_arr->count];
break;
}
}
}
/* Remove from name index */
node_ptr_array_t *name_arr = cbm_ht_get(gb->nodes_by_name, n->name);
if (name_arr) {
for (int j = 0; j < name_arr->count; j++) {
if (name_arr->items[j]->id == n->id) {
name_arr->items[j] = name_arr->items[--name_arr->count];
break;
}
}
}
cbm_ht_delete(gb->node_by_qn, n->qualified_name);
const char *stored_key = cbm_ht_get_key(gb->node_by_id, id_buf);
cbm_ht_delete(gb->node_by_id, id_buf);
free((void *)stored_key);
/* NULL out QN so dump's liveness check (cbm_ht_get by QN) fails
* even if a new node with the same QN is inserted later via merge. */
free(n->qualified_name);
n->qualified_name = NULL;
deleted_count++;
}
if (deleted_count == 0) {
cbm_ht_free(deleted_set);
return 0;
}
/* Cascade-delete edges referencing deleted nodes */
int write_idx = 0;
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
char src_id[32];
char tgt_id[32];
make_id_key(src_id, sizeof(src_id), e->source_id);
make_id_key(tgt_id, sizeof(tgt_id), e->target_id);
if (cbm_ht_get(deleted_set, src_id) || cbm_ht_get(deleted_set, tgt_id)) {
char key[EDGE_KEY_BUF];
make_edge_key(key, sizeof(key), e->source_id, e->target_id, e->type);
const char *ekey = cbm_ht_get_key(gb->edge_by_key, key);
cbm_ht_delete(gb->edge_by_key, key);
free((void *)ekey);
make_src_type_key(key, sizeof(key), e->source_id, e->type);
edge_ptr_array_t *st = cbm_ht_get(gb->edges_by_source_type, key);
if (st) {
for (int j = 0; j < st->count; j++) {
if (st->items[j]->id == e->id) {
st->items[j] = st->items[--st->count];
break;
}
}
}
make_src_type_key(key, sizeof(key), e->target_id, e->type);
edge_ptr_array_t *tt = cbm_ht_get(gb->edges_by_target_type, key);
if (tt) {
for (int j = 0; j < tt->count; j++) {
if (tt->items[j]->id == e->id) {
tt->items[j] = tt->items[--tt->count];
break;
}
}
}
edge_ptr_array_t *bt = cbm_ht_get(gb->edges_by_type, e->type);
if (bt) {
for (int j = 0; j < bt->count; j++) {
if (bt->items[j]->id == e->id) {
bt->items[j] = bt->items[--bt->count];
break;
}
}
}
free_edge_strings(e);
free(e);
} else {
gb->edges.items[write_idx++] = gb->edges.items[i];
}
}
gb->edges.count = write_idx;
cbm_ht_foreach(deleted_set, free_key_only, NULL);
cbm_ht_free(deleted_set);
{
char s_buf[16];
char d_buf[16];
snprintf(s_buf, sizeof(s_buf), "%d", scanned);
snprintf(d_buf, sizeof(d_buf), "%d", deleted_count);
cbm_log_info("gbuf.delete_by_file", "file", file_path, "scanned", s_buf, "deleted", d_buf);
}
return deleted_count;
}
int cbm_gbuf_load_from_db(cbm_gbuf_t *gb, const char *db_path, const char *project) {
if (!gb || !db_path || !project) {
return -1;
}
cbm_store_t *store = cbm_store_open_path(db_path);
if (!store) {
return -1;
}
sqlite3 *db = cbm_store_get_db(store);
if (!db) {
cbm_store_close(store);
return -1;
}
/* First pass: find max node ID for mapping array */
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(db, "SELECT MAX(id) FROM nodes WHERE project = ?", -1, &stmt, NULL) !=
SQLITE_OK) {
cbm_store_close(store);
return -1;
}
sqlite3_bind_text(stmt, 1, project, -1, SQLITE_STATIC);
int64_t max_old_id = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
max_old_id = sqlite3_column_int64(stmt, 0);
}
sqlite3_finalize(stmt);
int64_t *old_to_new = calloc((size_t)(max_old_id + 1), sizeof(int64_t));
if (!old_to_new) {
cbm_store_close(store);
return -1;
}
/* Load all nodes */
if (sqlite3_prepare_v2(
db,
"SELECT id, label, name, qualified_name, file_path, start_line, end_line, properties "
"FROM nodes WHERE project = ? ORDER BY id",
-1, &stmt, NULL) != SQLITE_OK) {
free(old_to_new);
cbm_store_close(store);
return -1;
}
sqlite3_bind_text(stmt, 1, project, -1, SQLITE_STATIC);
while (sqlite3_step(stmt) == SQLITE_ROW) {
int64_t old_id = sqlite3_column_int64(stmt, 0);
const char *label = (const char *)sqlite3_column_text(stmt, 1);
const char *name = (const char *)sqlite3_column_text(stmt, 2);
const char *qn = (const char *)sqlite3_column_text(stmt, 3);
const char *fp = (const char *)sqlite3_column_text(stmt, 4);
int sl = sqlite3_column_int(stmt, 5);
int el = sqlite3_column_int(stmt, 6);
const char *props = (const char *)sqlite3_column_text(stmt, 7);
int64_t new_id = cbm_gbuf_upsert_node(gb, label, name, qn, fp, sl, el, props);
if (new_id > 0 && old_id <= max_old_id) {
old_to_new[old_id] = new_id;
}
}
sqlite3_finalize(stmt);
/* Load all edges, remap IDs */
if (sqlite3_prepare_v2(db,
"SELECT source_id, target_id, type, properties "
"FROM edges WHERE project = ?",
-1, &stmt, NULL) != SQLITE_OK) {
free(old_to_new);
cbm_store_close(store);
return -1;
}
sqlite3_bind_text(stmt, 1, project, -1, SQLITE_STATIC);
while (sqlite3_step(stmt) == SQLITE_ROW) {
int64_t old_src = sqlite3_column_int64(stmt, 0);
int64_t old_tgt = sqlite3_column_int64(stmt, 1);
const char *type = (const char *)sqlite3_column_text(stmt, 2);
const char *props = (const char *)sqlite3_column_text(stmt, 3);
int64_t new_src = (old_src <= max_old_id) ? old_to_new[old_src] : 0;
int64_t new_tgt = (old_tgt <= max_old_id) ? old_to_new[old_tgt] : 0;
if (new_src > 0 && new_tgt > 0) {
cbm_gbuf_insert_edge(gb, new_src, new_tgt, type, props);
}
}
sqlite3_finalize(stmt);
free(old_to_new);
cbm_store_close(store);
return 0;
}
void cbm_gbuf_foreach_node(const cbm_gbuf_t *gb, cbm_gbuf_node_visitor_fn fn, void *userdata) {
if (!gb || !fn) {
return;
}
for (int i = 0; i < gb->nodes.count; i++) {
const cbm_gbuf_node_t *n = gb->nodes.items[i];
if (n->qualified_name && cbm_ht_get(gb->node_by_qn, n->qualified_name)) {
fn(n, userdata);
}
}
}
void cbm_gbuf_foreach_edge(const cbm_gbuf_t *gb, cbm_gbuf_edge_visitor_fn fn, void *userdata) {
if (!gb || !fn) {
return;
}
for (int i = 0; i < gb->edges.count; i++) {
fn(gb->edges.items[i], userdata);
}
}
/* ── Edge operations ─────────────────────────────────────────────── */
int64_t cbm_gbuf_insert_edge(cbm_gbuf_t *gb, int64_t source_id, int64_t target_id, const char *type,
const char *properties_json) {
if (!gb || !type) {
return 0;
}
/* Check for dedup */
char key[EDGE_KEY_BUF];
make_edge_key(key, sizeof(key), source_id, target_id, type);
cbm_gbuf_edge_t *existing = cbm_ht_get(gb->edge_by_key, key);
if (existing) {
/* Merge properties (just replace for now) */
if (properties_json && strcmp(properties_json, "{}") != 0) {
free(existing->properties_json);
existing->properties_json = heap_strdup(properties_json);
}
return existing->id;
}
/* Heap-allocate a new edge (pointer stays stable) */
cbm_gbuf_edge_t *edge = calloc(1, sizeof(cbm_gbuf_edge_t));
if (!edge) {
return 0;
}
int64_t id;
if (gb->shared_ids) {
id = atomic_fetch_add_explicit(gb->shared_ids, 1, memory_order_relaxed);
} else {
id = gb->next_id++;
}
edge->id = id;
edge->project = gb->project;
edge->source_id = source_id;
edge->target_id = target_id;
edge->type = heap_strdup(type);
edge->properties_json = heap_strdup(properties_json);
/* Store pointer in array */
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(&gb->edges, edge);
/* Dedup index */
cbm_ht_set(gb->edge_by_key, strdup(key), edge);
/* Secondary indexes */
make_src_type_key(key, sizeof(key), source_id, type);
edge_ptr_array_t *st = get_or_create_edge_array(gb->edges_by_source_type, key);
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(st, (const cbm_gbuf_edge_t *)edge);
make_src_type_key(key, sizeof(key), target_id, type);
edge_ptr_array_t *tt = get_or_create_edge_array(gb->edges_by_target_type, key);
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(tt, (const cbm_gbuf_edge_t *)edge);
edge_ptr_array_t *bt = get_or_create_edge_array(gb->edges_by_type, type);
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(bt, (const cbm_gbuf_edge_t *)edge);
return id;
}
int cbm_gbuf_find_edges_by_source_type(const cbm_gbuf_t *gb, int64_t source_id, const char *type,
const cbm_gbuf_edge_t ***out, int *count) {
if (!gb || !out || !count) {
return -1;
}
char key[EDGE_KEY_BUF];
make_src_type_key(key, sizeof(key), source_id, type);
edge_ptr_array_t *arr = cbm_ht_get(gb->edges_by_source_type, key);
if (arr && arr->count > 0) {
*out = arr->items;
*count = arr->count;
} else {
*out = NULL;
*count = 0;
}
return 0;
}
int cbm_gbuf_find_edges_by_target_type(const cbm_gbuf_t *gb, int64_t target_id, const char *type,
const cbm_gbuf_edge_t ***out, int *count) {
if (!gb || !out || !count) {
return -1;
}
char key[EDGE_KEY_BUF];
make_src_type_key(key, sizeof(key), target_id, type);
edge_ptr_array_t *arr = cbm_ht_get(gb->edges_by_target_type, key);
if (arr && arr->count > 0) {
*out = arr->items;
*count = arr->count;
} else {
*out = NULL;
*count = 0;
}
return 0;
}
int cbm_gbuf_find_edges_by_type(const cbm_gbuf_t *gb, const char *type,
const cbm_gbuf_edge_t ***out, int *count) {
if (!gb || !out || !count) {
return -1;
}
edge_ptr_array_t *arr = cbm_ht_get(gb->edges_by_type, type);
if (arr && arr->count > 0) {
*out = arr->items;
*count = arr->count;
} else {
*out = NULL;
*count = 0;
}
return 0;
}
int cbm_gbuf_edge_count(const cbm_gbuf_t *gb) {
return gb ? gb->edges.count : 0;
}
int cbm_gbuf_edge_count_by_type(const cbm_gbuf_t *gb, const char *type) {
if (!gb || !type) {
return 0;
}
edge_ptr_array_t *arr = cbm_ht_get(gb->edges_by_type, type);
return arr ? arr->count : 0;
}
int cbm_gbuf_delete_edges_by_type(cbm_gbuf_t *gb, const char *type) {
if (!gb || !type) {
return -1;
}
/* Rebuild edges array without the deleted type */
int write_idx = 0;
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
if (strcmp(e->type, type) == 0) {
char key[EDGE_KEY_BUF];
make_edge_key(key, sizeof(key), e->source_id, e->target_id, e->type);
const char *ekey = cbm_ht_get_key(gb->edge_by_key, key);
cbm_ht_delete(gb->edge_by_key, key);
free((void *)ekey);
free_edge_strings(e);
free(e);
} else {
gb->edges.items[write_idx++] = gb->edges.items[i];
}
}
gb->edges.count = write_idx;
/* Rebuild edge secondary indexes */
cbm_ht_foreach(gb->edges_by_source_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_source_type);
cbm_ht_foreach(gb->edges_by_target_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_target_type);
cbm_ht_foreach(gb->edges_by_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_type);
gb->edges_by_source_type = cbm_ht_create(256);
gb->edges_by_target_type = cbm_ht_create(256);
gb->edges_by_type = cbm_ht_create(32);
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
char key[EDGE_KEY_BUF];
make_src_type_key(key, sizeof(key), e->source_id, e->type);
edge_ptr_array_t *st = get_or_create_edge_array(gb->edges_by_source_type, key);
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(st, (const cbm_gbuf_edge_t *)e);
make_src_type_key(key, sizeof(key), e->target_id, e->type);
edge_ptr_array_t *tt = get_or_create_edge_array(gb->edges_by_target_type, key);
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(tt, (const cbm_gbuf_edge_t *)e);
edge_ptr_array_t *bt = get_or_create_edge_array(gb->edges_by_type, e->type);
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(bt, (const cbm_gbuf_edge_t *)e);
}
return 0;
}
/* ── Merge ───────────────────────────────────────────────────────── */
/* Free remap hash table entries (key = heap string, value = heap int64_t*) */
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
static void free_remap_entry(const char *key, void *val, void *ud) {
(void)ud;
free((void *)key);
free(val);
}
int cbm_gbuf_merge(cbm_gbuf_t *dst, cbm_gbuf_t *src) {
if (!dst || !src) {
return -1;
}
if (src->nodes.count == 0 && src->edges.count == 0) {
return 0;
}
/* ID remap for QN-colliding nodes: "src_id" → (int64_t*) dst_id.
* Only populated when a src node's QN already exists in dst. */
CBMHashTable *remap = NULL;
for (int i = 0; i < src->nodes.count; i++) {
cbm_gbuf_node_t *sn = src->nodes.items[i];
if (!sn->qualified_name) {
continue;
}
/* Skip nodes deleted from QN index */
if (!cbm_ht_get(src->node_by_qn, sn->qualified_name)) {
continue;
}
cbm_gbuf_node_t *existing = cbm_ht_get(dst->node_by_qn, sn->qualified_name);
if (existing) {
/* QN collision: update dst node fields (src wins), keep dst's ID */
free(existing->label);
existing->label = heap_strdup(sn->label);
free(existing->name);
existing->name = heap_strdup(sn->name);
free(existing->file_path);
existing->file_path = heap_strdup(sn->file_path);
existing->start_line = sn->start_line;
existing->end_line = sn->end_line;
if (sn->properties_json) {
free(existing->properties_json);
existing->properties_json = heap_strdup(sn->properties_json);
}
/* Record remap if IDs differ */
if (sn->id != existing->id) {
if (!remap) {
remap = cbm_ht_create(32);
}
char key[32];
make_id_key(key, sizeof(key), sn->id);
int64_t *val = malloc(sizeof(int64_t));
*val = existing->id;
cbm_ht_set(remap, strdup(key), val);
}
} else {
/* No collision: copy node with original src ID */
cbm_gbuf_node_t *node = calloc(1, sizeof(cbm_gbuf_node_t));
if (!node) {
continue;
}
node->id = sn->id;
node->project = dst->project;
node->label = heap_strdup(sn->label);
node->name = heap_strdup(sn->name);
node->qualified_name = heap_strdup(sn->qualified_name);
node->file_path = heap_strdup(sn->file_path);
node->start_line = sn->start_line;
node->end_line = sn->end_line;
node->properties_json = heap_strdup(sn->properties_json);
/* Store in array */
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(&dst->nodes, node);
/* Primary indexes */
cbm_ht_set(dst->node_by_qn, node->qualified_name, node);
char id_buf[32];
make_id_key(id_buf, sizeof(id_buf), node->id);
const char *old_id_key = cbm_ht_get_key(dst->node_by_id, id_buf);
if (old_id_key) {
cbm_ht_set(dst->node_by_id, old_id_key, node);
} else {
cbm_ht_set(dst->node_by_id, strdup(id_buf), node);
}
/* Secondary indexes */
node_ptr_array_t *by_label =
get_or_create_node_array(dst->nodes_by_label, node->label ? node->label : "");
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(by_label, (const cbm_gbuf_node_t *)node);
node_ptr_array_t *by_name =
get_or_create_node_array(dst->nodes_by_name, node->name ? node->name : "");
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
cbm_da_push(by_name, (const cbm_gbuf_node_t *)node);
/* Keep next_id above all inserted IDs */
if (node->id >= dst->next_id) {
dst->next_id = node->id + 1;
}
}
}
/* Merge edges with optional ID remapping */
for (int i = 0; i < src->edges.count; i++) {
cbm_gbuf_edge_t *se = src->edges.items[i];
int64_t new_src = se->source_id;
int64_t new_tgt = se->target_id;
if (remap) {
char key[32];
make_id_key(key, sizeof(key), se->source_id);
int64_t *remapped = cbm_ht_get(remap, key);
if (remapped) {
new_src = *remapped;
}
make_id_key(key, sizeof(key), se->target_id);
remapped = cbm_ht_get(remap, key);
if (remapped) {
new_tgt = *remapped;
}
}
cbm_gbuf_insert_edge(dst, new_src, new_tgt, se->type, se->properties_json);
}
if (remap) {
cbm_ht_foreach(remap, free_remap_entry, NULL);
cbm_ht_free(remap);
}
return 0;
}
/* ── Dump / Flush ────────────────────────────────────────────────── */
/* Extract url_path value from a properties JSON string.
* Returns heap-allocated string or NULL. Caller must free. */
static char *extract_url_path(const char *props) {
if (!props) {
return NULL;
}
const char *key = strstr(props, "\"url_path\":\"");
if (!key) {
return NULL;
}
key += 12; /* strlen("\"url_path\":\"") */
const char *end = strchr(key, '"');
if (!end || end <= key) {
return NULL;
}
// NOLINTNEXTLINE(misc-include-cleaner) — strndup provided by standard header
return cbm_strndup(key, (size_t)(end - key));
}
int cbm_gbuf_dump_to_sqlite(cbm_gbuf_t *gb, const char *path) {
if (!gb || !path) {
return -1;
}
/* Count live nodes (not deleted from QN index) */
int live_count = 0;
for (int i = 0; i < gb->nodes.count; i++) {
cbm_gbuf_node_t *n = gb->nodes.items[i];
if (n->qualified_name && cbm_ht_get(gb->node_by_qn, n->qualified_name)) {
live_count++;
}
}
/* Build temp→final ID mapping */
int64_t max_temp_id = gb->next_id;
int64_t *temp_to_final = calloc((size_t)max_temp_id, sizeof(int64_t));
if (!temp_to_final) {
return -1;
}
/* Build CBMDumpNode array with sequential IDs (1..N) */
CBMDumpNode *dump_nodes =
malloc((size_t)(live_count > 0 ? live_count : 1) * sizeof(CBMDumpNode));
int node_idx = 0;
for (int i = 0; i < gb->nodes.count; i++) {
cbm_gbuf_node_t *n = gb->nodes.items[i];
if (!n->qualified_name || !cbm_ht_get(gb->node_by_qn, n->qualified_name)) {
continue;
}
int64_t final_id = node_idx + 1; /* 1-based sequential */
if (n->id < max_temp_id) {
temp_to_final[n->id] = final_id;
}
dump_nodes[node_idx] = (CBMDumpNode){
.id = final_id,
.project = gb->project,
.label = n->label,
.name = n->name,
.qualified_name = n->qualified_name,
.file_path = n->file_path ? n->file_path : "",
.start_line = n->start_line,
.end_line = n->end_line,
.properties = n->properties_json ? n->properties_json : "{}",
};
node_idx++;
}
/* Count valid edges (both endpoints resolved) */
int valid_edges = 0;
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
int64_t src = (e->source_id < max_temp_id) ? temp_to_final[e->source_id] : 0;
int64_t tgt = (e->target_id < max_temp_id) ? temp_to_final[e->target_id] : 0;
if (src > 0 && tgt > 0) {
valid_edges++;
}
}
/* Build CBMDumpEdge array with remapped IDs */
CBMDumpEdge *dump_edges =
malloc((size_t)(valid_edges > 0 ? valid_edges : 1) * sizeof(CBMDumpEdge));
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
char **url_paths = calloc((size_t)(valid_edges > 0 ? valid_edges : 1), sizeof(char *));
int edge_idx = 0;
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
int64_t src = (e->source_id < max_temp_id) ? temp_to_final[e->source_id] : 0;
int64_t tgt = (e->target_id < max_temp_id) ? temp_to_final[e->target_id] : 0;
if (src == 0 || tgt == 0) {
continue;
}
char *url_path = extract_url_path(e->properties_json);
url_paths[edge_idx] = url_path;
dump_edges[edge_idx] = (CBMDumpEdge){
.id = edge_idx + 1,
.project = gb->project,
.source_id = src,
.target_id = tgt,
.type = e->type,
.properties = e->properties_json ? e->properties_json : "{}",
.url_path = url_path ? url_path : "",
};
edge_idx++;
}
/* Generate ISO 8601 timestamp */
time_t now = time(NULL);
// NOLINTNEXTLINE(concurrency-mt-unsafe) — called once during single-threaded dump
struct tm *tm_val = gmtime(&now);
char indexed_at[64];
strftime(indexed_at, sizeof(indexed_at), "%Y-%m-%dT%H:%M:%SZ", tm_val);
/* Release lookup tables — no longer needed now that dump arrays are built.
* Frees hash tables (~200-400MB on large codebases) while keeping the
* raw node/edge data alive for cbm_write_db() to read via dump arrays. */
cbm_ht_free(gb->node_by_qn);
gb->node_by_qn = NULL;
cbm_ht_foreach(gb->node_by_id, free_key_only, NULL);
cbm_ht_free(gb->node_by_id);
gb->node_by_id = NULL;
cbm_ht_foreach(gb->nodes_by_label, free_node_array, NULL);
cbm_ht_free(gb->nodes_by_label);
gb->nodes_by_label = NULL;
cbm_ht_foreach(gb->nodes_by_name, free_node_array, NULL);
cbm_ht_free(gb->nodes_by_name);
gb->nodes_by_name = NULL;
cbm_ht_foreach(gb->edge_by_key, free_key_only, NULL);
cbm_ht_free(gb->edge_by_key);
gb->edge_by_key = NULL;
cbm_ht_foreach(gb->edges_by_source_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_source_type);
gb->edges_by_source_type = NULL;
cbm_ht_foreach(gb->edges_by_target_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_target_type);
gb->edges_by_target_type = NULL;
cbm_ht_foreach(gb->edges_by_type, free_edge_array, NULL);
cbm_ht_free(gb->edges_by_type);
gb->edges_by_type = NULL;
/* Write directly to final path — no .tmp + rename.
* Callers must delete the old .db before calling this (reindex)
* or ensure no file exists (first index). */
int rc = cbm_write_db(path, gb->project, gb->root_path, indexed_at, dump_nodes, node_idx,
dump_edges, edge_idx);
{
char b1[16];
char b2[16];
snprintf(b1, sizeof(b1), "%d", node_idx);
snprintf(b2, sizeof(b2), "%d", edge_idx);
cbm_log_info("gbuf.dump", "nodes", b1, "edges", b2);
}
/* Cleanup */
for (int i = 0; i < edge_idx; i++) {
free(url_paths[i]);
}
// NOLINTNEXTLINE(bugprone-multi-level-implicit-pointer-conversion)
free(url_paths);
free(dump_edges);
free(dump_nodes);
free(temp_to_final);
return rc;
}
int cbm_gbuf_flush_to_store(cbm_gbuf_t *gb, cbm_store_t *store) {
if (!gb || !store) {
return -1;
}
/* Upsert project */
cbm_store_upsert_project(store, gb->project, gb->root_path);
/* Begin bulk mode */
cbm_store_begin_bulk(store);
cbm_store_drop_indexes(store);
cbm_store_begin(store);
/* Delete existing project data */
cbm_store_delete_edges_by_project(store, gb->project);
cbm_store_delete_nodes_by_project(store, gb->project);
/* Build temp_id → real_id map.
* Temp IDs start at 1 and are sequential, but can have gaps from edge inserts.
* Use max_id as size. */
int64_t max_temp_id = gb->next_id;
int64_t *temp_to_real = calloc(max_temp_id, sizeof(int64_t));
for (int i = 0; i < gb->nodes.count; i++) {
cbm_gbuf_node_t *n = gb->nodes.items[i];
/* Skip if deleted from QN index */
if (!n->qualified_name || !cbm_ht_get(gb->node_by_qn, n->qualified_name)) {
continue;
}
cbm_node_t sn = {
.project = gb->project,
.label = n->label,
.name = n->name,
.qualified_name = n->qualified_name,
.file_path = n->file_path,
.start_line = n->start_line,
.end_line = n->end_line,
.properties_json = n->properties_json,
};
int64_t real_id = cbm_store_upsert_node(store, &sn);
if (real_id > 0 && n->id < max_temp_id) {
temp_to_real[n->id] = real_id;
}
}
/* Insert all edges with remapped IDs */
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
int64_t real_src = (e->source_id < max_temp_id) ? temp_to_real[e->source_id] : 0;
int64_t real_tgt = (e->target_id < max_temp_id) ? temp_to_real[e->target_id] : 0;
if (real_src == 0 || real_tgt == 0) {
continue;
}
cbm_edge_t se = {
.project = gb->project,
.source_id = real_src,
.target_id = real_tgt,
.type = e->type,
.properties_json = e->properties_json,
};
cbm_store_insert_edge(store, &se);
}
cbm_store_commit(store);
cbm_store_create_indexes(store);
cbm_store_end_bulk(store);
free(temp_to_real);
return 0;
}
int cbm_gbuf_merge_into_store(cbm_gbuf_t *gb, cbm_store_t *store) {
if (!gb || !store) {
return -1;
}
/* Begin bulk mode — no project wipe */
cbm_store_begin(store);
/* Build temp_id → real_id map */
int64_t max_temp_id = gb->next_id;
int64_t *temp_to_real = calloc(max_temp_id, sizeof(int64_t));
for (int i = 0; i < gb->nodes.count; i++) {
cbm_gbuf_node_t *n = gb->nodes.items[i];
if (!n->qualified_name || !cbm_ht_get(gb->node_by_qn, n->qualified_name)) {
continue;
}
cbm_node_t sn = {
.project = gb->project,
.label = n->label,
.name = n->name,
.qualified_name = n->qualified_name,
.file_path = n->file_path,
.start_line = n->start_line,
.end_line = n->end_line,
.properties_json = n->properties_json,
};
int64_t real_id = cbm_store_upsert_node(store, &sn);
if (real_id > 0 && n->id < max_temp_id) {
temp_to_real[n->id] = real_id;
}
}
for (int i = 0; i < gb->edges.count; i++) {
cbm_gbuf_edge_t *e = gb->edges.items[i];
int64_t real_src = (e->source_id < max_temp_id) ? temp_to_real[e->source_id] : 0;
int64_t real_tgt = (e->target_id < max_temp_id) ? temp_to_real[e->target_id] : 0;
if (real_src == 0 || real_tgt == 0) {
continue;
}
cbm_edge_t se = {
.project = gb->project,
.source_id = real_src,
.target_id = real_tgt,
.type = e->type,
.properties_json = e->properties_json,
};
cbm_store_insert_edge(store, &se);
}
cbm_store_commit(store);
free(temp_to_real);
return 0;
}