Repository navigation
Description
Version
codebase-memory-mcp 0.9.0
Platform
macOS (Apple Silicon)
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
standard
What happened, and what did you expect?
RETURN DISTINCT type(r) silently reports only a subset of the edge types present in the graph. On an index whose get_graph_schema lists 22 edge types, the query returns 5, with total: 5 and no truncation signal.
MATCH ()-[r]->() RETURN DISTINCT type(r) AS edge_type
Actual:
{"columns":["edge_type"],
"rows":[["CONTAINS_FILE"],["CONTAINS_FOLDER"],["DEFINES"],["CALLS"],["USAGE"]],
"total":5}Expected: the 22 types get_graph_schema reports for the same project.
The omitted types are not empty — they are individually countable in the same session:
| type | present in DISTINCT type(r)? |
count(r) |
|---|---|---|
DEFINES |
yes | 22,386 |
USAGE |
yes | 15,687 |
CALLS |
yes | 10,509 |
IMPORTS |
no | 3,460 |
CONTAINS_FILE |
yes | 2,250 |
DEFINES_METHOD |
no | 1,866 |
DECORATES |
no | 841 |
WRITES |
no | 480 |
CONTAINS_FOLDER |
yes | 338 |
FILE_CHANGES_WITH |
no | 315 |
INHERITS |
no | 297 |
SIMILAR_TO |
no | 294 |
HTTP_CALLS |
no | 194 |
SEMANTICALLY_RELATED |
no | 181 |
HANDLES |
no | 180 |
TESTS_FILE |
no | 126 |
CONFIGURES |
no | 71 |
RAISES |
no | 68 |
TESTS |
no | 35 |
DATA_FLOWS |
no | 14 |
THROWS |
no | 3 |
HAS_BRANCH |
no | 1 |
The 22 counts sum to exactly 59,596, which matches MATCH ()-[r]->() RETURN count(r) and the edges figure from index_status. So the graph is complete and only the DISTINCT projection is wrong. There is no obvious rule behind the five that survive — they are not the top five by count (IMPORTS at 3,460 and DEFINES_METHOD at 1,866 both outrank CONTAINS_FOLDER at 338, which is included).
This appears to be a regression in 0.9.0. A second machine running 0.8.1 against the same source tree returns 21 of 21 edge types correctly for the identical query. Only the 0.9.0 machine truncates.
Why this matters
The failure is silent and the output is well-formed, so it reads as an authoritative answer. Discovering the edge model is exactly what an agent does before writing any query_graph traversal — a caller who trusts this concludes that IMPORTS, HTTP_CALLS, DATA_FLOWS, INHERITS etc. do not exist in the graph and never queries them. In my case it also produced a false diagnosis: I read the truncated list as evidence the extractor had stopped emitting those edge types, when in fact all 22 were present.
Same family as #1292 (grouping key silently returns one wrong row) — a projection that quietly loses rows rather than erroring.
Reproduction
Repo-independent — reproduces on any indexed project:
codebase-memory-mcp cli get_graph_schema --project <slug>
# note the edge_types list and its length
codebase-memory-mcp cli query_graph --project <slug> \
--query "MATCH ()-[r]->() RETURN DISTINCT type(r) AS edge_type"
# returns a strict subset, with no truncation signalCross-check any omitted type individually to confirm it is non-empty:
codebase-memory-mcp cli query_graph --project <slug> \
--query "MATCH ()-[r:IMPORTS]->() RETURN count(r) AS n"Project scale (if relevant)
TypeScript/Swift/SQL monorepo, 2,250 files, 25,269 nodes / 59,596 edges, 22 edge types per get_graph_schema. Reproduced identically through the MCP server and cli mode.
Confirmations
- I searched existing issues and this is not a duplicate.
- The reproduction is repo-independent — it is a schema-level query that runs against any indexed project, and the mismatch is against the tool's own
get_graph_schemaoutput.
Activity
AmirF194 commentedon Jul 30, 2026
Open PR #1323 fixes the mechanism this hits, though not the whole gap at large scale. Detail below.
Your query (
MATCH ()-[r]->()) starts from an unlabeled node pattern, which routes throughscan_pattern_nodes()'s unlabeled branch insrc/cypher/cypher.c. On main (a65faeb4753787add7fd63eb3975b7abc91165d8) that branch callscbm_store_search()withlimit = max_rows * 10, andcbm_store_searchis a ranked/BM25 search path, not a plain scan, so it does not actually return every node even when the numeric limit is far above the node count. #1323 replaces it with a real unboundedcbm_store_find_nodes().I couldn't reproduce the truncation on a same-order-of-magnitude index (qlib, 6.5k nodes: 21/21 came back fine), so I indexed something bigger to get past whatever the search path's effective cutoff is:
run-llama/llama_index@ current default branch, 77,599 nodes / 305,782 edges,mode:full.a65faeb):MATCH ()-[r]->() RETURN DISTINCT type(r)→ 17 of 21 types,total:17, no truncation signal. Matches your symptom exactly.cb9be33452b9969e38c9164c7be4495ae187611b), same index, same query → 20 of 21. Big improvement, butHAS_BRANCH(a single edge) is still missing. Confirmed it's really there and queryable:MATCH (a)-[r]->(b:Branch) RETURN DISTINCT type(r)finds it fine (that pattern's scan starts from the far smaller/labeled side).So there's a second, separate cap on top of the one #1323 fixes.
execute_return_simple()(cypher.c:4344) projects rows withrb->row_count < proj_cap, andproj_capismax_rows, which defaults toCYPHER_RESULT_CEILING(100,000) when unset. That cap runs beforerb_apply_distinct()(called fromexecute_return_clause, right after), so on a big enough match set the DISTINCT step only ever sees the first ~100k raw rows, not the full set, and the "result exceeded 100k rows" guard a few lines down never fires because it checks the row count after dedup. I didn't step through this with a debugger, so treat the exact mechanism as a read, not an executed proof, but it lines up with what I observed: your project (59,596 edges) is comfortably under 100k, so #1323 alone should fully resolve the case in this issue; the residual gap I found only shows up past that ceiling.DeusData commentedon Aug 3, 2026
Thank you for quantifying the 0.8.1 to RC regression. PR #1323 addresses the early unlabeled candidate scan, but the remaining pre-
DISTINCTexecution cap can still hide edge types before aggregation and is not covered by that narrower fix. I have removed the unrelated Windows label and kept the issue open at high priority in0.9.1-rcuntil the full 22-type contract is reproduced and restored.DeusData commentedon Sep 26, 2026
Thank you, @braggintime, for the precise 22-versus-5 table, and @AmirF194 for tracing the scan and measuring the remaining gap on llama_index!
v0.10.7 shipped two fixes: one for the scan that skipped unlabeled nodes, and one that counts every matched row before aggregating. The big loss is gone. On a 150k-edge index,
RETURN DISTINCT type(r)now returns 21 of 22 types, and the response is no longer silent: it reportstruncated: true, truncation_reason: engine_limit.The one missing type (HAS_BRANCH, as predicted) has one remaining cause. Plain
RETURN DISTINCTstops at the engine row limit and only then removes duplicates. We'll move de-duplication ahead of the limit. Until then,MATCH ()-[r]->() WITH type(r) AS t RETURN t, count(*) AS creturns all 22.DeusData commentedon Oct 4, 2026
Thanks again, @braggintime, for the precise 22-versus-5 table, and @AmirF194 for tracing the scan. The last missing type is fixed now.
Plain
RETURN DISTINCTnow removes duplicates while it projects, so the engine row cap and an explicitLIMITboth count distinct rows. A value first seen late in the scan, likeHAS_BRANCH, can no longer fall off the end. On an index whereRETURN DISTINCT type(r)previously returned 22 of 23 edge types withengine_limit, it now returns all 23 withtruncated: false, in the same wall time. TheWITH ... count(*)workaround is no longer needed.Landed on main in 20fd3aa (#2511); it ships with the next release.
fix(cypher): count DISTINCT rows against the row cap (DeusData#1364)