mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-09 09:11:00 +08:00
feat(store): summarize overlay node read view
Add a read-only overlay node view summary that computes canonical visible nodes plus owned nodes from the latest ready overlay per file. This is the first tested base-minus-tombstone-plus-overlay read primitive and does not alter existing query behavior. Validation: CBM_ONLY_SUITE=store_nodes make -f Makefile.cbm -j8 test (89 passed); CBM_ONLY_SUITE=sqlite_writer make -f Makefile.cbm -j8 test (10 passed); make -f Makefile.cbm -j8 cbm; bash scripts/check-source-safety.sh; git diff --check. Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
@@ -4926,6 +4926,68 @@ int cbm_store_publish_overlay_file_delta(cbm_store_t *s,
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
int cbm_store_get_overlay_node_view_summary(cbm_store_t *s, const char *project,
|
||||
cbm_store_overlay_node_view_summary_t *out) {
|
||||
if (out) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
}
|
||||
if (!s || !s->db || !project || !project[0] || !out) {
|
||||
if (s) {
|
||||
store_set_error(s, "overlay_node_view_summary: invalid argument");
|
||||
}
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
static const char sql[] =
|
||||
"WITH active_files AS ("
|
||||
" SELECT t.rel_path, MAX(t.overlay_generation) AS overlay_generation"
|
||||
" FROM overlay_tombstones t"
|
||||
" JOIN overlay_generations g"
|
||||
" ON g.project = t.project AND g.overlay_generation = t.overlay_generation"
|
||||
" WHERE t.project = ?1 AND g.status = ?2 AND t.entity_kind = ?3 AND t.active = ?4"
|
||||
" GROUP BY t.rel_path"
|
||||
")"
|
||||
"SELECT "
|
||||
" (SELECT COUNT(*) FROM overlay_generations g"
|
||||
" WHERE g.project = ?1 AND g.status = ?2) AS ready_generations,"
|
||||
" (SELECT COUNT(*) FROM active_files) AS active_files,"
|
||||
" (SELECT COUNT(*) FROM nodes n"
|
||||
" WHERE n.project = ?1"
|
||||
" AND NOT EXISTS (SELECT 1 FROM active_files af"
|
||||
" WHERE af.rel_path = n.file_path)) AS canonical_visible,"
|
||||
" (SELECT COUNT(*) FROM overlay_nodes n"
|
||||
" JOIN active_files af"
|
||||
" ON af.rel_path = n.rel_path AND af.overlay_generation = n.overlay_generation"
|
||||
" WHERE n.project = ?1 AND n.owned = ?5) AS overlay_visible;";
|
||||
|
||||
sqlite3_stmt *stmt = NULL;
|
||||
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
|
||||
store_set_error_sqlite(s, "overlay_node_view_summary prepare");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
bind_text(stmt, ST_COL_1, project);
|
||||
bind_text(stmt, ST_COL_2, CBM_STORE_OVERLAY_STATUS_READY);
|
||||
bind_text(stmt, ST_COL_3, CBM_STORE_OVERLAY_TOMBSTONE_FILE);
|
||||
sqlite3_bind_int(stmt, ST_COL_4, STORE_OVERLAY_TOMBSTONE_ACTIVE);
|
||||
sqlite3_bind_int(stmt, ST_COL_5, STORE_OVERLAY_ROW_OWNED);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_ROW) {
|
||||
sqlite3_finalize(stmt);
|
||||
store_set_error_sqlite(s, "overlay_node_view_summary");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
|
||||
out->overlay_ready_generations = sqlite3_column_int(stmt, 0);
|
||||
out->active_file_tombstones = sqlite3_column_int(stmt, 1);
|
||||
out->canonical_nodes_visible = sqlite3_column_int(stmt, 2);
|
||||
out->overlay_owned_nodes_visible = sqlite3_column_int(stmt, 3);
|
||||
out->total_nodes_visible = out->canonical_nodes_visible + out->overlay_owned_nodes_visible;
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
static bool store_file_delta_batch_shape_valid(const cbm_store_file_delta_t *const *deltas,
|
||||
int delta_count, const char **out_project,
|
||||
int64_t *out_generation) {
|
||||
|
||||
@@ -669,6 +669,20 @@ int cbm_store_publish_overlay_file_delta(cbm_store_t *s,
|
||||
const cbm_store_file_delta_t *delta,
|
||||
int64_t overlay_generation);
|
||||
|
||||
typedef struct {
|
||||
int overlay_ready_generations;
|
||||
int active_file_tombstones;
|
||||
int canonical_nodes_visible;
|
||||
int overlay_owned_nodes_visible;
|
||||
int total_nodes_visible;
|
||||
} cbm_store_overlay_node_view_summary_t;
|
||||
|
||||
/* Summarize the current node read view as canonical nodes minus files with a
|
||||
* ready overlay tombstone plus owned nodes from the latest ready overlay per file.
|
||||
* This is a read-only helper; it does not change canonical query behavior. */
|
||||
int cbm_store_get_overlay_node_view_summary(cbm_store_t *s, const char *project,
|
||||
cbm_store_overlay_node_view_summary_t *out);
|
||||
|
||||
/* Record derived-view freshness. Status must be one of CBM_STORE_DERIVED_STATUS_*. */
|
||||
int cbm_store_set_derived_view_state(cbm_store_t *s, const char *project,
|
||||
const char *view_name, int64_t generation,
|
||||
|
||||
@@ -1390,6 +1390,95 @@ TEST(store_overlay_file_delta_publish_rejects_failed_generation) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_overlay_node_view_summary_counts_latest_ready_overlay) {
|
||||
enum { BASE_GENERATION = 1 };
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
ASSERT_EQ(cbm_store_upsert_project(s, "test", "/tmp/test"), CBM_STORE_OK);
|
||||
|
||||
cbm_node_t old_main = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "old_main",
|
||||
.qualified_name = "test.old_main",
|
||||
.file_path = "main.go",
|
||||
.properties_json = "{}"};
|
||||
cbm_node_t stable = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "stable",
|
||||
.qualified_name = "test.stable",
|
||||
.file_path = "stable.go",
|
||||
.properties_json = "{}"};
|
||||
ASSERT_GT(cbm_store_upsert_node(s, &old_main), 0);
|
||||
ASSERT_GT(cbm_store_upsert_node(s, &stable), 0);
|
||||
|
||||
cbm_store_overlay_node_view_summary_t summary = {0};
|
||||
ASSERT_EQ(cbm_store_get_overlay_node_view_summary(s, "test", &summary), CBM_STORE_OK);
|
||||
ASSERT_EQ(summary.overlay_ready_generations, 0);
|
||||
ASSERT_EQ(summary.active_file_tombstones, 0);
|
||||
ASSERT_EQ(summary.canonical_nodes_visible, 2);
|
||||
ASSERT_EQ(summary.overlay_owned_nodes_visible, 0);
|
||||
ASSERT_EQ(summary.total_nodes_visible, 2);
|
||||
|
||||
int64_t first_overlay = 0;
|
||||
ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION, &first_overlay),
|
||||
CBM_STORE_OK);
|
||||
cbm_node_t first_nodes[] = {
|
||||
{.project = "test",
|
||||
.label = "Function",
|
||||
.name = "new_main",
|
||||
.qualified_name = "test.new_main",
|
||||
.file_path = "main.go",
|
||||
.properties_json = "{}"},
|
||||
{.project = "test",
|
||||
.label = "Function",
|
||||
.name = "new_helper",
|
||||
.qualified_name = "test.new_helper",
|
||||
.file_path = "main.go",
|
||||
.properties_json = "{}"},
|
||||
};
|
||||
cbm_store_file_delta_t first_delta = {.project = "test",
|
||||
.rel_path = "main.go",
|
||||
.generation = BASE_GENERATION,
|
||||
.nodes = first_nodes,
|
||||
.node_count = 2};
|
||||
ASSERT_EQ(cbm_store_publish_overlay_file_delta(s, &first_delta, first_overlay),
|
||||
CBM_STORE_OK);
|
||||
|
||||
ASSERT_EQ(cbm_store_get_overlay_node_view_summary(s, "test", &summary), CBM_STORE_OK);
|
||||
ASSERT_EQ(summary.overlay_ready_generations, 1);
|
||||
ASSERT_EQ(summary.active_file_tombstones, 1);
|
||||
ASSERT_EQ(summary.canonical_nodes_visible, 1);
|
||||
ASSERT_EQ(summary.overlay_owned_nodes_visible, 2);
|
||||
ASSERT_EQ(summary.total_nodes_visible, 3);
|
||||
|
||||
int64_t second_overlay = 0;
|
||||
ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION, &second_overlay),
|
||||
CBM_STORE_OK);
|
||||
cbm_node_t second_node = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "newer_main",
|
||||
.qualified_name = "test.newer_main",
|
||||
.file_path = "main.go",
|
||||
.properties_json = "{}"};
|
||||
cbm_store_file_delta_t second_delta = {.project = "test",
|
||||
.rel_path = "main.go",
|
||||
.generation = BASE_GENERATION,
|
||||
.nodes = &second_node,
|
||||
.node_count = 1};
|
||||
ASSERT_EQ(cbm_store_publish_overlay_file_delta(s, &second_delta, second_overlay),
|
||||
CBM_STORE_OK);
|
||||
|
||||
ASSERT_EQ(cbm_store_get_overlay_node_view_summary(s, "test", &summary), CBM_STORE_OK);
|
||||
ASSERT_EQ(summary.overlay_ready_generations, 2);
|
||||
ASSERT_EQ(summary.active_file_tombstones, 1);
|
||||
ASSERT_EQ(summary.canonical_nodes_visible, 1);
|
||||
ASSERT_EQ(summary.overlay_owned_nodes_visible, 1);
|
||||
ASSERT_EQ(summary.total_nodes_visible, 2);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_owner_metadata_crud) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
@@ -4125,6 +4214,7 @@ SUITE(store_nodes) {
|
||||
RUN_TEST(store_overlay_file_delta_publish_rows_and_tombstone);
|
||||
RUN_TEST(store_overlay_file_delta_publish_rejects_invalid_delta_without_rows);
|
||||
RUN_TEST(store_overlay_file_delta_publish_rejects_failed_generation);
|
||||
RUN_TEST(store_overlay_node_view_summary_counts_latest_ready_overlay);
|
||||
RUN_TEST(store_owner_metadata_crud);
|
||||
RUN_TEST(store_rebuild_file_delta_owners_derives_from_graph);
|
||||
RUN_TEST(store_import_export_metadata_crud);
|
||||
|
||||
Reference in New Issue
Block a user