diff --git a/src/store/store.c b/src/store/store.c index c237332e..b9d1599b 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -2348,7 +2348,12 @@ static int search_where_basic(const cbm_search_params_t *params, char *where, in *wlen = where_append(where, where_sz, *wlen, nparams, bind_buf); where_bind_text(binds, bind_idx, params->project); } - if (params->label) { + /* Ignore an empty-string label: it is non-NULL but should behave like an + * omitted label (no filter), matching the BM25 query path. Without the + * params->label[0] guard, name_pattern/qn_pattern searches that pass + * label="" append `n.label = ''`, which matches no node and silently + * returns zero results (issue #481). */ + if (params->label && params->label[0]) { snprintf(bind_buf, sizeof(bind_buf), "n.label = ?%d", *bind_idx + SKIP_ONE); *wlen = where_append(where, where_sz, *wlen, nparams, bind_buf); where_bind_text(binds, bind_idx, params->label); diff --git a/tests/test_store_search.c b/tests/test_store_search.c index ded121a2..459c1c24 100644 --- a/tests/test_store_search.c +++ b/tests/test_store_search.c @@ -86,6 +86,62 @@ TEST(store_search_by_name_pattern) { PASS(); } +/* ── Empty-string label is ignored (issue #481) ────────────────── */ + +/* An empty-string label must behave like an omitted label (no filter), not be + * applied as a literal `n.label = ''` that matches nothing. Previously a + * name_pattern/qn_pattern search passing label="" returned zero results, while + * the BM25 query path ignored the empty label — an inconsistency that made + * structural class/service discovery silently fail. */ +TEST(store_search_empty_label_ignored) { + int64_t ids[3]; + cbm_store_t *s = setup_search_store(ids); + + /* name_pattern + label="" must match the same as name_pattern alone. */ + cbm_search_params_t empty_label = {.project = "test", + .name_pattern = ".*Submit.*", + .label = "", + .min_degree = -1, + .max_degree = -1}; + cbm_search_output_t out = {0}; + int rc = cbm_store_search(s, &empty_label, &out); + ASSERT_EQ(rc, CBM_STORE_OK); + ASSERT_EQ(out.count, 1); + ASSERT_STR_EQ(out.results[0].node.name, "SubmitOrder"); + cbm_store_search_free(&out); + + /* A non-empty label still filters: ".*Order.*" matches three names but only + * OrderService is a Class. */ + cbm_search_params_t cls = {.project = "test", + .name_pattern = ".*Order.*", + .label = "Class", + .min_degree = -1, + .max_degree = -1}; + cbm_search_output_t out2 = {0}; + rc = cbm_store_search(s, &cls, &out2); + ASSERT_EQ(rc, CBM_STORE_OK); + ASSERT_EQ(out2.count, 1); + ASSERT_STR_EQ(out2.results[0].node.name, "OrderService"); + cbm_store_search_free(&out2); + + /* qn_pattern shares the same WHERE builder, so empty label must be ignored + * there too. */ + cbm_search_params_t qn = {.project = "test", + .qn_pattern = ".*SubmitOrder", + .label = "", + .min_degree = -1, + .max_degree = -1}; + cbm_search_output_t out3 = {0}; + rc = cbm_store_search(s, &qn, &out3); + ASSERT_EQ(rc, CBM_STORE_OK); + ASSERT_EQ(out3.count, 1); + ASSERT_STR_EQ(out3.results[0].node.name, "SubmitOrder"); + cbm_store_search_free(&out3); + + cbm_store_close(s); + PASS(); +} + /* ── Search by file pattern ─────────────────────────────────────── */ TEST(store_search_by_file_pattern) { @@ -1234,6 +1290,7 @@ TEST(store_impact_summary_empty) { SUITE(store_search) { RUN_TEST(store_search_by_label); RUN_TEST(store_search_by_name_pattern); + RUN_TEST(store_search_empty_label_ignored); RUN_TEST(store_search_by_file_pattern); RUN_TEST(store_search_file_pattern_substring_issue200); RUN_TEST(store_search_pagination);