mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-03 08:03:38 +08:00
Merge pull request #1323 from Enferlain/fix/1196-unlabeled-candidate-limit
fix(cypher): scan all unlabeled query candidates
This commit is contained in:
+5
-22
@@ -3071,31 +3071,14 @@ static void scan_alternation_labels(cbm_store_t *store, const char *project, con
|
||||
free(copy);
|
||||
}
|
||||
|
||||
static void scan_pattern_nodes(cbm_store_t *store, const char *project, int max_rows,
|
||||
cbm_node_pattern_t *first, cbm_node_t **out_nodes, int *out_count) {
|
||||
static void scan_pattern_nodes(cbm_store_t *store, const char *project, cbm_node_pattern_t *first,
|
||||
cbm_node_t **out_nodes, int *out_count) {
|
||||
if (first->label && strchr(first->label, '|')) {
|
||||
scan_alternation_labels(store, project, first->label, out_nodes, out_count);
|
||||
} else if (first->label) {
|
||||
cbm_store_find_nodes_by_label(store, project, first->label, out_nodes, out_count);
|
||||
} else {
|
||||
cbm_search_params_t params = {.project = project,
|
||||
.min_degree = CYP_FOUND_NONE,
|
||||
.max_degree = CYP_FOUND_NONE,
|
||||
.limit = max_rows * CYP_GROWTH_10};
|
||||
cbm_search_output_t sout = {0};
|
||||
cbm_store_search(store, ¶ms, &sout);
|
||||
*out_count = sout.count;
|
||||
*out_nodes = malloc(sout.count * sizeof(cbm_node_t));
|
||||
for (int i = 0; i < sout.count; i++) {
|
||||
(*out_nodes)[i] = sout.results[i].node;
|
||||
sout.results[i].node.name = NULL;
|
||||
sout.results[i].node.project = NULL;
|
||||
sout.results[i].node.label = NULL;
|
||||
sout.results[i].node.qualified_name = NULL;
|
||||
sout.results[i].node.file_path = NULL;
|
||||
sout.results[i].node.properties_json = NULL;
|
||||
}
|
||||
cbm_store_search_free(&sout);
|
||||
cbm_store_find_nodes(store, project, out_nodes, out_count);
|
||||
}
|
||||
/* Apply inline property filters — free rejected nodes' strings */
|
||||
if (first->prop_count > 0) {
|
||||
@@ -4779,7 +4762,7 @@ static int expand_additional_patterns(cbm_store_t *store, cbm_query_t *q, const
|
||||
|
||||
cbm_node_t *extra_nodes = NULL;
|
||||
int extra_count = 0;
|
||||
scan_pattern_nodes(store, project, max_rows, &patn->nodes[0], &extra_nodes, &extra_count);
|
||||
scan_pattern_nodes(store, project, &patn->nodes[0], &extra_nodes, &extra_count);
|
||||
int rc = 0;
|
||||
if (patn->rel_count == 0) {
|
||||
rc = cross_join_nodes(bindings, bind_count, extra_nodes, extra_count, nvar, opt);
|
||||
@@ -4831,7 +4814,7 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec
|
||||
/* Step 1: Scan initial nodes */
|
||||
cbm_node_t *scanned = NULL;
|
||||
int scan_count = 0;
|
||||
scan_pattern_nodes(store, project, max_rows, &pat0->nodes[0], &scanned, &scan_count);
|
||||
scan_pattern_nodes(store, project, &pat0->nodes[0], &scanned, &scan_count);
|
||||
|
||||
/* Build initial bindings with early WHERE */
|
||||
int bind_cap = scan_count > max_rows ? scan_count : (max_rows > 0 ? max_rows : SKIP_ONE);
|
||||
|
||||
+18
-1
@@ -117,6 +117,7 @@ struct cbm_store {
|
||||
sqlite3_stmt *stmt_find_node_by_id;
|
||||
sqlite3_stmt *stmt_find_node_by_qn;
|
||||
sqlite3_stmt *stmt_find_node_by_qn_any; /* QN lookup without project filter */
|
||||
sqlite3_stmt *stmt_find_nodes;
|
||||
sqlite3_stmt *stmt_find_nodes_by_name;
|
||||
sqlite3_stmt *stmt_find_nodes_by_name_any; /* name lookup without project filter */
|
||||
sqlite3_stmt *stmt_find_nodes_by_label;
|
||||
@@ -1140,6 +1141,7 @@ void cbm_store_close(cbm_store_t *s) {
|
||||
finalize_stmt(&s->stmt_find_node_by_id);
|
||||
finalize_stmt(&s->stmt_find_node_by_qn);
|
||||
finalize_stmt(&s->stmt_find_node_by_qn_any);
|
||||
finalize_stmt(&s->stmt_find_nodes);
|
||||
finalize_stmt(&s->stmt_find_nodes_by_name);
|
||||
finalize_stmt(&s->stmt_find_nodes_by_name_any);
|
||||
finalize_stmt(&s->stmt_find_nodes_by_label);
|
||||
@@ -1978,7 +1980,9 @@ static int find_nodes_generic(cbm_store_t *s, sqlite3_stmt **slot, const char *s
|
||||
}
|
||||
|
||||
bind_text(stmt, SKIP_ONE, project);
|
||||
bind_text(stmt, ST_COL_2, val);
|
||||
if (val) {
|
||||
bind_text(stmt, ST_COL_2, val);
|
||||
}
|
||||
|
||||
int cap = ST_INIT_CAP_16;
|
||||
int n = 0;
|
||||
@@ -2006,6 +2010,19 @@ static int find_nodes_generic(cbm_store_t *s, sqlite3_stmt **slot, const char *s
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
int cbm_store_find_nodes(cbm_store_t *s, const char *project, cbm_node_t **out, int *count) {
|
||||
if (!s) {
|
||||
*out = NULL;
|
||||
*count = 0;
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
return find_nodes_generic(s, &s->stmt_find_nodes,
|
||||
"SELECT id, project, label, name, qualified_name, file_path, "
|
||||
"start_line, end_line, properties FROM nodes "
|
||||
"WHERE project = ?1;",
|
||||
project, NULL, out, count);
|
||||
}
|
||||
|
||||
int cbm_store_find_nodes_by_name(cbm_store_t *s, const char *project, const char *name,
|
||||
cbm_node_t **out, int *count) {
|
||||
if (!s) {
|
||||
|
||||
@@ -403,6 +403,9 @@ int cbm_store_find_node_by_qn(cbm_store_t *s, const char *project, const char *q
|
||||
/* Find node by qualified_name only (no project filter — QNs are globally unique). */
|
||||
int cbm_store_find_node_by_qn_any(cbm_store_t *s, const char *qn, cbm_node_t *out);
|
||||
|
||||
/* Find all nodes in a project. Returns allocated array, caller frees. */
|
||||
int cbm_store_find_nodes(cbm_store_t *s, const char *project, cbm_node_t **out, int *count);
|
||||
|
||||
/* Find nodes by name (exact match). Returns allocated array, caller frees. */
|
||||
int cbm_store_find_nodes_by_name(cbm_store_t *s, const char *project, const char *name,
|
||||
cbm_node_t **out, int *count);
|
||||
|
||||
@@ -1005,6 +1005,46 @@ TEST(cypher_exec_where_eq) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #1196: max_rows limits projected results, not the source-node candidates
|
||||
* considered before WHERE. The old unlabeled scan searched only
|
||||
* 10 * max_rows nodes, so a valid match later in search order disappeared. */
|
||||
TEST(cypher_exec_unlabeled_where_beyond_result_limit_issue1196) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
|
||||
|
||||
for (int i = 0; i < 11; i++) {
|
||||
char name[32];
|
||||
char qn[64];
|
||||
snprintf(name, sizeof(name), "early_%02d", i);
|
||||
snprintf(qn, sizeof(qn), "test.%s", name);
|
||||
cbm_node_t distractor = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = name,
|
||||
.qualified_name = qn,
|
||||
.file_path = "early.py"};
|
||||
ASSERT_GT(cbm_store_upsert_node(s, &distractor), 0);
|
||||
}
|
||||
|
||||
cbm_node_t late = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "zz_late_match",
|
||||
.qualified_name = "test.zz_late_match",
|
||||
.file_path = "late.py"};
|
||||
ASSERT_GT(cbm_store_upsert_node(s, &late), 0);
|
||||
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(
|
||||
s, "MATCH (n) WHERE n.name = \"zz_late_match\" RETURN n.name", "test", 1, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "zz_late_match");
|
||||
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #874: coalesce(var.prop, literal) in WHERE — null-safe numeric filters
|
||||
* for audit queries over OPTIONAL graph properties. The parser rejected the
|
||||
* call outright ("unexpected operator"); RETURN-side coalesce already
|
||||
@@ -3897,6 +3937,7 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_issue252_tointeger);
|
||||
RUN_TEST(cypher_issue305_count_star_alias);
|
||||
RUN_TEST(cypher_exec_where_eq);
|
||||
RUN_TEST(cypher_exec_unlabeled_where_beyond_result_limit_issue1196);
|
||||
RUN_TEST(cypher_exec_varlength_path_semantics_issue797);
|
||||
RUN_TEST(cypher_exec_where_coalesce_issue874);
|
||||
RUN_TEST(cypher_exec_where_regex);
|
||||
|
||||
Reference in New Issue
Block a user