From e3d49da9377a28c48533113ff703a4b4b93fab08 Mon Sep 17 00:00:00 2001 From: King Star Date: Sun, 5 Jul 2026 19:46:30 +0800 Subject: [PATCH 1/3] fix(cypher): enforce variable-length path semantics Signed-off-by: King Star --- src/cypher/cypher.c | 5 +++++ src/store/store.c | 15 ++++++--------- tests/test_cypher.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 667029d5c..99efa0d6d 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -2973,11 +2973,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 55581c254..8f6bb8c0d 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -3274,19 +3274,16 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const } snprintf(sql, sizeof(sql), - /* SHORTEST-PATH semantics: the UNION dedupes (node, hop) PAIRS, - * so a single self-loop minted every hop level for every node it - * could reach — walk-padding that fabricated *k..k Cypher matches - * of arbitrary length and exploded the row set to nodes x depth - * (#797). MIN(hop) GROUP BY node returns each node once at its - * minimal distance. */ - "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 n.id, n.project, n.label, n.name, n.qualified_name, " "n.file_path, n.start_line, n.end_line, n.properties, MIN(bfs.hop) AS hop " diff --git a/tests/test_cypher.c b/tests/test_cypher.c index bc627f5c5..3ff3886fe 100644 --- a/tests/test_cypher.c +++ b/tests/test_cypher.c @@ -1053,6 +1053,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}; @@ -2893,6 +2935,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 bdcf926cfa6890a2d78d0fbcb10da57a39071866 Mon Sep 17 00:00:00 2001 From: King Star Date: Mon, 6 Jul 2026 00:53:39 +0800 Subject: [PATCH 2/3] 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 8f6bb8c0d..ba60ca6a7 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) @@ -3245,6 +3247,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)); @@ -3273,6 +3287,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, ''" @@ -3284,6 +3300,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 n.id, n.project, n.label, n.name, n.qualified_name, " "n.file_path, n.start_line, n.end_line, n.properties, MIN(bfs.hop) AS hop " @@ -3293,7 +3310,8 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const "GROUP BY n.id " "ORDER BY 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 5cbf1a199..e589fc228 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); From 6040d8a57f521fbc0a667d64afe9b1671b8925b7 Mon Sep 17 00:00:00 2001 From: King Star Date: Sun, 12 Jul 2026 23:49:05 +0800 Subject: [PATCH 3/3] fix(cypher): scope trail traversal to path expansion Signed-off-by: King Star --- src/cypher/cypher.c | 3 +- src/store/store.c | 93 +++++++++++++++++++++++++++------------ src/store/store.h | 5 +++ tests/test_cypher.c | 15 +++++-- tests/test_store_search.c | 59 +++++++++++++++++++++++-- 5 files changed, 140 insertions(+), 35 deletions(-) diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 99efa0d6d..94bdec058 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -2972,7 +2972,8 @@ 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_store_bfs_trail(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++) { diff --git a/src/store/store.c b/src/store/store.c index ba60ca6a7..0b65d55dc 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -23,6 +23,7 @@ enum { ST_COL_7 = 7, ST_COL_8 = 8, ST_COL_9 = 9, + ST_COL_10 = 10, ST_FOUND = -1, ST_BUF_16 = 16, ST_BUF_64 = 64, @@ -3259,8 +3260,9 @@ static int bfs_cte_row_limit(int max_results) { 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) { +static int 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, + bool trail, cbm_traverse_result_t *out) { memset(out, 0, sizeof(*out)); cbm_node_t root = {0}; @@ -3287,31 +3289,45 @@ 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, ''" - " UNION" - " 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" - " LIMIT %d" - ")" - "SELECT n.id, n.project, n.label, n.name, n.qualified_name, " - "n.file_path, n.start_line, n.end_line, n.properties, MIN(bfs.hop) AS hop " - "FROM bfs " - "JOIN nodes n ON n.id = bfs.node_id " - "WHERE bfs.hop > 0 " /* exclude root at hop 0 (self via a loop still appears) */ - "GROUP BY n.id " - "ORDER BY hop " - "LIMIT %d;", - (long long)start_id, next_id, join_cond, types_clause, max_depth, cte_row_limit, - max_results); + int cte_row_limit = 0; + if (trail) { + 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, ''" + " UNION" + " 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" + " 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, " + "(SELECT count(*) FROM bfs) " + "FROM bfs JOIN nodes n ON n.id = bfs.node_id " + "WHERE bfs.hop > 0 ORDER BY bfs.hop LIMIT %d;", + (long long)start_id, next_id, join_cond, types_clause, max_depth, + cte_row_limit + SKIP_ONE, max_results); + } else { + snprintf(sql, sizeof(sql), + "WITH RECURSIVE bfs(node_id, hop) AS (" + " SELECT %lld, 0" + " UNION" + " SELECT %s, bfs.hop + 1" + " FROM bfs" + " JOIN edges e ON %s" + " WHERE e.type IN (%s) AND bfs.hop < %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 " + "FROM bfs JOIN nodes n ON n.id = bfs.node_id " + "WHERE bfs.hop > 0 ORDER BY bfs.hop LIMIT %d;", + (long long)start_id, next_id, join_cond, types_clause, max_depth, max_results); + } sqlite3_stmt *stmt = NULL; rc = sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL); @@ -3331,6 +3347,7 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const int cap = ST_INIT_CAP_16; int n = 0; + int cte_rows = 0; cbm_node_hop_t *visited = malloc(cap * sizeof(cbm_node_hop_t)); int scan_rc15; @@ -3341,6 +3358,9 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const } scan_node(stmt, &visited[n].node); visited[n].hop = sqlite3_column_int(stmt, ST_COL_9); + if (trail) { + cte_rows = sqlite3_column_int(stmt, ST_COL_10); + } n++; } if (scan_rc15 != SQLITE_DONE) { /* SCANCHK:15:stmt */ @@ -3353,6 +3373,12 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const sqlite3_finalize(stmt); + if (trail && cte_rows > cte_row_limit) { + char limit_buf[16]; + snprintf(limit_buf, sizeof(limit_buf), "%d", cte_row_limit); + cbm_log_warn("cypher.trail_truncated", "cte_rows", limit_buf, "result", "partial"); + } + out->visited = visited; out->visited_count = n; @@ -3368,6 +3394,19 @@ int cbm_store_bfs(cbm_store_t *s, int64_t start_id, const char *direction, const return CBM_STORE_OK; } +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) { + return store_bfs(s, start_id, direction, edge_types, edge_type_count, max_depth, max_results, + false, out); +} + +int cbm_store_bfs_trail(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) { + return store_bfs(s, start_id, direction, edge_types, edge_type_count, max_depth, max_results, + true, out); +} + void cbm_store_traverse_free(cbm_traverse_result_t *out) { if (!out) { return; diff --git a/src/store/store.h b/src/store/store.h index ad5072836..9887a345d 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -447,6 +447,11 @@ void cbm_store_search_free(cbm_search_output_t *out); 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); +/* Variable-length Cypher traversal with relationship-trail semantics. */ +int cbm_store_bfs_trail(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); + /* Free a traverse result's allocated memory. */ void cbm_store_traverse_free(cbm_traverse_result_t *out); diff --git a/tests/test_cypher.c b/tests/test_cypher.c index 3ff3886fe..9126f2fa4 100644 --- a/tests/test_cypher.c +++ b/tests/test_cypher.c @@ -559,15 +559,24 @@ TEST(cypher_exec_varlength_path_semantics_issue797) { ASSERT_EQ(r1.row_count, 1); cbm_cypher_result_free(&r1); - /* Bug 2: *2..2 from loopy — only the REAL 2-chain (leaf); the self-loop - * must not be reused to pad paths (relationship uniqueness). */ + /* Bug 2: *2..2 from loopy has two relationship-unique trails: the + * self-loop followed by e1 reaches mid, and e1 followed by e2 reaches leaf. + * Reusing the self-loop within one trail remains forbidden. */ cbm_cypher_result_t r2 = {0}; ASSERT_EQ(cbm_cypher_execute(s, "MATCH (a {name: \"loopy\"})-[:CALLS*2..2]->(b) " "RETURN DISTINCT b.name", "test", 0, &r2), 0); - ASSERT_EQ(r2.row_count, 1); /* leaf only */ + ASSERT_EQ(r2.row_count, 2); + bool saw_mid = false; + bool saw_leaf = false; + for (int i = 0; i < r2.row_count; i++) { + saw_mid |= strcmp(r2.rows[i][0], "mid") == 0; + saw_leaf |= strcmp(r2.rows[i][0], "leaf") == 0; + } + ASSERT_TRUE(saw_mid); + ASSERT_TRUE(saw_leaf); cbm_cypher_result_free(&r2); /* Bug 2 amplifier: no directed path of length 5 exists at all. */ diff --git a/tests/test_store_search.c b/tests/test_store_search.c index e589fc228..646416909 100644 --- a/tests/test_store_search.c +++ b/tests/test_store_search.c @@ -4,6 +4,7 @@ * Ported from internal/store/store_test.go (TestSearch, TestBFS, etc.) */ #include "../src/foundation/compat.h" +#include "../src/foundation/log.h" #include "test_framework.h" #include "test_helpers.h" #include @@ -12,6 +13,15 @@ #include #include +static char trail_log[1024]; + +static void capture_trail_log(const char *line) { + size_t used = strlen(trail_log); + if (used < sizeof(trail_log) - 1) { + snprintf(trail_log + used, sizeof(trail_log) - used, "%s\n", line); + } +} + /* Helper: create a typical graph for search/traversal tests. * * Nodes: SubmitOrder (Function), ProcessOrder (Function), OrderService (Class) @@ -992,7 +1002,7 @@ TEST(store_bfs_with_risk_labels) { PASS(); } -TEST(store_bfs_caps_recursive_cte_rows) { +TEST(store_bfs_reachability_is_not_trail_capped) { cbm_store_t *s = cbm_store_open_memory(); cbm_store_upsert_project(s, "test", "/tmp/test"); @@ -1018,8 +1028,48 @@ TEST(store_bfs_caps_recursive_cte_rows) { 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); + ASSERT_EQ(result.visited_count, NODE_COUNT); + + cbm_store_traverse_free(&result); + cbm_store_close(s); + PASS(); +} + +TEST(store_bfs_trail_warns_when_path_rows_are_truncated) { + cbm_store_t *s = cbm_store_open_memory(); + cbm_store_upsert_project(s, "test", "/tmp/test"); + + int64_t ids[14]; + for (int i = 0; i < 14; i++) { + char name[16]; + snprintf(name, sizeof(name), "node-%d", i); + cbm_node_t node = {.project = "test", + .label = "Function", + .name = name, + .qualified_name = name, + .file_path = "graph.c"}; + ids[i] = cbm_store_upsert_node(s, &node); + } + for (int source = 0; source < 13; source++) { + for (int target = source + 1; target < 14; target++) { + cbm_edge_t edge = {.project = "test", + .source_id = ids[source], + .target_id = ids[target], + .type = "CALLS"}; + cbm_store_insert_edge(s, &edge); + } + } + + trail_log[0] = '\0'; + cbm_log_set_sink(capture_trail_log); + const char *types[] = {"CALLS"}; + cbm_traverse_result_t result = {0}; + int rc = cbm_store_bfs_trail(s, ids[0], "outbound", types, 1, 10, 5000, &result); + cbm_log_set_sink(NULL); + + ASSERT_EQ(rc, CBM_STORE_OK); + ASSERT_TRUE(strstr(trail_log, "cypher.trail_truncated") != NULL); + ASSERT_TRUE(strstr(trail_log, "result=partial") != NULL); cbm_store_traverse_free(&result); cbm_store_close(s); @@ -1522,7 +1572,8 @@ 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_reachability_is_not_trail_capped); + RUN_TEST(store_bfs_trail_warns_when_path_rows_are_truncated); RUN_TEST(store_bfs_cross_service_summary); RUN_TEST(store_glob_to_like); RUN_TEST(store_extract_like_hints);