Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions src/pipeline/pipeline_incremental.c
Original file line number Diff line number Diff line change
Expand Up @@ -645,25 +645,49 @@ static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *p
struct timespec t;
cbm_clock_gettime(CLOCK_MONOTONIC, &t);

/* Preserve ADR content stored in project_summaries before replacing the DB. */
char *saved_adr = NULL;
cbm_store_t *adr_store = cbm_store_open_path(db_path);
if (adr_store) {
cbm_adr_t existing = {0};
if (cbm_store_adr_get(adr_store, project, &existing) == CBM_STORE_OK) {
if (existing.content) {
saved_adr = strdup(existing.content);
}
cbm_store_adr_free(&existing);
}
cbm_store_close(adr_store);
}

int rc = CBM_STORE_ERR;

if ((cbm_unlink(db_path) != 0 && errno != ENOENT) || cbm_remove_db_sidecars(db_path) != 0) {
cbm_log_error("incremental.err", "msg", "clear_staging_failed", "path", db_path);
return CBM_STORE_ERR;
goto cleanup;
}

int dump_rc = cbm_gbuf_dump_to_sqlite(gbuf, db_path);
cbm_log_info("incremental.dump", "rc", itoa_buf(dump_rc), "elapsed_ms",
itoa_buf((int)elapsed_ms(t)));
if (dump_rc != 0) {
return dump_rc;
rc = dump_rc;
goto cleanup;
}

cbm_store_t *hash_store = cbm_store_open_path(db_path);
if (!hash_store) {
cbm_log_error("incremental.err", "msg", "open_staging_after_dump", "path", db_path);
return CBM_STORE_ERR;
goto cleanup;
}
bool adr_restore_failed = false;
/* #992: restore the captured ADR before persisting hashes, mirroring the
* full-reindex path (#516) -- the DB replacement above dropped it. */
if (saved_adr && cbm_store_adr_store(hash_store, project, saved_adr) != CBM_STORE_OK) {
cbm_log_error("incremental.err", "msg", "adr_restore", "project", project);
adr_restore_failed = true;
}
int rc =
persist_hashes(hash_store, project, files, file_count, mode_skipped, mode_skipped_count);

rc = persist_hashes(hash_store, project, files, file_count, mode_skipped, mode_skipped_count);

/* Coverage rows (#963): re-write the merged set into the rebuilt DB
* (AFTER hashes, so the deleted-file prune sees the live file set). */
Expand Down Expand Up @@ -701,6 +725,12 @@ static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *p
}
}
cbm_store_close(hash_store);
if (adr_restore_failed) {
rc = CBM_PIPELINE_ABORT_PRESERVE_DB;
}

cleanup:
free(saved_adr);
return rc;
}

Expand Down
71 changes: 71 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "test_framework.h"
#include "test_helpers.h"
#include "foundation/mem.h" // cbm_mem_init/budget (back-pressure futile-nap test)
#include "foundation/log.h"
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "store/store.h"
Expand All @@ -29,6 +30,13 @@
/* ── Helper: create temp test repo with known layout ───────────── */

static char g_tmpdir[256];
static bool g_incremental_route_seen;

static void capture_incremental_route(const char *line) {
if (strstr(line, "pipeline.route") && strstr(line, "incremental")) {
g_incremental_route_seen = true;
}
}

/* Create:
* /tmp/cbm_test_XXXXXX/
Expand Down Expand Up @@ -344,6 +352,68 @@ TEST(pipeline_adr_survives_full_reindex) {
PASS();
}

TEST(pipeline_adr_survives_incremental_reindex) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_adr_incr_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("failed to create temp dir");
}

char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/test.db", tmp);

char path[512];
snprintf(path, sizeof(path), "%s/main.py", tmp);
FILE *f = fopen(path, "w");
ASSERT_NOT_NULL(f);
fprintf(f, "def foo():\n return 1\n");
fclose(f);

cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p1);
ASSERT_EQ(cbm_pipeline_run(p1), 0);
const char *project = cbm_pipeline_project_name(p1);
char project_copy[256];
snprintf(project_copy, sizeof(project_copy), "%s", project);
cbm_pipeline_free(p1);

const char *adr_text = "# Decision\nIncremental reindex keeps ADR content.";
cbm_store_t *s1 = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s1);
ASSERT_EQ(cbm_store_adr_store(s1, project_copy, adr_text), CBM_STORE_OK);
cbm_store_close(s1);

/* Change one existing file so cbm_pipeline_run routes through incremental
* reindex instead of the full delete/rebuild path. */
f = fopen(path, "a");
ASSERT_NOT_NULL(f);
fprintf(f, "\ndef bar():\n return 2\n");
fclose(f);

g_incremental_route_seen = false;
cbm_log_set_sink_ex(capture_incremental_route, CBM_LOG_SINK_REPLACE);
cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p2);
int pipeline_rc = cbm_pipeline_run(p2);
cbm_log_set_sink(NULL);
ASSERT_EQ(pipeline_rc, 0);
ASSERT_TRUE(g_incremental_route_seen);
cbm_pipeline_free(p2);

cbm_store_t *s2 = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s2);
cbm_adr_t adr = {0};
int rc = cbm_store_adr_get(s2, project_copy, &adr);
ASSERT_EQ(rc, CBM_STORE_OK);
ASSERT_NOT_NULL(adr.content);
ASSERT_STR_EQ(adr.content, adr_text);
cbm_store_adr_free(&adr);
cbm_store_close(s2);

rm_rf(tmp);
PASS();
}

TEST(pipeline_structure_edges) {
if (setup_test_repo() != 0) {
FAIL("failed to create temp dir");
Expand Down Expand Up @@ -7772,6 +7842,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_structure_nodes);
RUN_TEST(pipeline_committed_counts_match_persisted);
RUN_TEST(pipeline_adr_survives_full_reindex);
RUN_TEST(pipeline_adr_survives_incremental_reindex);
RUN_TEST(pipeline_structure_edges);
RUN_TEST(pipeline_branch_root_structure);
RUN_TEST(pipeline_project_name_derived);
Expand Down
Loading