fix(cypher): label-filtered edge traversal silently truncates at 10 results

MATCH (c:Class)-[:DEFINES_METHOD]->(m:Method) returned at most 10 results
for any class, regardless of how many methods it actually has.

Root cause: bind_cap was set to scan_count (the number of nodes matched in
the initial pattern — typically 1 when querying a single class by name).
max_new = bind_cap * 10 = 10, so the edge expansion loop exited after
collecting 10 results. No error, no warning, no truncation indicator.

This is language-agnostic: any class with more than 10 methods in any
language was silently truncated. The fix is two characters:
  bind_cap = scan_count > max_rows ? scan_count : max_rows

Regression test: a Python class with 15 methods must return all 15 via
MATCH (c:Class)-[:DEFINES_METHOD]->(m:Method) with label filtering.

Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
This commit is contained in:
Thomas Dyar committed 2026-06-12 14:39:38 -04:00
1 parent e599df1d56
commit c43fc8df0e
2 files changed
+41 -8

No files matched your search

+1 -1
View File
@@ -4201,7 +4201,7 @@ static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *projec
scan_pattern_nodes(store, project, max_rows, &pat0->nodes[0], &scanned, &scan_count);
/* Build initial bindings with early WHERE */
int bind_cap = scan_count > 0 ? scan_count : SKIP_ONE;
int bind_cap = scan_count > max_rows ? scan_count : (max_rows > 0 ? max_rows : SKIP_ONE);
binding_t *bindings = malloc((bind_cap + SKIP_ONE) * sizeof(binding_t));
int bind_count = 0;
const char *var_name = pat0->nodes[0].variable ? pat0->nodes[0].variable : "_n0";
+40 -7
View File
@@ -400,8 +400,8 @@ TEST(incr_modify_file) {
/* Single-file incremental should be faster than full */
if ((int)ms > (int)(g_full_index_ms * 1.5)) {
printf(" [PERF WARNING] incremental slower than 1.5x full: %.0fms vs %.0fms\n",
ms, g_full_index_ms);
printf(" [PERF WARNING] incremental slower than 1.5x full: %.0fms vs %.0fms\n", ms,
g_full_index_ms);
}
printf(" [perf] modify 1 file: %.0fms (full was %.0fms)\n", ms, g_full_index_ms);
@@ -910,12 +910,12 @@ static int resp_lacks_key(const char *resp, const char *key) {
}
/* Helper: assert tool call succeeds, warn if slow */
#define TOOL_OK(resp, ms) \
do { \
ASSERT((resp) != NULL); \
if ((int)(ms) > PERF_WARN_MS) { \
#define TOOL_OK(resp, ms) \
do { \
ASSERT((resp) != NULL); \
if ((int)(ms) > PERF_WARN_MS) { \
printf(" [PERF WARNING] tool call: %.0fms (>%dms)\n", (ms), PERF_WARN_MS); \
} \
} \
} while (0)
/* Helper: assert response is not an error */
@@ -932,6 +932,38 @@ TEST(tool_list_projects_basic) {
PASS();
}
TEST(tool_qg_defines_method_more_than_10) {
write_file_at("fastapi/big_class.py", "class BigClass:\n"
" def m1(self): pass\n"
" def m2(self): pass\n"
" def m3(self): pass\n"
" def m4(self): pass\n"
" def m5(self): pass\n"
" def m6(self): pass\n"
" def m7(self): pass\n"
" def m8(self): pass\n"
" def m9(self): pass\n"
" def m10(self): pass\n"
" def m11(self): pass\n"
" def m12(self): pass\n"
" def m13(self): pass\n"
" def m14(self): pass\n"
" def m15(self): pass\n");
char *idx = index_repo();
ASSERT(idx != NULL);
free(idx);
double ms;
char *r = call_tool_timed("query_graph", &ms,
"{\"project\":\"%s\","
"\"query\":\"MATCH (c:Class)-[:DEFINES_METHOD]->(m:Method)"
" WHERE c.name = 'BigClass' RETURN count(m) AS n\"}",
g_project);
TOOL_OK(r, ms);
ASSERT(strstr(r, "\"15\"") != NULL || strstr(r, "\\\"15\\\"") != NULL);
free(r);
PASS();
}
TEST(tool_list_projects_has_current) {
double ms;
char *r = call_tool_timed("list_projects", &ms, "{}");
@@ -3042,6 +3074,7 @@ SUITE(incremental) {
RUN_TEST(tool_qg_configures);
RUN_TEST(tool_qg_handles);
RUN_TEST(tool_qg_defines_method);
RUN_TEST(tool_qg_defines_method_more_than_10);
RUN_TEST(tool_qg_no_limit);
RUN_TEST(tool_qg_empty_result);