fix(store): ADR sidecar follow-ups from #2315 review

Three small items promised in the #2315 review; behaviour is otherwise
unchanged.

1. A failed ADR restore on artifact import is no longer silent
   (src/pipeline/artifact.c). The import dropped both a failed reopen of
   the installed DB and the status of
   cbm_store_adr_restore_from_graph_if_absent, so a team ADR that never
   reached the sidecar left no trace. The restore moves into
   artifact_import_restore_adr, which logs
   level=warn msg=artifact.import err=adr_restore detail=<store error>
   path=<db> on either failure. The import still returns 0: the graph DB
   is already installed when the restore runs, and the restore stays
   best-effort as documented. adr_restore_if_absent now sets the store
   error for its begin, copy and commit failures, so the logged detail
   names the failing step.

2. The doc comment of adr_copy_legacy_once sat above adr_copy_rows,
   stacked on top of the comment that documents adr_copy_rows, which left
   adr_copy_legacy_once undocumented. It now sits above
   adr_copy_legacy_once (src/store/store.c).

3. adr_restore_if_absent no longer inserts the migrated marker. That
   INSERT OR IGNORE could never change state. Its only caller,
   cbm_store_adr_restore_from_graph_if_absent, takes the sidecar handle
   from adr_conn_write, which hands out a handle (cached or fresh) only
   after adr_copy_legacy_once returned OK, and that function returns OK
   only when the marker row is already committed (fast path, or the
   re-check under the write lock) or it just inserted and committed it.
   No code deletes an adr_meta row, so the insert hit the existing
   primary key every time, and its result was discarded anyway.

Tests: artifact_import_logs_failed_adr_restore puts a directory at
<db>.adr.db (the hook-free trick from the #2315 pipeline test), then
asserts the import still succeeds, the graph DB is installed, and the
warn line carries the store reason. It is red without the artifact.c
change. artifact_export_roundtrip_keeps_adr now also asserts that the
fresh destination sidecar is marked migrated after the import, which
pins the invariant item 3 relies on.

Refs #2315

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-31 20:46:20 +01:00
1 parent 3b46abec25
commit 2acbd3b028
3 files changed
+89 -21

No files matched your search

+23 -10
View File
@@ -1130,6 +1130,28 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
/* ── Import ──────────────────────────────────────────────────────── */
/* Restore the imported ADR (carried in the graph DB's project_summaries) into
* the per-project sidecar so it arrives with the shared artifact — even for a
* teammate who already indexed the project (a marked sidecar with no ADR),
* which the marker-gated migrate-once would skip. INSERT OR IGNORE means a
* local ADR is never overwritten. Best-effort: the graph DB is already
* installed when this runs, so a failed restore never fails the import, but it
* is logged with the store's reason so a team ADR that did not arrive is
* diagnosable instead of silently missing. */
static void artifact_import_restore_adr(const char *cache_db_path) {
cbm_store_t *restore = cbm_store_open_path_existing(cache_db_path);
if (!restore) {
cbm_log_warn("artifact.import", "err", "adr_restore", "detail", "reopen_imported_db",
"path", cache_db_path);
return;
}
if (cbm_store_adr_restore_from_graph_if_absent(restore) != CBM_STORE_OK) {
cbm_log_warn("artifact.import", "err", "adr_restore", "detail", cbm_store_error(restore),
"path", cache_db_path);
}
cbm_store_close(restore);
}
int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
if (!repo_path || !cache_db_path) {
return CBM_NOT_FOUND;
@@ -1257,16 +1279,7 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
cbm_unlink(wal);
cbm_unlink(shm);
/* Restore the imported ADR (carried in the graph DB's project_summaries) into
* the per-project sidecar so it arrives with the shared artifact — even for a
* teammate who already indexed the project (a marked sidecar with no ADR),
* which the marker-gated migrate-once would skip. INSERT OR IGNORE means a
* local ADR is never overwritten. Best-effort. */
cbm_store_t *restore = cbm_store_open_path_existing(cache_db_path);
if (restore) {
(void)cbm_store_adr_restore_from_graph_if_absent(restore);
cbm_store_close(restore);
}
artifact_import_restore_adr(cache_db_path);
cbm_log_info("artifact.import", "db", cache_db_path, "size_mb",
itoa_buf((int)((size_t)dlen / ART_BYTES_PER_MB)));
+14 -11
View File
@@ -12010,13 +12010,6 @@ static bool adr_migrated(sqlite3 *adb) {
return done;
}
/* Copy any legacy ADR rows from the graph DB's project_summaries into the
* sidecar (INSERT OR IGNORE, so a local row is never clobbered), then set the
* one-time marker. The marker check, copy and marker insert run in ONE
* BEGIN IMMEDIATE transaction on the sidecar, so two processes migrating at
* once cannot re-copy a row the other just deleted. Returns CBM_STORE_ERR on
* any failure (transaction rolled back) so callers can fail closed and preserve
* the old generation. No-op once the marker is set. */
/* Copy every ADR row from the graph DB's project_summaries into the sidecar with
* INSERT OR IGNORE (so a row already present locally is never overwritten). Must
* run inside a transaction on the sidecar connection. Returns OK/ERR; does not
@@ -12070,6 +12063,13 @@ static int adr_copy_rows(cbm_store_t *s, sqlite3 *adb) {
return (rc == SQLITE_DONE) ? CBM_STORE_OK : CBM_STORE_ERR;
}
/* Copy any legacy ADR rows from the graph DB's project_summaries into the
* sidecar (INSERT OR IGNORE, so a local row is never clobbered), then set the
* one-time marker. The marker check, copy and marker insert run in ONE
* BEGIN IMMEDIATE transaction on the sidecar, so two processes migrating at
* once cannot re-copy a row the other just deleted. Returns CBM_STORE_ERR on
* any failure (transaction rolled back) so callers can fail closed and preserve
* the old generation. No-op once the marker is set. */
static int adr_copy_legacy_once(cbm_store_t *s, sqlite3 *adb) {
if (adr_migrated(adb)) {
return CBM_STORE_OK; /* fast path: already migrated, no lock needed */
@@ -12106,19 +12106,22 @@ fail:
* DB's project_summaries, and a teammate who already indexed the project (so the
* sidecar is marked, but has no ADR) must still receive it. INSERT OR IGNORE
* guarantees a local ADR is never clobbered; running ungated by the marker is
* what distinguishes this from the one-time legacy migration. */
* what distinguishes this from the one-time legacy migration. It never writes
* the marker: `adb` comes from adr_conn_write, which hands out a sidecar handle
* only after adr_copy_legacy_once committed it, and nothing deletes it. On
* failure the store error names the step (the import logs it). */
static int adr_restore_if_absent(cbm_store_t *s, sqlite3 *adb) {
if (sqlite3_exec(adb, "BEGIN IMMEDIATE;", NULL, NULL, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr restore begin");
return CBM_STORE_ERR;
}
if (adr_copy_rows(s, adb) != CBM_STORE_OK) {
store_set_error(s, "adr restore copy failed");
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return CBM_STORE_ERR;
}
/* Mark migrated too, so a later reindex's before-delete migration is a no-op. */
(void)sqlite3_exec(adb, "INSERT OR IGNORE INTO adr_meta (key, value) VALUES ('migrated','1');",
NULL, NULL, NULL);
if (sqlite3_exec(adb, "COMMIT;", NULL, NULL, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr restore commit");
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return CBM_STORE_ERR;
}
+52
View File
@@ -252,6 +252,21 @@ TEST(artifact_export_roundtrip_keeps_adr) {
cbm_store_adr_free(&adr);
cbm_store_close(dst);
/* The fresh destination sidecar is marked migrated after the import, so a
* later reindex's migrate-before-delete is a no-op. Opening the sidecar for
* the restore already commits the marker; the restore itself adds none. */
char side[1100];
snprintf(side, sizeof(side), "%s.adr.db", import_db);
sqlite3 *raw = NULL;
ASSERT_EQ(sqlite3_open_v2(side, &raw, SQLITE_OPEN_READONLY, NULL), SQLITE_OK);
sqlite3_stmt *st = NULL;
ASSERT_EQ(
sqlite3_prepare_v2(raw, "SELECT value FROM adr_meta WHERE key='migrated';", -1, &st, NULL),
SQLITE_OK);
ASSERT_EQ(sqlite3_step(st), SQLITE_ROW);
sqlite3_finalize(st);
sqlite3_close(raw);
cleanup_dir(g_tmpdir);
PASS();
}
@@ -391,6 +406,42 @@ TEST(artifact_import_restores_adr_when_local_absent) {
PASS();
}
/* A failed ADR restore must not be silent (#2315 follow-up). A DIRECTORY at
* "<db>.adr.db" makes the sidecar open fail deterministically (no hook). The
* graph DB is already installed when the restore runs, so the import still
* succeeds (best-effort, unchanged), but the team ADR that did not arrive is
* logged with the store's reason instead of being dropped without a trace. */
TEST(artifact_import_logs_failed_adr_restore) {
setup_artifact_test();
create_test_db(g_db);
cbm_store_t *src = cbm_store_open_path(g_db);
ASSERT_NOT_NULL(src);
ASSERT_EQ(cbm_store_adr_store(src, "test-proj", "# Decision\nshared A"), CBM_STORE_OK);
cbm_store_close(src);
ASSERT_EQ(cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST), 0);
char import_db[1024];
snprintf(import_db, sizeof(import_db), "%s/imported.db", g_tmpdir);
char adr_dir[1100];
snprintf(adr_dir, sizeof(adr_dir), "%s.adr.db", import_db);
ASSERT_EQ(cbm_mkdir(adr_dir), 0);
capture_logs_start();
int rc = cbm_artifact_import(g_repo, import_db);
const char *logs = capture_logs_end();
ASSERT_EQ(rc, 0); /* the graph DB is installed; the restore stays best-effort */
ASSERT_TRUE(cbm_file_exists(import_db));
const char *line = strstr(logs, "level=warn msg=artifact.import err=adr_restore");
ASSERT_NOT_NULL(line);
/* The store's reason is carried (the text log format maps spaces to '_'). */
ASSERT(strstr(line, "detail=adr_sidecar_open") != NULL);
cleanup_dir(g_tmpdir);
PASS();
}
TEST(artifact_export_best_roundtrip) {
setup_artifact_test();
create_test_db(g_db);
@@ -1361,6 +1412,7 @@ SUITE(artifact) {
RUN_TEST(artifact_export_without_adr_does_not_resurrect_legacy_row);
RUN_TEST(artifact_import_does_not_clobber_local_adr);
RUN_TEST(artifact_import_restores_adr_when_local_absent);
RUN_TEST(artifact_import_logs_failed_adr_restore);
RUN_TEST(artifact_export_best_roundtrip);
RUN_TEST(artifact_exists_check);
RUN_TEST(artifact_commit_hash);