Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/store/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -2452,9 +2452,9 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
const char *select_cols = "SELECT n.id, n.project, n.label, n.name, n.qualified_name, "
"n.file_path, n.start_line, n.end_line, n.properties, "
"(SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id AND "
"e.type IN ('CALLS', 'USAGE')) AS in_deg, "
"e.type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')) AS in_deg, "
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND "
"e.type IN ('CALLS', 'USAGE')) AS out_deg ";
"e.type IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS')) AS out_deg ";

char where[CBM_SZ_2K] = "";
search_bind_t binds[ST_SEARCH_MAX_BINDS];
Expand Down
177 changes: 176 additions & 1 deletion tests/test_store_search.c
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,15 @@ TEST(store_search_pagination) {

/* ── Search with degree filter ──────────────────────────────────── */

static int search_result_index_by_name(const cbm_search_output_t *out, const char *name) {
for (int i = 0; i < out->count; i++) {
if (out->results[i].node.name && strcmp(out->results[i].node.name, name) == 0) {
return i;
}
}
return -1;
}

TEST(store_search_degree_filter) {
int64_t ids[3];
cbm_store_t *s = setup_search_store(ids);
Expand All @@ -239,7 +248,7 @@ TEST(store_search_degree_filter) {
ASSERT_EQ(out.count, 2);
cbm_store_search_free(&out);

/* max_degree = 0 should find nodes with no CALLS edges */
/* max_degree = 0 should find nodes with no counted degree edges */
params.min_degree = -1; /* no min */
params.max_degree = 0; /* only zero-degree nodes */
params.label = "Function";
Expand All @@ -253,6 +262,168 @@ TEST(store_search_degree_filter) {
PASS();
}

TEST(store_search_degree_counts_inherits) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");

cbm_node_t parent = {.project = "test",
.label = "Class",
.name = "AbstractAttachmentDto",
.qualified_name = "test.AbstractAttachmentDto"};
int64_t parent_id = cbm_store_upsert_node(s, &parent);
ASSERT_GT(parent_id, 0);

const char *child_names[] = {"AttachmentDtoA", "AttachmentDtoB", "AttachmentDtoC"};
const char *child_qns[] = {"test.AttachmentDtoA", "test.AttachmentDtoB", "test.AttachmentDtoC"};
for (int i = 0; i < 3; i++) {
cbm_node_t child = {.project = "test",
.label = "Class",
.name = child_names[i],
.qualified_name = child_qns[i]};
int64_t child_id = cbm_store_upsert_node(s, &child);
ASSERT_GT(child_id, 0);
cbm_edge_t edge = {
.project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"};
ASSERT_GT(cbm_store_insert_edge(s, &edge), 0);
}

int in_deg = 0;
int out_deg = 0;
cbm_store_node_degree(s, parent_id, &in_deg, &out_deg);
ASSERT_EQ(in_deg, 0);
ASSERT_EQ(out_deg, 0);

cbm_search_params_t params = {
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
cbm_search_output_t out = {0};
int rc = cbm_store_search(s, &params, &out);
ASSERT_EQ(rc, CBM_STORE_OK);
int parent_idx = search_result_index_by_name(&out, "AbstractAttachmentDto");
ASSERT_GTE(parent_idx, 0);
ASSERT_EQ(out.results[parent_idx].in_degree, 3);
ASSERT_EQ(out.results[parent_idx].out_degree, 0);
cbm_store_search_free(&out);

cbm_store_close(s);
PASS();
}

TEST(store_search_degree_calls_plus_inherits_no_double_count) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");

cbm_node_t child = {.project = "test",
.label = "Class",
.name = "AttachmentDto",
.qualified_name = "test.AttachmentDto"};
cbm_node_t parent = {.project = "test",
.label = "Class",
.name = "BaseAttachmentDto",
.qualified_name = "test.BaseAttachmentDto"};
cbm_node_t fn = {.project = "test",
.label = "Function",
.name = "normalizeAttachment",
.qualified_name = "test.normalizeAttachment"};
int64_t child_id = cbm_store_upsert_node(s, &child);
int64_t parent_id = cbm_store_upsert_node(s, &parent);
int64_t fn_id = cbm_store_upsert_node(s, &fn);
ASSERT_GT(child_id, 0);
ASSERT_GT(parent_id, 0);
ASSERT_GT(fn_id, 0);

cbm_edge_t calls = {
.project = "test", .source_id = child_id, .target_id = fn_id, .type = "CALLS"};
cbm_edge_t inherits = {
.project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"};
ASSERT_GT(cbm_store_insert_edge(s, &calls), 0);
ASSERT_GT(cbm_store_insert_edge(s, &inherits), 0);

int in_deg = 0;
int out_deg = 0;
cbm_store_node_degree(s, child_id, &in_deg, &out_deg);
ASSERT_EQ(in_deg, 0);
ASSERT_EQ(out_deg, 1);

cbm_search_params_t params = {
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
cbm_search_output_t out = {0};
int rc = cbm_store_search(s, &params, &out);
ASSERT_EQ(rc, CBM_STORE_OK);
int child_idx = search_result_index_by_name(&out, "AttachmentDto");
ASSERT_GTE(child_idx, 0);
ASSERT_EQ(out.results[child_idx].in_degree, 0);
ASSERT_EQ(out.results[child_idx].out_degree, 2);
cbm_store_search_free(&out);

cbm_store_close(s);
PASS();
}

TEST(store_search_min_degree_includes_inherits_only) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");

cbm_node_t child = {.project = "test",
.label = "Class",
.name = "InheritanceOnlyChild",
.qualified_name = "test.InheritanceOnlyChild"};
cbm_node_t parent = {.project = "test",
.label = "Class",
.name = "InheritanceOnlyParent",
.qualified_name = "test.InheritanceOnlyParent"};
int64_t child_id = cbm_store_upsert_node(s, &child);
int64_t parent_id = cbm_store_upsert_node(s, &parent);
ASSERT_GT(child_id, 0);
ASSERT_GT(parent_id, 0);

cbm_edge_t edge = {
.project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"};
ASSERT_GT(cbm_store_insert_edge(s, &edge), 0);

cbm_search_params_t params = {
.project = "test", .label = "Class", .min_degree = 1, .max_degree = -1};
cbm_search_output_t out = {0};
int rc = cbm_store_search(s, &params, &out);
ASSERT_EQ(rc, CBM_STORE_OK);
ASSERT_GTE(search_result_index_by_name(&out, "InheritanceOnlyParent"), 0);
cbm_store_search_free(&out);

cbm_store_close(s);
PASS();
}

TEST(store_search_isolated_node_zero_degree) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");

cbm_node_t node = {.project = "test",
.label = "Class",
.name = "LonelyClass",
.qualified_name = "test.LonelyClass"};
int64_t node_id = cbm_store_upsert_node(s, &node);
ASSERT_GT(node_id, 0);

int in_deg = 0;
int out_deg = 0;
cbm_store_node_degree(s, node_id, &in_deg, &out_deg);
ASSERT_EQ(in_deg, 0);
ASSERT_EQ(out_deg, 0);

cbm_search_params_t params = {
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
cbm_search_output_t out = {0};
int rc = cbm_store_search(s, &params, &out);
ASSERT_EQ(rc, CBM_STORE_OK);
int idx = search_result_index_by_name(&out, "LonelyClass");
ASSERT_GTE(idx, 0);
ASSERT_EQ(out.results[idx].in_degree, 0);
ASSERT_EQ(out.results[idx].out_degree, 0);
cbm_store_search_free(&out);

cbm_store_close(s);
PASS();
}

/* ── Search all (no filters) ────────────────────────────────────── */

TEST(store_search_all) {
Expand Down Expand Up @@ -1295,6 +1466,10 @@ SUITE(store_search) {
RUN_TEST(store_search_file_pattern_substring_issue200);
RUN_TEST(store_search_pagination);
RUN_TEST(store_search_degree_filter);
RUN_TEST(store_search_degree_counts_inherits);
RUN_TEST(store_search_degree_calls_plus_inherits_no_double_count);
RUN_TEST(store_search_min_degree_includes_inherits_only);
RUN_TEST(store_search_isolated_node_zero_degree);
RUN_TEST(store_search_all);
RUN_TEST(store_search_exclude_labels);
RUN_TEST(store_search_case_insensitive);
Expand Down
Loading