diff --git a/src/store/store.c b/src/store/store.c index b9d1599b..995f6e85 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -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]; diff --git a/tests/test_store_search.c b/tests/test_store_search.c index 459c1c24..5cbf1a19 100644 --- a/tests/test_store_search.c +++ b/tests/test_store_search.c @@ -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); @@ -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"; @@ -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, ¶ms, &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, ¶ms, &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, ¶ms, &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, ¶ms, &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) { @@ -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);