Skip to content
Draft
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
27 changes: 5 additions & 22 deletions src/cypher/cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -3013,31 +3013,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) {
Expand Down Expand Up @@ -4593,7 +4576,7 @@ static void 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);
if (patn->rel_count == 0) {
cross_join_nodes(bindings, bind_count, extra_nodes, extra_count, nvar, opt);
} else {
Expand Down Expand Up @@ -4640,7 +4623,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);
Expand Down
19 changes: 18 additions & 1 deletion src/store/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,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;
Expand Down Expand Up @@ -1019,6 +1020,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);
Expand Down Expand Up @@ -1708,7 +1710,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;
Expand Down Expand Up @@ -1736,6 +1740,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) {
Expand Down
3 changes: 3 additions & 0 deletions src/store/store.h
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,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);
Expand Down
41 changes: 41 additions & 0 deletions tests/test_cypher.c
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,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
Expand Down Expand Up @@ -3201,6 +3241,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);
Expand Down
Loading