From cf1b08fa2be73b0069c4c8fad0029cb51b84a8c0 Mon Sep 17 00:00:00 2001 From: Thomas Dyar Date: Mon, 1 Jun 2026 21:49:44 -0400 Subject: [PATCH] fix(cypher): label-filtered edge traversal silently truncates at 10 results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/cypher/cypher.c | 2 +- tests/test_incremental.c | 50 ++++++++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/cypher/cypher.c b/src/cypher/cypher.c index 02d069e1..3d9a1ab3 100644 --- a/src/cypher/cypher.c +++ b/src/cypher/cypher.c @@ -3673,7 +3673,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"; diff --git a/tests/test_incremental.c b/tests/test_incremental.c index 9e229bba..59bc3e41 100644 --- a/tests/test_incremental.c +++ b/tests/test_incremental.c @@ -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, "{}"); @@ -2824,8 +2856,7 @@ SUITE(incremental) { return; } - int skip_perf = (getenv("CBM_SKIP_PERF") != NULL && - getenv("CBM_SKIP_PERF")[0] != '0' && + int skip_perf = (getenv("CBM_SKIP_PERF") != NULL && getenv("CBM_SKIP_PERF")[0] != '0' && getenv("CBM_SKIP_PERF")[0] != '\0'); /* Phase 1: Full index baseline (needed for tool tests below) */ @@ -3050,6 +3081,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);