Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/cypher/cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
29 changes: 25 additions & 4 deletions src/store/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -2842,14 +2856,20 @@ 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) 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"
" 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 "
Expand All @@ -2858,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);
Expand Down
44 changes: 44 additions & 0 deletions tests/test_cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand Down
36 changes: 36 additions & 0 deletions tests/test_store_search.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "test_framework.h"
#include "test_helpers.h"
#include <store/store.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading