Skip to content
Merged
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
12 changes: 12 additions & 0 deletions src/pipeline/pass_cross_repo.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,25 @@ static bool cr_store_has_exact_project(cbm_store_t *store, const char *project)
return matches;
}

/* True when `project` is a usable cross-repo input: its store holds exactly
* that project AND carries the schema the read-write open in
* cr_open_existing_project requires. Checking only the first let a pre-#768
* store (still readable, so list_projects shows it) pass validation and the
* ["*"] enumeration, then abort the whole run at its write open — after the
* source's previous CROSS_* generation had already been deleted. The probe is
* read-only, so the legacy store is left untouched for its reindex. (#2133) */
static bool cr_project_exists(const char *project) {
char path[CR_PATH_BUF];
if (!cr_db_path(project, path, sizeof(path))) {
return false;
}
cbm_store_t *store = cbm_store_open_path_query(path);
bool exists = cr_store_has_exact_project(store, project);
if (exists && !cbm_store_edges_schema_current(store)) {
cbm_log_warn("cross_repo.project_unusable", "project", project, "reason",
"pre_768_schema_reindex_required");
exists = false;
}
cbm_store_close(store);
return exists;
}
Expand Down
24 changes: 15 additions & 9 deletions src/store/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,18 @@ static void iso_now(char *buf, size_t sz) {

/* ── Schema ─────────────────────────────────────────────────────── */

bool cbm_store_edges_schema_current(cbm_store_t *s) {
sqlite3_stmt *probe = NULL;
if (!s || !s->db ||
sqlite3_prepare_v2(s->db, "SELECT local_name_gen FROM edges LIMIT 0;", CBM_NOT_FOUND,
&probe, NULL) != SQLITE_OK) {
sqlite3_finalize(probe);
return false;
}
sqlite3_finalize(probe);
return true;
}

static int init_schema(cbm_store_t *s) {
const char *ddl =
"CREATE TABLE IF NOT EXISTS projects ("
Expand Down Expand Up @@ -357,15 +369,9 @@ static int init_schema(cbm_store_t *s) {
* callers already treat an unopenable DB as incompatible (a full index
* deletes + rebuilds it, artifact import refuses and falls back to a
* reindex). Read-only query opens skip init_schema and keep working. */
{
sqlite3_stmt *probe = NULL;
if (sqlite3_prepare_v2(s->db, "SELECT local_name_gen FROM edges LIMIT 0;", CBM_NOT_FOUND,
&probe, NULL) != SQLITE_OK) {
cbm_log_warn("store.schema", "result", "incompatible", "missing",
"edges.local_name_gen");
return CBM_STORE_ERR;
}
sqlite3_finalize(probe);
if (!cbm_store_edges_schema_current(s)) {
cbm_log_warn("store.schema", "result", "incompatible", "missing", "edges.local_name_gen");
return CBM_STORE_ERR;
}

/* FTS5 contentless virtual table for BM25 full-text search.
Expand Down
5 changes: 5 additions & 0 deletions src/store/store.h
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,11 @@ const char *cbm_store_db_path(const cbm_store_t *s);
* (projects table has correct types, no corruption indicators).
* Returns false if corruption is detected — caller should delete and re-index. */
bool cbm_store_check_integrity(cbm_store_t *s);
/* True when the edges table carries the #768 local_name_gen discriminator —
* the schema a read-write open (cbm_store_open_path_existing) requires. A
* pre-#768 store still serves read-only queries but refuses every write open
* until it is reindexed. Read-only: safe on a store opened for query. */
bool cbm_store_edges_schema_current(cbm_store_t *s);
/* Shallow check + PRAGMA quick_check — catches page-level corruption.
* O(db size); use on rare paths (artifact import), not hot opens. */
bool cbm_store_check_integrity_deep(cbm_store_t *s);
Expand Down
89 changes: 89 additions & 0 deletions tests/test_cross_repo.c
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,93 @@ TEST(cross_repo_wildcard_keeps_projects_containing_internal_tokens) {
PASS();
}

/* A project store written before the #768 edges.local_name_gen column: it
* still answers read-only queries (list_projects shows it), but the
* read-write open every cross-repo target needs refuses it until a reindex. */
static bool cross_repo_create_pre768_project(const cross_repo_fixture_t *fixture,
const char *project) {
char path[512];
if (!cross_repo_project_path(fixture, project, path, sizeof(path))) {
return false;
}
sqlite3 *db = NULL;
if (sqlite3_open(path, &db) != SQLITE_OK) {
sqlite3_close(db);
return false;
}
char sql[1024];
snprintf(sql, sizeof(sql),
"CREATE TABLE projects(name TEXT PRIMARY KEY, indexed_at TEXT NOT NULL,"
" root_path TEXT NOT NULL);"
"CREATE TABLE nodes(id INTEGER PRIMARY KEY AUTOINCREMENT, project TEXT NOT NULL,"
" label TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL,"
" file_path TEXT DEFAULT '', start_line INTEGER DEFAULT 0,"
" end_line INTEGER DEFAULT 0, properties TEXT DEFAULT '{}',"
" UNIQUE(project, qualified_name));"
"CREATE TABLE edges(id INTEGER PRIMARY KEY AUTOINCREMENT, project TEXT NOT NULL,"
" source_id INTEGER NOT NULL, target_id INTEGER NOT NULL, type TEXT NOT NULL,"
" properties TEXT DEFAULT '{}', UNIQUE(source_id, target_id, type));"
"INSERT INTO projects VALUES('%s', '2026-06-01T00:00:00Z', '/pre768');",
project);
bool ok = sqlite3_exec(db, sql, NULL, NULL, NULL) == SQLITE_OK;
sqlite3_close(db);
return ok;
}

/* #2133: ["*"] enumerated every store a read-only open accepts, then aborted
* the whole run on the first one the matcher's read-write open refused — a
* single pre-#768 index anywhere in the cache made the wildcard fail with
* "missing, invalid, or not indexed" while naming the same live targets
* worked. A store the matcher cannot use is not a wildcard target. */
TEST(cross_repo_wildcard_skips_pre768_store_issue2133) {
cross_repo_fixture_t fixture;
bool setup = cross_repo_fixture_begin(&fixture) &&
cross_repo_seed_http_pair(&fixture, "wild-src", "wild-api", "/orders", "w") &&
cross_repo_create_pre768_project(&fixture, "aa-pre768-store");
if (!setup) {
cross_repo_fixture_end(&fixture);
FAIL("failed to seed pre-#768 wildcard fixture");
}

const char *targets[] = {"*"};
cbm_cross_repo_result_t result = cbm_cross_repo_match("wild-src", targets, 1);
int edges = cross_repo_count_edges(&fixture, "wild-src", "CROSS_HTTP_CALLS");
cross_repo_fixture_end(&fixture);

ASSERT_FALSE(result.failed);
ASSERT_EQ(result.projects_scanned, 1);
ASSERT_EQ(result.http_edges, 1);
ASSERT_EQ(edges, 1);
PASS();
}

/* Naming an unusable store stays an error, but it must be refused during
* validation, before the source's previous CROSS_* generation is deleted. */
TEST(cross_repo_named_pre768_target_fails_before_cleanup_issue2133) {
cross_repo_fixture_t fixture;
bool setup = cross_repo_fixture_begin(&fixture) &&
cross_repo_seed_http_pair(&fixture, "named-src", "named-api", "/orders", "n") &&
cross_repo_create_pre768_project(&fixture, "aa-pre768-store");
if (!setup) {
cross_repo_fixture_end(&fixture);
FAIL("failed to seed pre-#768 named fixture");
}

const char *live[] = {"named-api"};
cbm_cross_repo_result_t initial = cbm_cross_repo_match("named-src", live, 1);
int before = cross_repo_count_edges(&fixture, "named-src", "CROSS_HTTP_CALLS");
const char *with_pre768[] = {"named-api", "aa-pre768-store"};
cbm_cross_repo_result_t result = cbm_cross_repo_match("named-src", with_pre768, 2);
int after = cross_repo_count_edges(&fixture, "named-src", "CROSS_HTTP_CALLS");
cross_repo_fixture_end(&fixture);

ASSERT_FALSE(initial.failed);
ASSERT_EQ(before, 1);
ASSERT_TRUE(result.failed);
ASSERT_EQ(after, before);
PASS();
}

static bool cross_repo_seed_bounded_scan(const cross_repo_fixture_t *fixture,
const char *source_project, const char *target_project) {
enum { TEST_SCAN_ROWS = 4097 };
Expand Down Expand Up @@ -523,6 +610,8 @@ SUITE(cross_repo) {
RUN_TEST(cross_repo_accepts_project_with_missed_shadow_row_issue1609);
RUN_TEST(cross_repo_null_target_fails_without_dereference);
RUN_TEST(cross_repo_wildcard_keeps_projects_containing_internal_tokens);
RUN_TEST(cross_repo_wildcard_skips_pre768_store_issue2133);
RUN_TEST(cross_repo_named_pre768_target_fails_before_cleanup_issue2133);
RUN_TEST(cross_repo_scan_bound_counts_examined_rows_not_matches);
RUN_TEST(cross_repo_propagates_delete_failure);
RUN_TEST(cross_repo_failed_bidirectional_insert_is_not_counted);
Expand Down
Loading