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:
Andrew Hundt
2026-07-03 18:33:23 -04:00
parent 6b8e3ac22a
commit bcbc95829f
3 changed files with 166 additions and 0 deletions
+62
View File
@@ -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) {
+14
View File
@@ -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,
+90
View File
@@ -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);