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

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

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

    cypherCypher query language parser/executor bugs

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions