From ad673af994bcc1434bea727479cf2a712295ba43 Mon Sep 17 00:00:00 2001 From: King Star Date: Sun, 5 Jul 2026 19:46:30 +0800 Subject: [PATCH 1/2] fix(cypher): enforce variable-length path semantics Signed-off-by: King Star --- src/cypher/cypher.c | 5 +++++ src/store/store.c | 9 ++++++--- tests/test_cypher.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 4f554ea4..61a96968 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -2904,11 +2904,16 @@ static void expand_var_length(cbm_store_t *store, cbm_rel_pattern_t *rel, cbm_traverse_result_t tr = {0}; const char *dir = rel->direction ? rel->direction : "outbound"; cbm_store_bfs(store, src->id, dir, rel->types, rel->type_count, max_depth, CBM_PERCENT, &tr); + cbm_node_t *bound_to = binding_get(b, to_var); + int64_t bound_to_id = bound_to ? bound_to->id : 0; for (int v = 0; v < tr.visited_count && *new_count < max_new; v++) { cbm_node_hop_t *hop = &tr.visited[v]; if (hop->hop < rel->min_hops) { continue; } + if (bound_to && hop->node.id != bound_to_id) { + continue; + } if (target_node->label && !label_alt_matches(hop->node.label, target_node->label)) { continue; } diff --git a/src/store/store.c b/src/store/store.c index 33a9453c..5e86d194 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -2843,13 +2843,16 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const } snprintf(sql, sizeof(sql), - "WITH RECURSIVE bfs(node_id, hop) AS (" - " SELECT %lld, 0" + "WITH RECURSIVE bfs(node_id, hop, edge_path) AS (" + " SELECT %lld, 0, ''" " UNION" - " SELECT %s, bfs.hop + 1" + " SELECT %s, bfs.hop + 1," + " CASE WHEN bfs.edge_path = '' THEN CAST(e.id AS TEXT)" + " ELSE bfs.edge_path || ',' || e.id END" " FROM bfs" " JOIN edges e ON %s" " WHERE e.type IN (%s) AND bfs.hop < %d" + " AND instr(',' || bfs.edge_path || ',', ',' || e.id || ',') = 0" ")" "SELECT DISTINCT n.id, n.project, n.label, n.name, n.qualified_name, " "n.file_path, n.start_line, n.end_line, n.properties, bfs.hop " diff --git a/tests/test_cypher.c b/tests/test_cypher.c index 5fe3cd59..daeb025b 100644 --- a/tests/test_cypher.c +++ b/tests/test_cypher.c @@ -902,6 +902,48 @@ TEST(cypher_exec_var_length_explicit_bound_capped) { PASS(); } +TEST(cypher_exec_variable_length_repeated_node_var_unifies) { + cbm_store_t *s = setup_cypher_store(); + cbm_cypher_result_t r = {0}; + + int rc = cbm_cypher_execute(s, + "MATCH (f:Function)-[:CALLS*1..2]->(f:Function) " + "RETURN f.name", + "test", 0, &r); + ASSERT_EQ(rc, 0); + ASSERT_EQ(r.row_count, 0); + + cbm_cypher_result_free(&r); + cbm_store_close(s); + PASS(); +} + +TEST(cypher_exec_var_length_no_reuse_self_loop) { + cbm_store_t *s = cbm_store_open_memory(); + cbm_store_upsert_project(s, "test", "/tmp/test"); + + cbm_node_t n = {.project = "test", + .label = "Function", + .name = "Recursive", + .qualified_name = "test.Recursive", + .file_path = "recursive.go"}; + int64_t id = cbm_store_upsert_node(s, &n); + cbm_edge_t e = {.project = "test", .source_id = id, .target_id = id, .type = "CALLS"}; + cbm_store_insert_edge(s, &e); + + cbm_cypher_result_t r = {0}; + int rc = cbm_cypher_execute(s, + "MATCH (f:Function {name: \"Recursive\"})-[:CALLS*2..2]" + "->(g:Function) RETURN g.name", + "test", 0, &r); + ASSERT_EQ(rc, 0); + ASSERT_EQ(r.row_count, 0); + + cbm_cypher_result_free(&r); + cbm_store_close(s); + PASS(); +} + TEST(cypher_exec_defines_edge) { cbm_store_t *s = setup_cypher_store(); cbm_cypher_result_t r = {0}; @@ -2650,6 +2692,8 @@ SUITE(cypher) { RUN_TEST(cypher_exec_order_by); RUN_TEST(cypher_exec_variable_length); RUN_TEST(cypher_exec_var_length_explicit_bound_capped); + RUN_TEST(cypher_exec_variable_length_repeated_node_var_unifies); + RUN_TEST(cypher_exec_var_length_no_reuse_self_loop); RUN_TEST(cypher_exec_defines_edge); RUN_TEST(cypher_exec_no_results); RUN_TEST(cypher_exec_where_numeric); From 85c32f3010b9e443b7f9926def63cdb971aa5b52 Mon Sep 17 00:00:00 2001 From: King Star Date: Mon, 6 Jul 2026 00:53:39 +0800 Subject: [PATCH 2/2] fix(store): cap recursive BFS path rows Signed-off-by: King Star --- src/store/store.c | 20 +++++++++++++++++++- tests/test_store_search.c | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/store/store.c b/src/store/store.c index 5e86d194..b8c6f785 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -57,6 +57,8 @@ enum { ST_METHOD_PROP_LEN = 8, ST_PATH_PROP_LEN = 6, ST_HANDLER_PROP_LEN = 9, + ST_BFS_CTE_ROW_MULTIPLIER = 8, + ST_BFS_MAX_CTE_ROWS = 4096, }; #define SLEN(s) (sizeof(s) - 1) @@ -2814,6 +2816,18 @@ static void bfs_build_types_clause(int edge_type_count, char *buf, int buf_sz) { } } +static int bfs_cte_row_limit(int max_results) { + int result_budget = max_results > 0 ? max_results : ST_INIT_CAP_16; + long row_budget = (long)result_budget * ST_BFS_CTE_ROW_MULTIPLIER + SKIP_ONE; + if (row_budget > ST_BFS_MAX_CTE_ROWS) { + return ST_BFS_MAX_CTE_ROWS; + } + if (row_budget < ST_INIT_CAP_16) { + return ST_INIT_CAP_16; + } + return (int)row_budget; +} + int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const char **edge_types, int edge_type_count, int max_depth, int max_results, cbm_traverse_result_t *out) { memset(out, 0, sizeof(*out)); @@ -2842,6 +2856,8 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const next_id = "e.target_id"; } + int cte_row_limit = bfs_cte_row_limit(max_results); + snprintf(sql, sizeof(sql), "WITH RECURSIVE bfs(node_id, hop, edge_path) AS (" " SELECT %lld, 0, ''" @@ -2853,6 +2869,7 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const " JOIN edges e ON %s" " WHERE e.type IN (%s) AND bfs.hop < %d" " AND instr(',' || bfs.edge_path || ',', ',' || e.id || ',') = 0" + " LIMIT %d" ")" "SELECT DISTINCT n.id, n.project, n.label, n.name, n.qualified_name, " "n.file_path, n.start_line, n.end_line, n.properties, bfs.hop " @@ -2861,7 +2878,8 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const "WHERE bfs.hop > 0 " /* exclude root */ "ORDER BY bfs.hop " "LIMIT %d;", - (long long)start_id, next_id, join_cond, types_clause, max_depth, max_results); + (long long)start_id, next_id, join_cond, types_clause, max_depth, cte_row_limit, + max_results); sqlite3_stmt *stmt = NULL; rc = sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL); diff --git a/tests/test_store_search.c b/tests/test_store_search.c index 5cbf1a19..e589fc22 100644 --- a/tests/test_store_search.c +++ b/tests/test_store_search.c @@ -7,6 +7,7 @@ #include "test_framework.h" #include "test_helpers.h" #include +#include #include #include #include @@ -991,6 +992,40 @@ TEST(store_bfs_with_risk_labels) { PASS(); } +TEST(store_bfs_caps_recursive_cte_rows) { + cbm_store_t *s = cbm_store_open_memory(); + cbm_store_upsert_project(s, "test", "/tmp/test"); + + cbm_node_t root = {.project = "test", + .label = "Function", + .name = "Root", + .qualified_name = "test.Root"}; + int64_t root_id = cbm_store_upsert_node(s, &root); + + enum { NODE_COUNT = 4200 }; + for (int i = 0; i < NODE_COUNT; i++) { + char name[32]; + snprintf(name, sizeof(name), "Leaf%d", i); + cbm_node_t leaf = { + .project = "test", .label = "Function", .name = name, .qualified_name = name}; + int64_t leaf_id = cbm_store_upsert_node(s, &leaf); + cbm_edge_t edge = { + .project = "test", .source_id = root_id, .target_id = leaf_id, .type = "CALLS"}; + cbm_store_insert_edge(s, &edge); + } + + const char *types[] = {"CALLS"}; + cbm_traverse_result_t result = {0}; + int rc = cbm_store_bfs(s, root_id, "outbound", types, 1, 1, 5000, &result); + ASSERT_EQ(rc, CBM_STORE_OK); + ASSERT_TRUE(result.visited_count > 0); + ASSERT_TRUE(result.visited_count < NODE_COUNT); + + cbm_store_traverse_free(&result); + cbm_store_close(s); + PASS(); +} + /* ── BFS cross-service summary ─────────────────────────────────── */ TEST(store_bfs_cross_service_summary) { @@ -1487,6 +1522,7 @@ SUITE(store_search) { RUN_TEST(store_cross_service_detection); RUN_TEST(store_deduplicate_hops); RUN_TEST(store_bfs_with_risk_labels); + RUN_TEST(store_bfs_caps_recursive_cte_rows); RUN_TEST(store_bfs_cross_service_summary); RUN_TEST(store_glob_to_like); RUN_TEST(store_extract_like_hints);