Merge pull request #1323 from Enferlain/fix/1196-unlabeled-candidate-limit

fix(cypher): scan all unlabeled query candidates
This commit is contained in:
Martin Vogel
2026-08-18 00:05:18 +02:00
committed by GitHub
4 changed files with 67 additions and 23 deletions
+5 -22
View File
@@ -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, &params, &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
View File
@@ -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) {
+3
View File
@@ -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);
+41
View File
@@ -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);