Skip to content

cbm_store_count_nodes returns 0 for a failed COUNT(*), so index_status reports a corrupt project as nodes: 0, status: "empty" #2012

Closed

Description

@metehanulusoy

What happens

cbm_store_count_nodes() treats 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 nodes, and index_status renders it as the positive assertion status: "empty".

Reproduction

Index any repository, then damage the nodes b-tree so the count cannot be read:

DB=~/.cache/codebase-memory-mcp/<project>.db      # adjust to your cache dir
sqlite3 "$DB" "PRAGMA wal_checkpoint(TRUNCATE);"
python3 - <<'PY'
import sqlite3
db = "<path to db>"
con = sqlite3.connect(db)
pages = [r[0] for r in con.execute(
    "SELECT rootpage FROM sqlite_master WHERE tbl_name='nodes' AND rootpage>0")]
psize = con.execute("PRAGMA page_size").fetchone()[0]
con.close()
with open(db, "r+b") as f:
    for p in pages:
        f.seek((p - 1) * psize); f.write(b"\xff" * psize)
PY
sqlite3 "$DB" "SELECT COUNT(*) FROM nodes;"
# Error: stepping, database disk image is malformed (11)

index_status on that project answers, with isError: false:

{"project":"…","nodes":0,"edges":8,"status":"empty", …}

Before the damage the same project reported "nodes":8,"edges":8,"status":"ready".

Note nodes: 0 alongside edges: 8. A project with no nodes cannot have edges, so the response contradicts itself — that inconsistency is the only signal that anything went wrong, and nothing acts on it.

The user or agent reading this concludes the repository was never indexed. The likely next step is a full multi-minute index_repository, or abandoning graph queries against a graph that is actually damaged and needs a rebuild for a completely different reason. The corruption itself is never surfaced.

Note that a corrupt nodes page alone is not enough to trigger it: SQLite can still satisfy COUNT(*) from an index, so the reproducer damages the table's index root pages too. That also means a real-world partial corruption may surface here rather than in a query that touches the table.

Mechanism

src/store/store.c:2870:

int cbm_store_count_nodes(cbm_store_t *s, const char *project) {
    ...
    int count = 0;
    if (sqlite3_step(stmt) == SQLITE_ROW) {
        count = sqlite3_column_int(stmt, 0);
    }
    sqlite3_reset(stmt);
    return count;
}

There is no else. The function already has an error channel — it returns CBM_STORE_ERR when prepare_cached fails a few lines above — so the caller can already tell a failure from a count; only the step result is not reported through it.

src/mcp/mcp.c:5101:

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");

Suggested fix

Return CBM_STORE_ERR from cbm_store_count_nodes when the step is neither SQLITE_ROW nor SQLITE_DONE, recording the sqlite error through store_set_error_sqlite the way cbm_store_delete_nodes_by_project directly below already does, and have index_status surface that as a tool error instead of "empty". cbm_store_count_edges reads the same way and should move with it — otherwise the pair can keep disagreeing.

Happy to open the PR if you want it; flagging first since it changes what an MCP tool returns on a failure path.

Environment

Activity

  1. added
    bugSomething isn't working
    ux/behaviorDisplay bugs, docs, adoption UX
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    and removed
    cypherCypher query language parser/executor bugs
    on Sep 3, 2026
  2. added 4 commits that reference this issue on Sep 5, 2026
    32a485d
    3e1a228
    99c3bb4
    73c509f
  3. DeusData commented on Sep 19, 2026

    @DeusData
    Owner

    Thank you for tracing the misleading empty result. Current main's store count helper initializes the count to zero and returns it when sqlite3_step does not return a row; index_status then maps a nonpositive node count to empty. A prepare failure already returns an error sentinel, so the correction must address both count-error propagation and status handling while preserving a genuinely empty successful count. PR #2065 covers this area and remains pending review. No additional reporter information is needed to begin that work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.ux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions