From d70b4d2df714bd4b9cca3ffa5832dbf4a71a75c3 Mon Sep 17 00:00:00 2001 From: Andrew Hundt Date: Tue, 30 Jun 2026 23:12:23 -0400 Subject: [PATCH] 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 --- src/pipeline/pipeline.c | 11 ++++ src/store/store.c | 106 +++++++++++++++++++++++++++++++++++++++ src/store/store.h | 5 ++ tests/test_pipeline.c | 35 +++++++++++++ tests/test_store_nodes.c | 79 +++++++++++++++++++++++++++++ 5 files changed, 236 insertions(+) diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index fdb7237fb..cd4c15cd6 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -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)); diff --git a/src/store/store.c b/src/store/store.c index 1fe90435d..d89467364 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -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 = diff --git a/src/store/store.h b/src/store/store.h index db8c0f9e9..71f86e536 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -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); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 826e43245..1e0df9074 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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); diff --git a/tests/test_store_nodes.c b/tests/test_store_nodes.c index 90d70796b..f34ccbb27 100644 --- a/tests/test_store_nodes.c +++ b/tests/test_store_nodes.c @@ -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);