fix(pipeline): seed delta ownership after full index

Normal full indexing persisted file_state for future incremental runs but did not populate node_owners or edge_owners, so the exact-delta planner always failed its existing-ownership precondition on real stores. Add a set-based SQLite rebuild helper and call it only when incremental_reindex is enabled, preserving default full-index overhead.

Add store and pipeline regressions covering graph-derived owner rows and the enabled full-index path. Validation: git diff --check; scripts/check-source-safety.sh; make -j8 -f Makefile.cbm build/c/test-runner; CBM_ONLY_SUITE=store_nodes ./build/c/test-runner; CBM_ONLY_SUITE=pipeline ./build/c/test-runner; CBM_ONLY_SUITE=incremental ./build/c/test-runner.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-06-30 23:12:23 -04:00
parent 4d60f86ecb
commit d70b4d2df7
5 changed files with 236 additions and 0 deletions
+11
View File
@@ -1391,6 +1391,17 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
rc = state_rc;
goto cleanup;
}
if (p->incremental_reindex != CBM_INCREMENTAL_REINDEX_OFF) {
int owner_rc = cbm_store_rebuild_file_delta_owners(
hash_store, p->project_name, CBM_PIPELINE_COMPAT_GENERATION);
if (owner_rc != CBM_STORE_OK) {
cbm_log_error("pipeline.err", "phase", "rebuild_file_delta_owners", "rc",
itoa_buf(owner_rc));
cbm_store_close(hash_store);
rc = owner_rc;
goto cleanup;
}
}
cbm_store_close(hash_store);
cbm_log_info("pass.timing", "pass", "persist_hashes", "files",
itoa_buf(file_count));
+106
View File
@@ -2099,6 +2099,112 @@ int cbm_store_upsert_edge_owner(cbm_store_t *s, const char *project, int64_t edg
return CBM_STORE_OK;
}
static int store_exec_rebuild_owner_sql(cbm_store_t *s, const char *sql, const char *project,
int64_t generation, const char *op) {
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, op);
return CBM_STORE_ERR;
}
bind_text(stmt, ST_COL_1, project);
sqlite3_bind_int64(stmt, ST_COL_2, generation);
if (sqlite3_step(stmt) != SQLITE_DONE) {
store_set_error_sqlite(s, op);
sqlite3_finalize(stmt);
return CBM_STORE_ERR;
}
sqlite3_finalize(stmt);
return CBM_STORE_OK;
}
int cbm_store_rebuild_file_delta_owners(cbm_store_t *s, const char *project,
int64_t generation) {
if (!s || !project || !project[0] || generation < 0) {
if (s) {
store_set_error(s, "rebuild_file_delta_owners: invalid argument");
}
return CBM_STORE_ERR;
}
int rc = cbm_store_begin(s);
if (rc != CBM_STORE_OK) {
return rc;
}
sqlite3_stmt *delete_nodes = NULL;
sqlite3_stmt *delete_edges = NULL;
const char *delete_node_sql = "DELETE FROM node_owners WHERE project = ?1;";
const char *delete_edge_sql = "DELETE FROM edge_owners WHERE project = ?1;";
if (sqlite3_prepare_v2(s->db, delete_node_sql, CBM_NOT_FOUND, &delete_nodes, NULL) !=
SQLITE_OK ||
sqlite3_prepare_v2(s->db, delete_edge_sql, CBM_NOT_FOUND, &delete_edges, NULL) !=
SQLITE_OK) {
store_set_error_sqlite(s, "rebuild_file_delta_owners delete prepare");
sqlite3_finalize(delete_nodes);
sqlite3_finalize(delete_edges);
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
bind_text(delete_nodes, ST_COL_1, project);
bind_text(delete_edges, ST_COL_1, project);
if (sqlite3_step(delete_nodes) != SQLITE_DONE || sqlite3_step(delete_edges) != SQLITE_DONE) {
store_set_error_sqlite(s, "rebuild_file_delta_owners delete");
sqlite3_finalize(delete_nodes);
sqlite3_finalize(delete_edges);
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
sqlite3_finalize(delete_nodes);
sqlite3_finalize(delete_edges);
const char *node_sql =
"INSERT INTO node_owners (project, node_id, rel_path, generation) "
"SELECT project, id, file_path, ?2 FROM nodes "
"WHERE project = ?1 AND file_path IS NOT NULL AND file_path <> '';";
rc = store_exec_rebuild_owner_sql(s, node_sql, project, generation,
"rebuild_file_delta_owners nodes");
if (rc != CBM_STORE_OK) {
(void)cbm_store_rollback(s);
return rc;
}
const char *edge_sql =
"INSERT INTO edge_owners (project, edge_id, rel_path, derived_kind, generation) "
"SELECT e.project, e.id, "
"CASE WHEN src.file_path IS NOT NULL AND src.file_path <> '' THEN src.file_path "
"ELSE tgt.file_path END, ?3, ?2 "
"FROM edges e "
"JOIN nodes src ON src.id = e.source_id "
"JOIN nodes tgt ON tgt.id = e.target_id "
"WHERE e.project = ?1 AND ("
"(src.file_path IS NOT NULL AND src.file_path <> '') OR "
"(tgt.file_path IS NOT NULL AND tgt.file_path <> ''));";
sqlite3_stmt *edge_stmt = NULL;
if (sqlite3_prepare_v2(s->db, edge_sql, CBM_NOT_FOUND, &edge_stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "rebuild_file_delta_owners edges prepare");
sqlite3_finalize(edge_stmt);
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
bind_text(edge_stmt, ST_COL_1, project);
sqlite3_bind_int64(edge_stmt, ST_COL_2, generation);
bind_text(edge_stmt, ST_COL_3, CBM_STORE_DERIVED_KIND_DIRECT);
if (sqlite3_step(edge_stmt) != SQLITE_DONE) {
store_set_error_sqlite(s, "rebuild_file_delta_owners edges");
sqlite3_finalize(edge_stmt);
(void)cbm_store_rollback(s);
return CBM_STORE_ERR;
}
sqlite3_finalize(edge_stmt);
rc = cbm_store_commit(s);
if (rc != CBM_STORE_OK) {
(void)cbm_store_rollback(s);
return rc;
}
return CBM_STORE_OK;
}
int cbm_store_delete_node_owners_by_file(cbm_store_t *s, const char *project,
const char *rel_path) {
sqlite3_stmt *stmt =
+5
View File
@@ -510,6 +510,11 @@ int cbm_store_upsert_edge_owner(cbm_store_t *s, const char *project, int64_t edg
const char *rel_path, const char *derived_kind,
int64_t generation);
/* Rebuild file-delta owner rows from the persisted graph for one project.
* Used after a full index when exact incremental reindexing is enabled. */
int cbm_store_rebuild_file_delta_owners(cbm_store_t *s, const char *project,
int64_t generation);
int cbm_store_delete_node_owners_by_file(cbm_store_t *s, const char *project,
const char *rel_path);
+35
View File
@@ -997,6 +997,40 @@ TEST(pipeline_full_and_incremental_persist_file_state) {
PASS();
}
TEST(pipeline_incremental_full_index_rebuilds_owner_metadata) {
if (setup_test_repo() != 0) {
FAIL("failed to create temp dir");
}
char db_path[512];
int n = snprintf(db_path, sizeof(db_path), "%s/test_owner_metadata.db", g_tmpdir);
ASSERT_GT(n, 0);
ASSERT_LT((size_t)n, sizeof(db_path));
cbm_config_t *cfg = incremental_test_config(g_tmpdir);
ASSERT_NOT_NULL(cfg);
cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int node_owners = 0;
int edge_owners = 0;
ASSERT_EQ(cbm_store_count_file_delta_owners(s, project, "pkg/util/helper.go",
&node_owners, &edge_owners),
CBM_STORE_OK);
ASSERT_GT(node_owners, 0);
cbm_store_close(s);
cbm_pipeline_free(p);
cbm_config_close(cfg);
teardown_test_repo();
PASS();
}
/* ── Git history pass tests ─────────────────────────────────────── */
TEST(githistory_is_trackable) {
@@ -9036,6 +9070,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_calls_resolution);
RUN_TEST(pipeline_incremental_preserves_cross_file_calls);
RUN_TEST(pipeline_full_and_incremental_persist_file_state);
RUN_TEST(pipeline_incremental_full_index_rebuilds_owner_metadata);
/* Git history pass */
RUN_TEST(githistory_is_trackable);
RUN_TEST(githistory_compute_coupling);
+79
View File
@@ -924,6 +924,84 @@ TEST(store_owner_metadata_crud) {
PASS();
}
TEST(store_rebuild_file_delta_owners_derives_from_graph) {
enum { TEST_GENERATION = 7 };
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 nodes[3] = {
{.project = "test",
.label = "Function",
.name = "main",
.qualified_name = "test.main",
.file_path = "main.go",
.properties_json = "{}"},
{.project = "test",
.label = "Function",
.name = "helper",
.qualified_name = "test.helper",
.file_path = "helper.go",
.properties_json = "{}"},
{.project = "test",
.label = "Package",
.name = "pkg",
.qualified_name = "test.pkg",
.file_path = "",
.properties_json = "{}"},
};
int64_t main_id = cbm_store_upsert_node(s, &nodes[0]);
int64_t helper_id = cbm_store_upsert_node(s, &nodes[1]);
int64_t package_id = cbm_store_upsert_node(s, &nodes[2]);
ASSERT_GT(main_id, 0);
ASSERT_GT(helper_id, 0);
ASSERT_GT(package_id, 0);
cbm_edge_t direct_edge = {.project = "test",
.source_id = main_id,
.target_id = helper_id,
.type = "CALLS",
.properties_json = "{}"};
cbm_edge_t target_fallback_edge = {.project = "test",
.source_id = package_id,
.target_id = helper_id,
.type = "CONTAINS",
.properties_json = "{}"};
int64_t direct_edge_id = cbm_store_insert_edge(s, &direct_edge);
int64_t fallback_edge_id = cbm_store_insert_edge(s, &target_fallback_edge);
ASSERT_GT(direct_edge_id, 0);
ASSERT_GT(fallback_edge_id, 0);
ASSERT_EQ(cbm_store_upsert_node_owner(s, "test", main_id, "stale.go", TEST_GENERATION - 1),
CBM_STORE_OK);
ASSERT_EQ(cbm_store_upsert_edge_owner(s, "test", direct_edge_id, "stale.go", NULL,
TEST_GENERATION - 1),
CBM_STORE_OK);
ASSERT_EQ(store_count_metadata_owners(s, 0, "test", "stale.go"), 1);
ASSERT_EQ(store_count_metadata_owners(s, 1, "test", "stale.go"), 1);
ASSERT_EQ(cbm_store_rebuild_file_delta_owners(s, "test", TEST_GENERATION), CBM_STORE_OK);
int node_owners = 0;
int edge_owners = 0;
ASSERT_EQ(cbm_store_count_file_delta_owners(s, "test", "main.go", &node_owners,
&edge_owners),
CBM_STORE_OK);
ASSERT_EQ(node_owners, 1);
ASSERT_EQ(edge_owners, 1);
ASSERT_EQ(cbm_store_count_file_delta_owners(s, "test", "helper.go", &node_owners,
&edge_owners),
CBM_STORE_OK);
ASSERT_EQ(node_owners, 1);
ASSERT_EQ(edge_owners, 1);
ASSERT_EQ(store_count_metadata_owners(s, 0, "test", "stale.go"), 0);
ASSERT_EQ(store_count_metadata_owners(s, 1, "test", "stale.go"), 0);
cbm_store_close(s);
PASS();
}
TEST(store_import_export_metadata_crud) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
@@ -3207,6 +3285,7 @@ SUITE(store_nodes) {
RUN_TEST(store_index_generation_finish_complete);
RUN_TEST(store_index_generation_finish_failed_and_invalid_status);
RUN_TEST(store_owner_metadata_crud);
RUN_TEST(store_rebuild_file_delta_owners_derives_from_graph);
RUN_TEST(store_import_export_metadata_crud);
RUN_TEST(store_file_delta_affected_paths_from_exports_and_imports);
RUN_TEST(store_file_delta_affected_paths_high_fanout_dedupes);