fix(store): report a failed COUNT read instead of returning zero - #2065
fix(store): report a failed COUNT read instead of returning zero#2065kavish-19 wants to merge 2 commits into
Conversation
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
|
Thanks for this — the store half is exactly right. I reproduced the defect on main (a failed COUNT step really does come back as 0), built the PR merged onto current main and ran 15 suites across store, dump, graph-buffer, artifact, incremental, mcp, httpd and index-resilience: 894 passed, 0 failed, and your new test goes red the moment the store change is reverted, so it binds. Lint is clean and the sign-off is in place. One thing keeps me from merging it as "Closes #2012", and it is in the PR description rather than the code: the consumer is not already written for a negative count. The guard you quote lives in int nodes = cbm_store_count_nodes(store, project);
int edges = cbm_store_count_edges(store, project);
...
yyjson_mut_obj_add_str(doc, root, "status", nodes > 0 ? "ready" : "empty");
...
if (nodes == 0) { /* "Project is empty. Re-run index_repository..." hint */ }So after your change the corrupt database from #2012 would answer
Leave |
7dfd2c0 to
3e1a228
Compare
|
You're right, and thank you for checking the consumer rather than taking my word for it. I verified the correction before acting on it: the guard I quoted is at Pushed as a second commit, rebased onto current
Left alone as you asked: Every branch of the new decision, checked directly: The healthy and genuinely-empty paths are unchanged; only a negative count behaves differently. One thing I still cannot do on this machine, so you know what my "verified" covers: |
cbm_store_count_nodes and cbm_store_count_edges treat every sqlite3_step result other than SQLITE_ROW as a count of zero. A read that failed — SQLITE_CORRUPT, SQLITE_BUSY, SQLITE_IOERR — is therefore indistinguishable from a project that genuinely holds no rows, and index_status renders it as the positive assertion status "empty". A user or agent reading that concludes the repository was never indexed and starts a multi-minute re-index, while the corruption itself is never surfaced. Both functions already have an error channel: each returns CBM_STORE_ERR when prepare_cached fails. Only the step result was not reported through it. index_status is already written for that value — it sets degraded on a negative node count and clamps a negative edge count — so the guard existed and simply never fired. Initialise count to CBM_STORE_ERR so a non-row step is reported as a failed read. A successful step still overwrites it with the real count, so the healthy path is unchanged. Closes DeusData#2012 Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
The store change alone does not close DeusData#2012. handle_index_status reads the counts directly and renders status as `nodes > 0 ? "ready" : "empty"`, so a negative count from a failed read still answered "empty" — now with a bare -1 printed as the count, and with no hint at all, because the "Project is empty" branch is guarded on `nodes == 0`. The guard cited earlier lives in build_index_success_response, which is index_repository's response builder, not this path. Treat a negative node or edge count as a failed read: report status "error", suppress the negative numbers rather than emitting them as counts, and replace the re-index hint with one that names the table that could not be read. Closes DeusData#2012 Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
3e1a228 to
73c509f
Compare
|
The index_status change is exactly what was needed — thank you. One CI gate is red: cppcheck (a lint check, our CI runs cppcheck 2.20) flags the new hint selection: src/mcp/mcp.c:6705:30: style: Condition 'nodes<0' is always false [knownConditionTrueFalse]. It is the nested ternary — after the 'nodes < 0 && edges < 0' branch, cppcheck reasons about the remaining cases and rejects the second 'nodes < 0' test. Our rule for linter findings is to refactor rather than suppress: pick the hint with a plain if / else-if / else chain into a 'const char *hint' (both-unreadable, nodes-unreadable, else edges-unreadable), then add it once — or use a single message that names 'the nodes and/or edges table'. Everything else on the run is green. Keep the sign-off and I'll merge on green. |
Fixes #2012.
Symptom
cbm_store_count_nodes()treats everysqlite3_step()result other thanSQLITE_ROWas a count of zero. A read that failed —SQLITE_CORRUPT,SQLITE_BUSY,SQLITE_IOERR— is indistinguishable from a project that genuinely holds no nodes, andindex_statusrenders it as the positive assertionstatus: "empty". A user or agent reading that concludes the repository was never indexed and kicks off a multi-minute re-index, while the corruption is never surfaced.Root cause
There is no
else. Both functions already carry an error channel — each returnsCBM_STORE_ERRwhenprepare_cachedfails — so callers already receive a negative value from this API today. Only the step result was never reported through it.Correction (was wrong in the first version of this description). I originally claimed the consumer was already written for a negative count, quoting a
if (nodes < 0) { degraded = true; ... }guard. That guard is inbuild_index_success_response—index_repository's response builder — not inhandle_index_status. I found it by grep and did not check the enclosing function. Thanks to @DeusData for catching it.handle_index_status(src/mcp/mcp.c) reads the counts directly and renders:So the store change on its own would have left the corrupt database from #2012 answering
nodes: -1, edges: -1, status: "empty"— the same false all-clear, now with a bare-1as the count and no hint at all, sincenodes == 0is false. That is why this PR now carries a second commit.The fix
Two commits, one claim — a failed read must never read as an empty project:
fix(store)—cbm_store_count_nodes/cbm_store_count_edgesinitialisecounttoCBM_STORE_ERR, so a non-row step is reported through the error channel each already uses for a failed prepare. A successful step overwrites it, so the healthy path is unchanged.fix(mcp)—handle_index_statustreats a negative node or edge count as a failed read:status: "error", the negative numbers suppressed rather than emitted as counts, and a hint naming the table that could not be read in place of the "Project is empty" re-index advice.I included
count_edgesbecausehandle_index_statusreads the two as a pair — fixing only the node side would leave a corruptedgestable contributing a bare-1to the same response. The issue's own reproduction shows the pair contradicting itself (nodes: 0besideedges: 8).Not changed:
cbm_store_count_edges_by_typehas the same shape but is not read byindex_status, andcbm_store_count_vectorshas the shape without an existing error channel — giving it one would be a new contract. Both felt like separate changes rather than part of this one; happy to follow up if you want them.Verification
Store half — harness linked against
src/store/store.c, inserting a node then dropping the tables after the statements are cached, so the step fails rather than the prepare:index_status half — every branch of the new decision, checked directly:
The healthy and genuinely-empty paths are untouched; only a negative count changes behaviour.
Tests added:
tests/test_store_nodes.c→store_count_failed_read_is_not_zerotests/test_mcp.c→tool_index_status_reports_an_unreadable_count_as_an_error, using the in-memory server as you suggested: callindex_statusonce (caching the statements), drop the tables, call again, and assertstatus: "error", no negative counts, and a hint naming the table.What I could not run locally, and why
I could not run
scripts/test.shorscripts/lint.shon this machine, and I would rather say so than imply a green run:scripts/build.shfails atinternal/cbm/preprocessor.cppwithfatal error: 'cctype' file not found. The C++ standard-library headers are missing from this machine's Command Line Tools (the SDK contains.../MacOSX.sdk/usr/include/c++/v1/cctype, but clang does not resolve it even with-isysroot). It is a local toolchain fault, unrelated to this change, and it blocks the test runner because that also linkspreprocessor.o.clang-format,clang-tidyandcppcheckare not installed here.What I did instead: both changed files compile clean under the project's own warning set (
-std=c11 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-sign-compare), and I followed the surrounding formatting by hand. CI is the authority on the full suite and the linters. If it flags anything, I will fix it promptly.