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
2 changes: 1 addition & 1 deletion Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ TEST_DISCOVER_SRCS = \

TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c

TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c

TEST_WATCHER_SRCS = tests/test_watcher.c

Expand Down
27 changes: 24 additions & 3 deletions src/pipeline/pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -1116,17 +1116,30 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file
if (check_store && cbm_store_check_integrity(check_store)) {
cbm_file_hash_t *hashes = NULL;
int hash_count = 0;
int fmt = 0;

cbm_store_get_file_hashes(check_store, p->project_name, &hashes, &hash_count);
cbm_store_free_file_hashes(hashes, hash_count);
cbm_store_get_format_version(check_store, &fmt);

bool format_stale = (fmt != CBM_INDEX_FORMAT_VERSION);
cbm_store_close(check_store);
if (hash_count > 0 && file_count <= hash_count + (hash_count / PAIR_LEN)) {

if (format_stale) {
if (hash_count > 0) {
char stored_buf[CBM_SZ_32];
snprintf(stored_buf, sizeof(stored_buf), "%d", fmt);
cbm_log_info("pipeline.route", "path", "format_change_reindex", "stored_format",
stored_buf);
}
/* fall through to delete + full rebuild */
} else if (hash_count > 0 && file_count <= hash_count + (hash_count / PAIR_LEN)) {
cbm_log_info("pipeline.route", "path", "incremental", "stored_hashes",
itoa_buf(hash_count));
int rc = cbm_pipeline_run_incremental(p, db_path, files, file_count);
free(db_path);
return rc;
}
if (hash_count > 0) {
} else if (hash_count > 0) {
cbm_log_info("pipeline.route", "path", "mode_change_reindex", "stored_hashes",
itoa_buf(hash_count), "discovered", itoa_buf(file_count));
}
Expand Down Expand Up @@ -1224,8 +1237,13 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil
CBM_PROF_START(t_reopen);
cbm_store_t *hash_store = cbm_store_open_path(db_path);
CBM_PROF_END("persist", "1_reopen", t_reopen);
bool format_stamped = true;
if (hash_store) {
bool hash_records_complete = true;
if (cbm_store_set_format_version(hash_store, CBM_INDEX_FORMAT_VERSION) != CBM_STORE_OK) {
cbm_log_error("pipeline.err", "phase", "persist_format_version");
format_stamped = false;
}
CBM_PROF_START(t_delhash);
if (cbm_store_delete_file_hashes(hash_store, p->project_name) != CBM_STORE_OK) {
hash_records_complete = false;
Expand Down Expand Up @@ -1377,6 +1395,9 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_info_t *fil
free(p->saved_adr);
p->saved_adr = NULL;

if (!format_stamped) {
return CBM_NOT_FOUND;
}
/* Export persistent artifact if enabled */
if (p->persistence) {
CBM_PROF_START(t_art);
Expand Down
29 changes: 20 additions & 9 deletions src/pipeline/pipeline_incremental.c
Original file line number Diff line number Diff line change
Expand Up @@ -643,11 +643,11 @@ static void run_postpasses(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed_fil
* Mode-skipped hash rows are preserved across the rebuild so subsequent
* reindexes can correctly distinguish "never indexed" from "indexed but
* not visited this pass". */
static void dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *project,
cbm_file_info_t *files, int file_count,
const cbm_file_hash_t *mode_skipped, int mode_skipped_count,
const char *repo_path, const cbm_coverage_row_t *cov, int cov_count,
const cbm_coverage_meta_t *meta_template) {
static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *project,
cbm_file_info_t *files, int file_count,
const cbm_file_hash_t *mode_skipped, int mode_skipped_count,
const char *repo_path, const cbm_coverage_row_t *cov, int cov_count,
const cbm_coverage_meta_t *meta_template) {
struct timespec t;
cbm_clock_gettime(CLOCK_MONOTONIC, &t);

Expand All @@ -662,12 +662,20 @@ static void dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *
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)));

bool format_stamped = true;
cbm_store_t *hash_store = cbm_store_open_path(db_path);
if (hash_store) {
bool hash_records_complete = persist_hashes(hash_store, project, files, file_count,
mode_skipped, mode_skipped_count);

/* #769: cbm_writer_open truncates and rebuilds the DB file from
* scratch on every dump, so the format version written by the last
* full index is gone by the time we get here. */
if (cbm_store_set_format_version(hash_store, CBM_INDEX_FORMAT_VERSION) != CBM_STORE_OK) {
cbm_log_error("incremental.err", "msg", "persist_format_version", "project", project);
format_stamped = false;
}

/* Coverage rows (#963): re-write the merged set into the rebuilt DB
* (AFTER hashes, so the deleted-file prune sees the live file set). */
cbm_project_t project_info = {0};
Expand Down Expand Up @@ -705,6 +713,8 @@ static void dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *
if (repo_path && cbm_artifact_exists(repo_path)) {
cbm_artifact_export(db_path, repo_path, project, CBM_ARTIFACT_FAST);
}

return format_stamped ? 0 : CBM_NOT_FOUND;
}

/* ── Incremental pipeline entry point ────────────────────────────── */
Expand Down Expand Up @@ -1010,13 +1020,14 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
.ignored_files_total = run_ignored_total,
.coverage_version = 1,
};
dump_and_persist(existing, db_path, project, files, file_count, mode_skipped,
mode_skipped_count, cbm_pipeline_repo_path(p), cov, cov_n, &coverage_meta);
int dp_rc =
dump_and_persist(existing, db_path, project, files, file_count, mode_skipped,
mode_skipped_count, cbm_pipeline_repo_path(p), cov, cov_n, &coverage_meta);
free(cov);
cbm_store_free_coverage(old_cov, old_cov_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
cbm_gbuf_free(existing);

cbm_log_info("incremental.done", "elapsed_ms", itoa_buf((int)elapsed_ms(t0)));
return 0;
return dp_rc;
}
30 changes: 30 additions & 0 deletions src/store/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -2918,6 +2918,36 @@ int cbm_store_list_files(cbm_store_t *s, const char *project, char ***out, int *
return CBM_STORE_OK;
}

/* ── Index format version (PRAGMA user_version) ─────────────────── */

int cbm_store_get_format_version(cbm_store_t *s, int *out) {
if (!s || !s->db || !out) {
return CBM_STORE_ERR;
}
*out = 0;
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(s->db, "PRAGMA user_version;", CBM_NOT_FOUND, &stmt, NULL) !=
SQLITE_OK) {
return CBM_STORE_ERR;
}
if (sqlite3_step(stmt) == SQLITE_ROW) {
*out = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
return CBM_STORE_OK;
}

/* PRAGMA values cannot be bound; format the integer constant in. Writes, so
* only call on a read-write connection (never from configure_pragmas). */
int cbm_store_set_format_version(cbm_store_t *s, int version) {
if (!s || !s->db) {
return CBM_STORE_ERR;
}
char sql[ST_BUF_64];
snprintf(sql, sizeof(sql), "PRAGMA user_version = %d;", version);
return exec_sql(s, sql);
}

/* ── Node neighbor names ──────────────────────────────────────── */

static int query_neighbor_names(sqlite3 *db, const char *sql, int64_t node_id, int limit,
Expand Down
8 changes: 8 additions & 0 deletions src/store/store.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ typedef struct cbm_store cbm_store_t;
#define CBM_STORE_OK 0
#define CBM_STORE_ERR (-1)
#define CBM_STORE_NOT_FOUND (-2)
#define CBM_INDEX_FORMAT_VERSION 1

/* ── Data structures ────────────────────────────────────────────── */

Expand Down Expand Up @@ -77,6 +78,13 @@ void cbm_store_node_degree(cbm_store_t *s, int64_t node_id, int *in_deg, int *ou
* Returns CBM_STORE_OK or CBM_STORE_ERR. */
int cbm_store_list_files(cbm_store_t *s, const char *project, char ***out, int *count);

/* Persisted index-format identity. Bump when a change alters the QN scheme
* or node identity of an already-written graph, so an old DB is routed
* through the full-reindex path instead of producing a mixed graph.
* 1 = File QNs keep the file extension (#769). */
int cbm_store_get_format_version(cbm_store_t *s, int *out);
int cbm_store_set_format_version(cbm_store_t *s, int version);

/* Get caller/callee names for a node (CALLS/HTTP_CALLS/ASYNC_CALLS edges).
* Returns 0 on success. Caller must free each out_callers[i]/out_callees[i]
* and the arrays themselves. */
Expand Down
79 changes: 79 additions & 0 deletions tests/test_fqn.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,77 @@ TEST(fqn_compute_basic_rs) {
PASS();
}

TEST(fqn_compute_file_sibling_distinct) {
char *ts = cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.ts", "__file__");
char *html = cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.html", "__file__");
char *scss = cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.scss", "__file__");
ASSERT_NOT_NULL(ts);
ASSERT_NOT_NULL(html);
ASSERT_NOT_NULL(scss);
ASSERT_STR_NEQ(ts, html);
ASSERT_STR_NEQ(ts, scss);
ASSERT_STR_NEQ(html, scss);
free(ts);
free(html);
free(scss);
PASS();
}

TEST(fqn_compute_symbol_still_strips) {
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.ts", "BadgeComponent"),
"proj.app.badge.badge.component.BadgeComponent");
PASS();
}

TEST(fqn_module_siblings_still_share) {
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "app/badge/badge.component.ts"),
"proj.app.badge.badge.component");
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "app/badge/badge.component.html"),
"proj.app.badge.badge.component");
PASS();
}

/* #769: exact __file__ QNs for component siblings. */
TEST(fqn_compute_file_sibling_exact) {
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.ts", "__file__"),
"proj.app.badge.badge.component.ts.__file__");
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.html", "__file__"),
"proj.app.badge.badge.component.html.__file__");
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.component.scss", "__file__"),
"proj.app.badge.badge.component.scss.__file__");
PASS();
}

TEST(fqn_compute_file_multi_extension) {
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/badge/badge.spec.ts", "__file__"),
"proj.app.badge.badge.spec.ts.__file__");
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "archive.tar.gz", "__file__"),
"proj.archive.tar.gz.__file__");
PASS();
}

TEST(fqn_compute_file_no_extension) {
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "Makefile", "__file__"),
"proj.Makefile.__file__");
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "LICENSE", "__file__"),
"proj.LICENSE.__file__");
PASS();
}

TEST(fqn_compute_file_dotfile) {
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", ".gitignore", "__file__"),
"proj..gitignore.__file__");
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "config/.env", "__file__"),
"proj.config..env.__file__");
PASS();
}

TEST(fqn_compute_file_index_ts) {
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app/index.ts", "__file__"),
"proj.app.index.ts.__file__");
PASS();
}

/* ── Nested paths ─────────────────────────────────────────────── */

TEST(fqn_compute_nested_two_levels) {
Expand Down Expand Up @@ -595,6 +666,14 @@ SUITE(fqn) {
RUN_TEST(fqn_file_qn_distinguishes_same_stem_header_source_issue964);
RUN_TEST(fqn_module_qn_still_strips_extension);
RUN_TEST(fqn_compute_basic_rs);
RUN_TEST(fqn_compute_file_sibling_distinct);
RUN_TEST(fqn_compute_symbol_still_strips);
RUN_TEST(fqn_module_siblings_still_share);
RUN_TEST(fqn_compute_file_sibling_exact);
RUN_TEST(fqn_compute_file_multi_extension);
RUN_TEST(fqn_compute_file_no_extension);
RUN_TEST(fqn_compute_file_dotfile);
RUN_TEST(fqn_compute_file_index_ts);

/* fqn_compute: nested paths */
RUN_TEST(fqn_compute_nested_two_levels);
Expand Down
Loading