feat(mcp): enable overlay relationship search

Use the active overlay graph read model for search_graph relationship-only requests while keeping connected-neighbor enrichment on canonical ids until that path is separately implemented.

Add active_edges CTE support to cbm_store_search_overlay_view so relationship filters, entry-point filters, and degree filters can use visible canonical edges plus latest ready owned overlay edges keyed by qualified name.

Make search degree-filter wrapping fail explicitly on allocation or SQL truncation instead of silently truncating generated SQL.

Validation: CBM_ONLY_SUITE=store_nodes make -f Makefile.cbm -j8 test (/private/tmp/cbm-pan83-overlay-relationship-store-20260703T2043.log, 93 passed); CBM_ONLY_SUITE=mcp build/c/test-runner (/private/tmp/cbm-pan83-overlay-relationship-mcp-20260703T2048.log, 146 passed); make -f Makefile.cbm -j8 cbm (/private/tmp/cbm-pan83-overlay-relationship-product-build-20260703T2048.log); bash scripts/check-source-safety.sh (/private/tmp/cbm-pan83-overlay-relationship-source-safety-20260703T2048.log); git diff --check (/private/tmp/cbm-pan83-overlay-relationship-diff-check-20260703T2048.log).
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-03 19:41:10 -04:00
parent c0aa7e6f20
commit a21970f4f5
4 changed files with 373 additions and 65 deletions
+25 -13
View File
@@ -118,6 +118,7 @@ static void add_response_warning(yyjson_mut_doc *doc, yyjson_mut_val *root, cons
#define CBM_MCP_FRESHNESS_STALE_WITH_WARNING "stale_with_warning"
#define CBM_MCP_FRESHNESS_READ_MODEL_KEY "read_model"
#define CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_NODES "overlay_active_nodes"
#define CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_GRAPH "overlay_active_graph"
#define CBM_MCP_EXACT_DELTA_KEY "exact_delta"
static yyjson_mut_val *ensure_response_freshness(yyjson_mut_doc *doc, yyjson_mut_val *root) {
@@ -270,7 +271,7 @@ static void add_overlay_node_read_view_summary(yyjson_mut_doc *doc, yyjson_mut_v
static void add_overlay_active_node_search_freshness(
yyjson_mut_doc *doc, yyjson_mut_val *root,
const cbm_store_overlay_node_view_summary_t *summary) {
const cbm_store_overlay_node_view_summary_t *summary, bool uses_active_edges) {
if (!summary || summary->active_file_tombstones <= 0) {
return;
}
@@ -279,7 +280,9 @@ static void add_overlay_active_node_search_freshness(
return;
}
yyjson_mut_obj_add_str(doc, freshness, CBM_MCP_FRESHNESS_READ_MODEL_KEY,
CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_NODES);
uses_active_edges
? CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_GRAPH
: CBM_MCP_FRESHNESS_READ_MODEL_OVERLAY_ACTIVE_NODES);
yyjson_mut_obj_add_int(doc, freshness, "overlay_ready_generations",
summary->overlay_ready_generations);
yyjson_mut_obj_add_int(doc, freshness, "active_file_tombstones",
@@ -290,10 +293,15 @@ static void add_overlay_active_node_search_freshness(
summary->overlay_owned_nodes_visible);
yyjson_mut_obj_add_int(doc, freshness, "total_nodes_visible",
summary->total_nodes_visible);
add_response_warning(doc, root,
"search_graph graph mode used overlay active node rows; edge traversal, "
"FTS query mode, and trace results remain canonical until separately "
"enabled.");
add_response_warning(
doc, root,
uses_active_edges
? "search_graph graph mode used overlay active node and relationship rows; "
"connected-neighbor enrichment, FTS query mode, and trace results remain "
"canonical until separately enabled."
: "search_graph graph mode used overlay active node rows; connected-neighbor "
"enrichment, FTS query mode, and trace results remain canonical until "
"separately enabled.");
}
static void add_pipeline_exact_delta_stats(yyjson_mut_doc *doc, yyjson_mut_val *root,
@@ -3729,9 +3737,12 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
cbm_store_get_overlay_node_view_summary(store, project, &overlay_summary) ==
CBM_STORE_OK &&
overlay_summary.active_file_tombstones > 0;
bool overlay_edge_semantics_requested =
relationship || include_connected || exclude_entry_points;
bool overlay_search_used = overlay_ready_for_nodes && !overlay_edge_semantics_requested;
bool overlay_active_edges_requested =
relationship || exclude_entry_points || min_degree >= 0 || max_degree >= 0 ||
(sort_by && (strcmp(sort_by, "degree") == 0 || strcmp(sort_by, "calls") == 0 ||
strcmp(sort_by, "linkrank") == 0));
bool overlay_requires_canonical_edge_ids = include_connected;
bool overlay_search_used = overlay_ready_for_nodes && !overlay_requires_canonical_edge_ids;
if (overlay_search_used) {
cbm_store_search_overlay_view(store, &params, &out);
} else {
@@ -3761,12 +3772,13 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
out.node_degree_stale);
add_dirty_file_freshness(doc, root, store, project);
if (overlay_search_used) {
add_overlay_active_node_search_freshness(doc, root, &overlay_summary);
} else if (overlay_ready_for_nodes && overlay_edge_semantics_requested) {
add_overlay_active_node_search_freshness(doc, root, &overlay_summary,
overlay_active_edges_requested);
} else if (overlay_ready_for_nodes && overlay_requires_canonical_edge_ids) {
add_response_warning(doc, root,
"overlay rows are ready, but this search_graph request used "
"canonical graph rows because relationship/connected traversal "
"needs canonical edge ids until overlay edge reads are enabled.");
"canonical graph rows because connected-neighbor enrichment "
"needs canonical edge ids until overlay connected reads are enabled.");
}
if (search_graph_uses_route_derived_graph(label, relationship) &&
cbm_store_derived_view_is_stale(store, project, CBM_STORE_DERIVED_VIEW_ROUTES)) {
+171 -52
View File
@@ -6022,24 +6022,41 @@ static char *make_like_hint(const char *literal) {
return buf;
}
static void search_apply_degree_filter(char *sql, size_t sql_sz, const cbm_search_params_t *p) {
static int search_apply_degree_filter(cbm_store_t *s, char *sql, size_t sql_sz,
const cbm_search_params_t *p) {
bool has_degree_filter = (p->min_degree >= 0 || p->max_degree >= 0);
if (!has_degree_filter) {
return;
return CBM_STORE_OK;
}
char *inner_sql = malloc(sql_sz);
if (!inner_sql) {
store_set_error(s, "search degree filter out of memory");
return CBM_STORE_ERR;
}
int n = snprintf(inner_sql, sql_sz, "%s", sql);
if (n < 0 || (size_t)n >= sql_sz) {
free(inner_sql);
store_set_error(s, "search degree filter input SQL truncated");
return CBM_STORE_ERR;
}
char inner_sql[CBM_SZ_4K];
snprintf(inner_sql, sizeof(inner_sql), "%s", sql);
if (p->min_degree >= 0 && p->max_degree >= 0) {
snprintf(sql, sql_sz,
"SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d AND (in_deg + out_deg) <= %d",
inner_sql, p->min_degree, p->max_degree);
n = snprintf(sql, sql_sz,
"SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d "
"AND (in_deg + out_deg) <= %d",
inner_sql, p->min_degree, p->max_degree);
} else if (p->min_degree >= 0) {
snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d", inner_sql,
p->min_degree);
n = snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) >= %d",
inner_sql, p->min_degree);
} else {
snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) <= %d", inner_sql,
p->max_degree);
n = snprintf(sql, sql_sz, "SELECT * FROM (%s) WHERE (in_deg + out_deg) <= %d",
inner_sql, p->max_degree);
}
free(inner_sql);
if (n < 0 || (size_t)n >= sql_sz) {
store_set_error(s, "search degree filter SQL truncated");
return CBM_STORE_ERR;
}
return CBM_STORE_OK;
}
/* Append a WHERE clause fragment, joining with AND if not the first. */
@@ -6267,7 +6284,27 @@ static void search_where_advanced(const cbm_search_params_t *params, char *where
char excl_clause[CBM_SZ_512];
search_build_exclude_labels(params->exclude_labels, binds, bind_idx, excl_clause,
(int)sizeof(excl_clause));
(void)where_append(where, where_sz, *wlen, nparams, excl_clause);
*wlen = where_append(where, where_sz, *wlen, nparams, excl_clause);
}
}
static void search_where_overlay_edges(const cbm_search_params_t *params, char *where,
int where_sz, int *wlen, int *nparams,
search_bind_t *binds, int *bind_idx) {
if (params->relationship) {
char rel_clause[CBM_SZ_256];
snprintf(rel_clause, sizeof(rel_clause),
"EXISTS(SELECT 1 FROM active_edges e WHERE "
"(e.source_qn = n.qualified_name OR e.target_qn = n.qualified_name) "
"AND e.type = ?%d)",
*bind_idx + SKIP_ONE);
*wlen = where_append(where, where_sz, *wlen, nparams, rel_clause);
where_bind_text(binds, bind_idx, params->relationship);
}
if (params->exclude_entry_points) {
*wlen = where_append(where, where_sz, *wlen, nparams,
"EXISTS(SELECT 1 FROM active_edges e "
"WHERE e.target_qn = n.qualified_name)");
}
}
@@ -6282,6 +6319,79 @@ static int search_build_where(const cbm_search_params_t *params, char *where, in
return nparams;
}
static bool search_overlay_needs_active_edges(const cbm_search_params_t *params) {
if (!params) {
return false;
}
if (params->relationship || params->exclude_entry_points ||
params->min_degree >= 0 || params->max_degree >= 0) {
return true;
}
return params->sort_by &&
(strcmp(params->sort_by, "degree") == 0 ||
strcmp(params->sort_by, "calls") == 0 ||
strcmp(params->sort_by, "linkrank") == 0);
}
static int search_build_active_overlay_cte(char *buf, size_t buf_sz, bool include_edges) {
int n = snprintf(buf, buf_sz,
"WITH active_files AS ("
" SELECT t.project, t.rel_path, MAX(t.overlay_generation) AS overlay_generation"
" FROM overlay_tombstones t"
" JOIN overlay_generations g"
" ON g.project = t.project AND g.overlay_generation = t.overlay_generation"
" WHERE g.status = ?1 AND t.entity_kind = ?2 AND t.active = %d"
" GROUP BY t.project, t.rel_path"
"), active_nodes AS ("
" SELECT n.id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
" n.start_line, n.end_line, n.properties"
" FROM nodes n"
" WHERE NOT EXISTS (SELECT 1 FROM active_files af"
" WHERE af.project = n.project AND af.rel_path = n.file_path)"
" UNION ALL"
" SELECT %d AS id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
" n.start_line, n.end_line, n.properties"
" FROM overlay_nodes n"
" JOIN active_files af"
" ON af.project = n.project AND af.rel_path = n.rel_path"
" AND af.overlay_generation = n.overlay_generation"
" WHERE n.owned = %d"
")",
STORE_OVERLAY_TOMBSTONE_ACTIVE, CBM_STORE_NO_NODE_ID,
STORE_OVERLAY_ROW_OWNED);
if (n < 0 || (size_t)n >= buf_sz) {
return CBM_STORE_ERR;
}
size_t used = (size_t)n;
if (!include_edges) {
n = snprintf(buf + used, buf_sz - used, " ");
return (n >= 0 && used + (size_t)n < buf_sz) ? CBM_STORE_OK : CBM_STORE_ERR;
}
n = snprintf(buf + used, buf_sz - used,
", active_edges AS ("
" SELECT s.qualified_name AS source_qn, t.qualified_name AS target_qn, e.type"
" FROM edges e"
" JOIN nodes s ON s.id = e.source_id"
" JOIN nodes t ON t.id = e.target_id"
" WHERE NOT EXISTS (SELECT 1 FROM active_files af"
" WHERE af.project = s.project AND af.rel_path = s.file_path)"
" AND NOT EXISTS (SELECT 1 FROM active_files af"
" WHERE af.project = t.project AND af.rel_path = t.file_path)"
" UNION ALL"
" SELECT e.source_qn, e.target_qn, e.type"
" FROM overlay_edges e"
" JOIN active_files af"
" ON af.project = e.project AND af.rel_path = e.rel_path"
" AND af.overlay_generation = e.overlay_generation"
" WHERE e.owned = %d"
") ",
STORE_OVERLAY_ROW_OWNED);
if (n < 0 || used + (size_t)n >= buf_sz) {
return CBM_STORE_ERR;
}
return CBM_STORE_OK;
}
static int search_execute_sql(cbm_store_t *s, const char *sql, const char *count_sql,
search_bind_t *binds, int bind_idx,
search_like_pool_t *like_pool, bool use_pagerank,
@@ -6483,7 +6593,10 @@ int cbm_store_search(cbm_store_t *s, const cbm_search_params_t *params, cbm_sear
/* Degree filters — upstream helper wraps in subquery on in_deg/out_deg. */
bool has_degree_filter = (params->min_degree >= 0 || params->max_degree >= 0);
search_apply_degree_filter(sql, sizeof(sql), params);
if (search_apply_degree_filter(s, sql, sizeof(sql), params) != CBM_STORE_OK) {
like_pool_free(&like_pool);
return CBM_STORE_ERR;
}
/* Count query — stripped of per-row edge subqueries for the common (no-degree-filter)
* case, since we only need the row count, not in_deg/out_deg. The degree-filter
@@ -6604,46 +6717,39 @@ int cbm_store_search_overlay_view(cbm_store_t *s, const cbm_search_params_t *par
bool use_pagerank =
(!params->sort_by || strcmp(params->sort_by, "relevance") == 0) && !out->pagerank_stale;
bool use_active_edges = search_overlay_needs_active_edges(params);
const char *in_degree_expr =
use_active_edges
? "(SELECT COUNT(*) FROM active_edges e WHERE e.target_qn = n.qualified_name)"
: "(SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id)";
const char *out_degree_expr =
use_active_edges
? "(SELECT COUNT(*) FROM active_edges e WHERE e.source_qn = n.qualified_name)"
: "(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id)";
char select_with_pr[CBM_SZ_512];
char select_without_pr[CBM_SZ_512];
snprintf(select_with_pr, sizeof(select_with_pr),
"SELECT n.id, n.project, n.label, n.name, n.qualified_name, "
"n.file_path, n.start_line, n.end_line, n.properties, "
"%s AS in_deg, %s AS out_deg, COALESCE(pr.rank, 0.0) AS pr_rank ",
in_degree_expr, out_degree_expr);
snprintf(select_without_pr, sizeof(select_without_pr),
"SELECT n.id, n.project, n.label, n.name, n.qualified_name, "
"n.file_path, n.start_line, n.end_line, n.properties, "
"%s AS in_deg, %s AS out_deg ",
in_degree_expr, out_degree_expr);
const char *select_cols =
use_pagerank
? "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) AS in_deg, "
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id) AS out_deg, "
"COALESCE(pr.rank, 0.0) AS pr_rank "
: "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) AS in_deg, "
"(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id) AS out_deg ";
use_pagerank ? select_with_pr : select_without_pr;
const char *from_join =
use_pagerank ? "FROM active_nodes n LEFT JOIN pagerank pr ON pr.node_id = n.id"
: "FROM active_nodes n";
char active_cte[CBM_SZ_4K];
snprintf(active_cte, sizeof(active_cte),
"WITH active_files AS ("
" SELECT t.project, t.rel_path, MAX(t.overlay_generation) AS overlay_generation"
" FROM overlay_tombstones t"
" JOIN overlay_generations g"
" ON g.project = t.project AND g.overlay_generation = t.overlay_generation"
" WHERE g.status = ?1 AND t.entity_kind = ?2 AND t.active = %d"
" GROUP BY t.project, t.rel_path"
"), active_nodes AS ("
" SELECT n.id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
" n.start_line, n.end_line, n.properties"
" FROM nodes n"
" WHERE NOT EXISTS (SELECT 1 FROM active_files af"
" WHERE af.project = n.project AND af.rel_path = n.file_path)"
" UNION ALL"
" SELECT %d AS id, n.project, n.label, n.name, n.qualified_name, n.file_path,"
" n.start_line, n.end_line, n.properties"
" FROM overlay_nodes n"
" JOIN active_files af"
" ON af.project = n.project AND af.rel_path = n.rel_path"
" AND af.overlay_generation = n.overlay_generation"
" WHERE n.owned = %d"
") ",
STORE_OVERLAY_TOMBSTONE_ACTIVE, CBM_STORE_NO_NODE_ID, STORE_OVERLAY_ROW_OWNED);
char active_cte[ST_SQL_BUF];
if (search_build_active_overlay_cte(active_cte, sizeof(active_cte), use_active_edges) !=
CBM_STORE_OK) {
store_set_error(s, "search_overlay_view active CTE SQL truncated");
return CBM_STORE_ERR;
}
char where[CBM_SZ_2K] = "";
search_bind_t binds[ST_SEARCH_MAX_BINDS];
@@ -6651,11 +6757,21 @@ int cbm_store_search_overlay_view(cbm_store_t *s, const cbm_search_params_t *par
int bind_idx = 0;
where_bind_text(binds, &bind_idx, CBM_STORE_OVERLAY_STATUS_READY);
where_bind_text(binds, &bind_idx, CBM_STORE_OVERLAY_TOMBSTONE_FILE);
cbm_search_params_t active_params = *params;
if (use_active_edges) {
active_params.relationship = NULL;
active_params.exclude_entry_points = false;
}
int nparams =
search_build_where(params, where, (int)sizeof(where), binds, &bind_idx, &like_pool);
search_build_where(&active_params, where, (int)sizeof(where), binds, &bind_idx, &like_pool);
int wlen = (int)strlen(where);
if (use_active_edges) {
search_where_overlay_edges(params, where, (int)sizeof(where), &wlen, &nparams, binds,
&bind_idx);
}
char sql[CBM_SZ_4K];
char count_sql[CBM_SZ_4K];
char sql[ST_SQL_BUF];
char count_sql[ST_SQL_BUF];
if (nparams > 0) {
snprintf(sql, sizeof(sql), "%s%s %s WHERE %s", active_cte, select_cols, from_join, where);
snprintf(count_sql, sizeof(count_sql), "%sSELECT COUNT(*) FROM active_nodes n WHERE %s",
@@ -6667,7 +6783,10 @@ int cbm_store_search_overlay_view(cbm_store_t *s, const cbm_search_params_t *par
}
bool has_degree_filter = (params->min_degree >= 0 || params->max_degree >= 0);
search_apply_degree_filter(sql, sizeof(sql), params);
if (search_apply_degree_filter(s, sql, sizeof(sql), params) != CBM_STORE_OK) {
like_pool_free(&like_pool);
return CBM_STORE_ERR;
}
if (has_degree_filter) {
snprintf(count_sql, sizeof(count_sql), "SELECT COUNT(*) FROM (%s)", sql);
}
+78
View File
@@ -903,6 +903,83 @@ TEST(tool_search_graph_uses_overlay_active_node_rows) {
PASS();
}
TEST(tool_search_graph_uses_overlay_active_relationship_rows) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
ASSERT_NOT_NULL(st);
const char *proj = "search-overlay-relationship";
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/search-overlay-relationship"),
CBM_STORE_OK);
cbm_mcp_server_set_project(srv, proj);
cbm_node_t old_main = {.project = proj,
.label = "Function",
.name = "old_main",
.qualified_name = "search.relationship.old_main",
.file_path = "main.c",
.properties_json = "{}"};
cbm_node_t stable = {.project = proj,
.label = "Function",
.name = "stable",
.qualified_name = "search.relationship.stable",
.file_path = "stable.c",
.properties_json = "{}"};
int64_t old_main_id = cbm_store_upsert_node(st, &old_main);
int64_t stable_id = cbm_store_upsert_node(st, &stable);
ASSERT_GT(old_main_id, 0);
ASSERT_GT(stable_id, 0);
cbm_edge_t old_edge = {.project = proj,
.source_id = old_main_id,
.target_id = stable_id,
.type = "CALLS"};
ASSERT_GT(cbm_store_insert_edge(st, &old_edge), 0);
int64_t overlay_generation = 0;
ASSERT_EQ(cbm_store_reserve_overlay_generation(st, proj, 1, &overlay_generation),
CBM_STORE_OK);
cbm_node_t new_main = {.project = proj,
.label = "Function",
.name = "new_main",
.qualified_name = "search.relationship.new_main",
.file_path = "main.c",
.properties_json = "{}"};
cbm_store_delta_edge_t new_edge = {.source_qn = "search.relationship.new_main",
.target_qn = "search.relationship.stable",
.type = "CALLS",
.properties_json = "{}",
.derived_kind = CBM_STORE_DERIVED_KIND_DIRECT};
cbm_store_file_delta_t delta = {.project = proj,
.rel_path = "main.c",
.generation = 1,
.nodes = &new_main,
.node_count = 1,
.edges = &new_edge,
.edge_count = 1};
ASSERT_EQ(cbm_store_publish_overlay_file_delta(st, &delta, overlay_generation),
CBM_STORE_OK);
char *resp =
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":148,\"method\":\"tools/call\","
"\"params\":{\"name\":\"search_graph\","
"\"arguments\":{\"project\":\"search-overlay-relationship\","
"\"relationship\":\"CALLS\",\"sort_by\":\"name\","
"\"limit\":5}}}");
ASSERT_NOT_NULL(resp);
char *inner = extract_text_content(resp);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "new_main"));
ASSERT_NOT_NULL(strstr(inner, "stable"));
ASSERT_NULL(strstr(inner, "old_main"));
ASSERT_NOT_NULL(strstr(inner, "\"read_model\":\"overlay_active_graph\""));
ASSERT_NOT_NULL(strstr(inner, "overlay active node and relationship rows"));
free(inner);
free(resp);
cbm_mcp_server_free(srv);
PASS();
}
static bool mcp_test_upsert_fts_node(cbm_store_t *st, const char *project, const char *label,
const char *name, const char *qualified_name,
const char *file_path) {
@@ -4228,6 +4305,7 @@ SUITE(mcp) {
RUN_TEST(tool_search_graph_warns_on_stale_route_view);
RUN_TEST(tool_search_graph_reports_dirty_metadata_without_hiding_canonical_rows);
RUN_TEST(tool_search_graph_uses_overlay_active_node_rows);
RUN_TEST(tool_search_graph_uses_overlay_active_relationship_rows);
RUN_TEST(tool_search_graph_query_reports_dirty_metadata_without_hiding_results);
RUN_TEST(tool_search_graph_query_sees_file_delta_fts_updates);
RUN_TEST(tool_search_graph_query_honors_file_pattern_issue552);
+99
View File
@@ -1701,6 +1701,104 @@ TEST(store_search_overlay_view_matches_full_rebuild_oracle) {
PASS();
}
TEST(store_search_overlay_view_uses_active_relationship_edges) {
enum { BASE_GENERATION = 1 };
cbm_store_t *live = cbm_store_open_memory();
cbm_store_t *oracle = cbm_store_open_memory();
ASSERT_NOT_NULL(live);
ASSERT_NOT_NULL(oracle);
ASSERT_EQ(cbm_store_upsert_project(live, "test", "/tmp/test"), CBM_STORE_OK);
ASSERT_EQ(cbm_store_upsert_project(oracle, "test", "/tmp/test"), CBM_STORE_OK);
cbm_node_t old_main = {.project = "test",
.label = "Function",
.name = "old_main",
.qualified_name = "test.old_main",
.file_path = "main.go",
.properties_json = "{}"};
cbm_node_t stable = {.project = "test",
.label = "Function",
.name = "stable",
.qualified_name = "test.stable",
.file_path = "stable.go",
.properties_json = "{}"};
int64_t old_main_id = cbm_store_upsert_node(live, &old_main);
int64_t stable_id = cbm_store_upsert_node(live, &stable);
ASSERT_GT(old_main_id, 0);
ASSERT_GT(stable_id, 0);
cbm_edge_t old_edge = {.project = "test",
.source_id = old_main_id,
.target_id = stable_id,
.type = "CALLS"};
ASSERT_GT(cbm_store_insert_edge(live, &old_edge), 0);
ASSERT_GT(cbm_store_upsert_node(oracle, &stable), 0);
int64_t overlay_generation = 0;
ASSERT_EQ(cbm_store_reserve_overlay_generation(live, "test", BASE_GENERATION,
&overlay_generation),
CBM_STORE_OK);
cbm_node_t new_main = {.project = "test",
.label = "Function",
.name = "new_main",
.qualified_name = "test.new_main",
.file_path = "main.go",
.properties_json = "{}"};
cbm_store_delta_edge_t new_edge = {.source_qn = "test.new_main",
.target_qn = "test.stable",
.type = "CALLS",
.properties_json = "{}",
.derived_kind = CBM_STORE_DERIVED_KIND_DIRECT};
cbm_store_file_delta_t delta = {.project = "test",
.rel_path = "main.go",
.generation = BASE_GENERATION,
.nodes = &new_main,
.node_count = 1,
.edges = &new_edge,
.edge_count = 1};
ASSERT_EQ(cbm_store_publish_overlay_file_delta(live, &delta, overlay_generation),
CBM_STORE_OK);
int64_t new_main_id = cbm_store_upsert_node(oracle, &new_main);
ASSERT_GT(new_main_id, 0);
int64_t oracle_stable_id = 0;
cbm_node_t oracle_stable = {0};
ASSERT_EQ(cbm_store_find_node_by_qn(oracle, "test", "test.stable", &oracle_stable),
CBM_STORE_OK);
oracle_stable_id = oracle_stable.id;
cbm_node_free_fields(&oracle_stable);
cbm_edge_t oracle_edge = {.project = "test",
.source_id = new_main_id,
.target_id = oracle_stable_id,
.type = "CALLS"};
ASSERT_GT(cbm_store_insert_edge(oracle, &oracle_edge), 0);
cbm_search_params_t params = {.project = "test",
.relationship = "CALLS",
.sort_by = "name",
.limit = 10,
.min_degree = 0,
.max_degree = -1};
cbm_search_output_t active = {0};
cbm_search_output_t expected = {0};
ASSERT_EQ(cbm_store_search_overlay_view(live, &params, &active), CBM_STORE_OK);
ASSERT_EQ(cbm_store_search(oracle, &params, &expected), CBM_STORE_OK);
ASSERT_EQ(active.total, expected.total);
ASSERT_EQ(active.count, expected.count);
ASSERT_EQ(active.count, 2);
ASSERT_STR_EQ(active.results[0].node.name, expected.results[0].node.name);
ASSERT_STR_EQ(active.results[1].node.name, expected.results[1].node.name);
ASSERT_STR_EQ(active.results[0].node.name, "new_main");
ASSERT_STR_EQ(active.results[1].node.name, "stable");
ASSERT_EQ(active.results[0].node.id, CBM_STORE_NO_NODE_ID);
ASSERT_GT(active.results[1].in_degree, 0);
ASSERT_GT(active.results[0].out_degree, 0);
cbm_store_search_free(&active);
cbm_store_search_free(&expected);
cbm_store_close(live);
cbm_store_close(oracle);
PASS();
}
TEST(store_owner_metadata_crud) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
@@ -4440,6 +4538,7 @@ SUITE(store_nodes) {
RUN_TEST(store_find_nodes_by_file_overlay_view_returns_latest_ready_rows);
RUN_TEST(store_search_overlay_view_without_ready_overlay_matches_canonical_search);
RUN_TEST(store_search_overlay_view_matches_full_rebuild_oracle);
RUN_TEST(store_search_overlay_view_uses_active_relationship_edges);
RUN_TEST(store_owner_metadata_crud);
RUN_TEST(store_rebuild_file_delta_owners_derives_from_graph);
RUN_TEST(store_import_export_metadata_crud);