665 lines
22 KiB
C
665 lines
22 KiB
C
/*
|
|
* test_store_edges.c — Tests for edge CRUD operations.
|
|
*
|
|
* Ported from internal/store/store_test.go (TestEdgeCRUD, TestInsertEdgeBatch,
|
|
* TestFindEdgesByURLPath, etc.)
|
|
*/
|
|
#include "test_framework.h"
|
|
#include <store/store.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
|
|
/* Helper: create a store with project + N nodes (A, B, C, ...) */
|
|
static cbm_store_t *setup_store_with_nodes(int n, int64_t *ids) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
char name[8], qn[32];
|
|
for (int i = 0; i < n; i++) {
|
|
snprintf(name, sizeof(name), "%c", 'A' + i);
|
|
snprintf(qn, sizeof(qn), "test.%c", 'A' + i);
|
|
cbm_node_t node = {
|
|
.project = "test", .label = "Function", .name = name, .qualified_name = qn};
|
|
ids[i] = cbm_store_upsert_node(s, &node);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/* ── Edge CRUD ──────────────────────────────────────────────────── */
|
|
|
|
TEST(store_edge_insert_find) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
/* Insert edge */
|
|
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
int64_t eid = cbm_store_insert_edge(s, &e);
|
|
ASSERT_GT(eid, 0);
|
|
|
|
/* Find by source */
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_source(s, ids[0], &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_STR_EQ(edges[0].type, "CALLS");
|
|
ASSERT_EQ(edges[0].source_id, ids[0]);
|
|
ASSERT_EQ(edges[0].target_id, ids[1]);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
/* Find by target */
|
|
rc = cbm_store_find_edges_by_target(s, ids[1], &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 1);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
/* Count */
|
|
int ecnt = cbm_store_count_edges(s, "test");
|
|
ASSERT_EQ(ecnt, 1);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_edge_dedup) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
/* Insert same edge twice — should not duplicate */
|
|
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e);
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
int ecnt = cbm_store_count_edges(s, "test");
|
|
ASSERT_EQ(ecnt, 1);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* #768 schema half: the edges UNIQUE constraint must discriminate IMPORTS
|
|
* edges by local_name so two named imports from the same specifier coexist
|
|
* as two rows, while re-inserting the SAME (src,tgt,IMPORTS,local_name)
|
|
* still upserts onto one row. Non-IMPORTS uniqueness is unchanged. */
|
|
TEST(store_imports_edge_local_name_coexist) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
cbm_edge_t ea = {.project = "test",
|
|
.source_id = ids[0],
|
|
.target_id = ids[1],
|
|
.type = "IMPORTS",
|
|
.properties_json = "{\"local_name\":\"A\"}"};
|
|
cbm_edge_t eb = {.project = "test",
|
|
.source_id = ids[0],
|
|
.target_id = ids[1],
|
|
.type = "IMPORTS",
|
|
.properties_json = "{\"local_name\":\"B\"}"};
|
|
|
|
int64_t ida = cbm_store_insert_edge(s, &ea);
|
|
int64_t idb = cbm_store_insert_edge(s, &eb);
|
|
ASSERT_GT(ida, 0);
|
|
ASSERT_GT(idb, 0);
|
|
ASSERT_NEQ(ida, idb); /* distinct local_name -> distinct rows */
|
|
|
|
/* Same (src,tgt,IMPORTS,local_name) again -> upsert onto the same row. */
|
|
int64_t ida_again = cbm_store_insert_edge(s, &ea);
|
|
ASSERT_EQ(ida_again, ida);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_type(s, "test", "IMPORTS", &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 2);
|
|
ASSERT_TRUE(strstr(edges[0].properties_json, "\"local_name\":\"A\"") != NULL ||
|
|
strstr(edges[1].properties_json, "\"local_name\":\"A\"") != NULL);
|
|
ASSERT_TRUE(strstr(edges[0].properties_json, "\"local_name\":\"B\"") != NULL ||
|
|
strstr(edges[1].properties_json, "\"local_name\":\"B\"") != NULL);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
/* Non-IMPORTS dedup is unchanged: differing properties (no local_name)
|
|
* must still upsert onto one row, not fan out via a NULL discriminator. */
|
|
cbm_edge_t c1 = {.project = "test",
|
|
.source_id = ids[0],
|
|
.target_id = ids[1],
|
|
.type = "CALLS",
|
|
.properties_json = "{}"};
|
|
cbm_edge_t c2 = {.project = "test",
|
|
.source_id = ids[0],
|
|
.target_id = ids[1],
|
|
.type = "CALLS",
|
|
.properties_json = "{\"weight\":5}"};
|
|
int64_t idc1 = cbm_store_insert_edge(s, &c1);
|
|
int64_t idc2 = cbm_store_insert_edge(s, &c2);
|
|
ASSERT_GT(idc1, 0);
|
|
ASSERT_EQ(idc1, idc2);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_edge_find_by_source_type) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_store_with_nodes(3, ids);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_edge_t e2 = {
|
|
.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "IMPORTS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_source_type(s, ids[0], "CALLS", &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_STR_EQ(edges[0].type, "CALLS");
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_edge_find_by_target_type) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_store_with_nodes(3, ids);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "CALLS"};
|
|
cbm_edge_t e2 = {
|
|
.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "IMPORTS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_target_type(s, ids[2], "CALLS", &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 1);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_edge_find_by_type) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_store_with_nodes(3, ids);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_edge_t e2 = {.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "CALLS"};
|
|
cbm_edge_t e3 = {
|
|
.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "IMPORTS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
cbm_store_insert_edge(s, &e3);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_type(s, "test", "CALLS", &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 2);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_edge_count_by_type) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_store_with_nodes(3, ids);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_edge_t e2 = {.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "CALLS"};
|
|
cbm_edge_t e3 = {
|
|
.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "IMPORTS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
cbm_store_insert_edge(s, &e3);
|
|
|
|
int cnt = cbm_store_count_edges_by_type(s, "test", "CALLS");
|
|
ASSERT_EQ(cnt, 2);
|
|
|
|
cnt = cbm_store_count_edges_by_type(s, "test", "IMPORTS");
|
|
ASSERT_EQ(cnt, 1);
|
|
|
|
cnt = cbm_store_count_edges_by_type(s, "test", "NONEXISTENT");
|
|
ASSERT_EQ(cnt, 0);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_edge_delete_by_type) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_store_with_nodes(3, ids);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_edge_t e2 = {
|
|
.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "IMPORTS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
|
|
cbm_store_delete_edges_by_type(s, "test", "CALLS");
|
|
int ecnt = cbm_store_count_edges(s, "test");
|
|
ASSERT_EQ(ecnt, 1);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Edge properties ────────────────────────────────────────────── */
|
|
|
|
TEST(store_edge_properties_json) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
cbm_edge_t e = {.project = "test",
|
|
.source_id = ids[0],
|
|
.target_id = ids[1],
|
|
.type = "HTTP_CALLS",
|
|
.properties_json = "{\"url_path\":\"/api/orders/create\",\"confidence\":0.8}"};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
cbm_store_find_edges_by_source(s, ids[0], &edges, &count);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_NOT_NULL(edges[0].properties_json);
|
|
ASSERT(strstr(edges[0].properties_json, "url_path") != NULL);
|
|
ASSERT(strstr(edges[0].properties_json, "/api/orders/create") != NULL);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_edge_null_properties) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
cbm_edge_t e = {.project = "test",
|
|
.source_id = ids[0],
|
|
.target_id = ids[1],
|
|
.type = "CALLS",
|
|
.properties_json = NULL};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
cbm_store_find_edges_by_source(s, ids[0], &edges, &count);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_NOT_NULL(edges[0].properties_json);
|
|
ASSERT_STR_EQ(edges[0].properties_json, "{}");
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Batch edge insert ──────────────────────────────────────────── */
|
|
|
|
TEST(store_edge_batch_insert) {
|
|
int64_t ids[10];
|
|
cbm_store_t *s = setup_store_with_nodes(10, ids);
|
|
|
|
/* Create edges: each node calls the next */
|
|
cbm_edge_t edges[9];
|
|
for (int i = 0; i < 9; i++) {
|
|
edges[i] = (cbm_edge_t){
|
|
.project = "test", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"};
|
|
}
|
|
|
|
int rc = cbm_store_insert_edge_batch(s, edges, 9);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
|
|
int ecnt = cbm_store_count_edges(s, "test");
|
|
ASSERT_EQ(ecnt, 9);
|
|
|
|
/* Re-insert should not duplicate */
|
|
rc = cbm_store_insert_edge_batch(s, edges, 9);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ecnt = cbm_store_count_edges(s, "test");
|
|
ASSERT_EQ(ecnt, 9);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_edge_batch_empty) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
int rc = cbm_store_insert_edge_batch(s, NULL, 0);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Edge cascade on node delete ────────────────────────────────── */
|
|
|
|
TEST(store_edge_cascade_on_node_delete) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_store_with_nodes(3, ids);
|
|
|
|
/* A→B, A→C */
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_edge_t e2 = {.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
|
|
/* Delete node A — edges should cascade */
|
|
cbm_store_delete_nodes_by_project(s, "test");
|
|
int ecnt = cbm_store_count_edges(s, "test");
|
|
ASSERT_EQ(ecnt, 0);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Batch insert with count=0 (non-NULL array) ────────────────── */
|
|
|
|
TEST(store_edge_batch_insert_zero_count) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_edge_t dummy = {.project = "test", .source_id = 1, .target_id = 2, .type = "CALLS"};
|
|
int rc = cbm_store_insert_edge_batch(s, &dummy, 0);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
|
|
int ecnt = cbm_store_count_edges(s, "test");
|
|
ASSERT_EQ(ecnt, 0);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Batch insert stress: 50 edges ─────────────────────────────── */
|
|
|
|
TEST(store_edge_batch_insert_50) {
|
|
/* Create 51 nodes so we can have 50 A→B edges with distinct targets */
|
|
int64_t ids[51];
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
for (int i = 0; i < 51; i++) {
|
|
char name[8], qn[32];
|
|
snprintf(name, sizeof(name), "N%d", i);
|
|
snprintf(qn, sizeof(qn), "test.N%d", i);
|
|
cbm_node_t node = {
|
|
.project = "test", .label = "Function", .name = name, .qualified_name = qn};
|
|
ids[i] = cbm_store_upsert_node(s, &node);
|
|
}
|
|
|
|
/* 50 edges: N0→N1, N1→N2, ..., N49→N50 */
|
|
cbm_edge_t edges[50];
|
|
for (int i = 0; i < 50; i++) {
|
|
edges[i] = (cbm_edge_t){
|
|
.project = "test", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"};
|
|
}
|
|
|
|
int rc = cbm_store_insert_edge_batch(s, edges, 50);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
|
|
int ecnt = cbm_store_count_edges(s, "test");
|
|
ASSERT_EQ(ecnt, 50);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── find_edges_by_source with non-existent source ─────────────── */
|
|
|
|
TEST(store_edge_find_source_nonexistent) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_source(s, 999999, &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 0);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── find_edges_by_target with non-existent target ─────────────── */
|
|
|
|
TEST(store_edge_find_target_nonexistent) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_target(s, 999999, &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 0);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── find_edges_by_type for non-existent type ──────────────────── */
|
|
|
|
TEST(store_edge_find_type_nonexistent) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_type(s, "test", "DOES_NOT_EXIST", &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 0);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── count_edges on empty project ──────────────────────────────── */
|
|
|
|
TEST(store_edge_count_empty_project) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "empty", "/tmp/empty");
|
|
|
|
int ecnt = cbm_store_count_edges(s, "empty");
|
|
ASSERT_EQ(ecnt, 0);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── count_edges_by_type for missing type ──────────────────────── */
|
|
|
|
TEST(store_edge_count_by_type_missing) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
int cnt = cbm_store_count_edges_by_type(s, "test", "NEVER_EXISTS");
|
|
ASSERT_EQ(cnt, 0);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── delete_edges_by_type preserves other types ────────────────── */
|
|
|
|
TEST(store_edge_delete_by_type_preserves_others) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_store_with_nodes(3, ids);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_edge_t e2 = {
|
|
.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "IMPORTS"};
|
|
cbm_edge_t e3 = {
|
|
.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "HTTP_CALLS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
cbm_store_insert_edge(s, &e3);
|
|
ASSERT_EQ(cbm_store_count_edges(s, "test"), 3);
|
|
|
|
/* Delete only CALLS */
|
|
cbm_store_delete_edges_by_type(s, "test", "CALLS");
|
|
ASSERT_EQ(cbm_store_count_edges(s, "test"), 2);
|
|
|
|
/* IMPORTS and HTTP_CALLS still exist */
|
|
ASSERT_EQ(cbm_store_count_edges_by_type(s, "test", "IMPORTS"), 1);
|
|
ASSERT_EQ(cbm_store_count_edges_by_type(s, "test", "HTTP_CALLS"), 1);
|
|
ASSERT_EQ(cbm_store_count_edges_by_type(s, "test", "CALLS"), 0);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── delete_edges_by_project preserves other projects ──────────── */
|
|
|
|
TEST(store_edge_delete_by_project_preserves_others) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "alpha", "/tmp/alpha");
|
|
cbm_store_upsert_project(s, "beta", "/tmp/beta");
|
|
|
|
/* Create nodes in both projects */
|
|
cbm_node_t na = {
|
|
.project = "alpha", .label = "Function", .name = "A", .qualified_name = "alpha.A"};
|
|
cbm_node_t nb = {
|
|
.project = "alpha", .label = "Function", .name = "B", .qualified_name = "alpha.B"};
|
|
cbm_node_t nc = {
|
|
.project = "beta", .label = "Function", .name = "C", .qualified_name = "beta.C"};
|
|
cbm_node_t nd = {
|
|
.project = "beta", .label = "Function", .name = "D", .qualified_name = "beta.D"};
|
|
int64_t idA = cbm_store_upsert_node(s, &na);
|
|
int64_t idB = cbm_store_upsert_node(s, &nb);
|
|
int64_t idC = cbm_store_upsert_node(s, &nc);
|
|
int64_t idD = cbm_store_upsert_node(s, &nd);
|
|
|
|
cbm_edge_t e1 = {.project = "alpha", .source_id = idA, .target_id = idB, .type = "CALLS"};
|
|
cbm_edge_t e2 = {.project = "beta", .source_id = idC, .target_id = idD, .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
|
|
ASSERT_EQ(cbm_store_count_edges(s, "alpha"), 1);
|
|
ASSERT_EQ(cbm_store_count_edges(s, "beta"), 1);
|
|
|
|
/* Delete alpha edges only */
|
|
cbm_store_delete_edges_by_project(s, "alpha");
|
|
ASSERT_EQ(cbm_store_count_edges(s, "alpha"), 0);
|
|
ASSERT_EQ(cbm_store_count_edges(s, "beta"), 1);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Edge with special chars in properties_json ────────────────── */
|
|
|
|
TEST(store_edge_properties_special_chars) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
/* JSON with unicode, quotes, backslashes */
|
|
const char *json = "{\"desc\":\"line1\\nline2\",\"path\":\"/api/v1/users?q=foo&bar=baz\","
|
|
"\"emoji\":\"\\u2603\"}";
|
|
cbm_edge_t e = {.project = "test",
|
|
.source_id = ids[0],
|
|
.target_id = ids[1],
|
|
.type = "HTTP_CALLS",
|
|
.properties_json = json};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
cbm_store_find_edges_by_source(s, ids[0], &edges, &count);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_NOT_NULL(edges[0].properties_json);
|
|
/* Round-trip should preserve the JSON string */
|
|
ASSERT(strstr(edges[0].properties_json, "line1\\nline2") != NULL);
|
|
ASSERT(strstr(edges[0].properties_json, "/api/v1/users") != NULL);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Edge with very long type string ───────────────────────────── */
|
|
|
|
TEST(store_edge_long_type_string) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
/* 200-char type string */
|
|
char long_type[201];
|
|
memset(long_type, 'X', 200);
|
|
long_type[200] = '\0';
|
|
|
|
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = long_type};
|
|
int64_t eid = cbm_store_insert_edge(s, &e);
|
|
ASSERT_GT(eid, 0);
|
|
|
|
/* Verify it round-trips */
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_source(s, ids[0], &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 1);
|
|
ASSERT_STR_EQ(edges[0].type, long_type);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── find_edges_by_source_type with non-existent type ──────────── */
|
|
|
|
TEST(store_edge_find_source_type_nonexistent) {
|
|
int64_t ids[2];
|
|
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
|
|
|
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
cbm_edge_t *edges = NULL;
|
|
int count = 0;
|
|
int rc = cbm_store_find_edges_by_source_type(s, ids[0], "NOPE", &edges, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 0);
|
|
cbm_store_free_edges(edges, count);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(store_edges) {
|
|
RUN_TEST(store_edge_insert_find);
|
|
RUN_TEST(store_edge_dedup);
|
|
RUN_TEST(store_imports_edge_local_name_coexist);
|
|
RUN_TEST(store_edge_find_by_source_type);
|
|
RUN_TEST(store_edge_find_by_target_type);
|
|
RUN_TEST(store_edge_find_by_type);
|
|
RUN_TEST(store_edge_count_by_type);
|
|
RUN_TEST(store_edge_delete_by_type);
|
|
RUN_TEST(store_edge_properties_json);
|
|
RUN_TEST(store_edge_null_properties);
|
|
RUN_TEST(store_edge_batch_insert);
|
|
RUN_TEST(store_edge_batch_empty);
|
|
RUN_TEST(store_edge_cascade_on_node_delete);
|
|
/* Edge case tests */
|
|
RUN_TEST(store_edge_batch_insert_zero_count);
|
|
RUN_TEST(store_edge_batch_insert_50);
|
|
RUN_TEST(store_edge_find_source_nonexistent);
|
|
RUN_TEST(store_edge_find_target_nonexistent);
|
|
RUN_TEST(store_edge_find_type_nonexistent);
|
|
RUN_TEST(store_edge_count_empty_project);
|
|
RUN_TEST(store_edge_count_by_type_missing);
|
|
RUN_TEST(store_edge_delete_by_type_preserves_others);
|
|
RUN_TEST(store_edge_delete_by_project_preserves_others);
|
|
RUN_TEST(store_edge_properties_special_chars);
|
|
RUN_TEST(store_edge_long_type_string);
|
|
RUN_TEST(store_edge_find_source_type_nonexistent);
|
|
}
|