diff --git a/src/store/store.c b/src/store/store.c index 34761d840..612122757 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -3985,6 +3985,97 @@ int cbm_store_count_overlay_generations(cbm_store_t *s, const char *project, return CBM_STORE_ERR; } +int cbm_store_claim_ready_overlay_generation(cbm_store_t *s, const char *project, + int64_t *out_overlay_generation, + int64_t *out_base_generation) { + if (out_overlay_generation) { + *out_overlay_generation = 0; + } + if (out_base_generation) { + *out_base_generation = 0; + } + if (!s || !s->db || !project || !project[0] || !out_overlay_generation || + !out_base_generation) { + if (s) { + store_set_error(s, "claim_ready_overlay_generation: invalid argument"); + } + return CBM_STORE_ERR; + } + + int rc = cbm_store_begin(s); + if (rc != CBM_STORE_OK) { + return rc; + } + + sqlite3_stmt *stmt = NULL; + static const char select_sql[] = + "SELECT overlay_generation, base_generation FROM overlay_generations " + "WHERE project = ?1 AND status = ?2 " + "ORDER BY overlay_generation ASC LIMIT 1;"; + if (sqlite3_prepare_v2(s->db, select_sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) { + store_set_error_sqlite(s, "claim_ready_overlay_generation select prepare"); + (void)cbm_store_rollback(s); + return CBM_STORE_ERR; + } + bind_text(stmt, ST_COL_1, project); + bind_text(stmt, ST_COL_2, CBM_STORE_OVERLAY_STATUS_READY); + rc = sqlite3_step(stmt); + if (rc == SQLITE_DONE) { + sqlite3_finalize(stmt); + (void)cbm_store_rollback(s); + return CBM_STORE_NOT_FOUND; + } + if (rc != SQLITE_ROW) { + sqlite3_finalize(stmt); + store_set_error_sqlite(s, "claim_ready_overlay_generation select"); + (void)cbm_store_rollback(s); + return CBM_STORE_ERR; + } + + int64_t overlay_generation = sqlite3_column_int64(stmt, 0); + int64_t base_generation = sqlite3_column_int64(stmt, ST_COL_1); + sqlite3_finalize(stmt); + stmt = NULL; + + static const char update_sql[] = + "UPDATE overlay_generations SET status = ?3, updated_at = ?4 " + "WHERE project = ?1 AND overlay_generation = ?2 AND status = ?5;"; + if (sqlite3_prepare_v2(s->db, update_sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) { + store_set_error_sqlite(s, "claim_ready_overlay_generation update prepare"); + (void)cbm_store_rollback(s); + return CBM_STORE_ERR; + } + char ts[CBM_SZ_64]; + iso_now(ts, sizeof(ts)); + bind_text(stmt, ST_COL_1, project); + sqlite3_bind_int64(stmt, ST_COL_2, overlay_generation); + bind_text(stmt, ST_COL_3, CBM_STORE_OVERLAY_STATUS_COMPACTING); + bind_text(stmt, ST_COL_4, ts); + bind_text(stmt, ST_COL_5, CBM_STORE_OVERLAY_STATUS_READY); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc != SQLITE_DONE) { + store_set_error_sqlite(s, "claim_ready_overlay_generation update"); + (void)cbm_store_rollback(s); + return CBM_STORE_ERR; + } + if (sqlite3_changes(s->db) != 1) { + store_set_error(s, "claim_ready_overlay_generation: ready generation not found"); + (void)cbm_store_rollback(s); + return CBM_STORE_NOT_FOUND; + } + + rc = cbm_store_commit(s); + if (rc != CBM_STORE_OK) { + (void)cbm_store_rollback(s); + return rc; + } + + *out_overlay_generation = overlay_generation; + *out_base_generation = base_generation; + return CBM_STORE_OK; +} + static int store_resolve_node_id(cbm_store_t *s, const char *project, const char *qn, int64_t *out_id) { const char *qns[1] = {qn}; diff --git a/src/store/store.h b/src/store/store.h index 177850458..386d4d4a4 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -688,6 +688,12 @@ int cbm_store_set_overlay_generation_status(cbm_store_t *s, const char *project, const char *status); int cbm_store_count_overlay_generations(cbm_store_t *s, const char *project, const char *status, int *out_count); +/* Atomically claim the oldest ready overlay generation for compaction by + * moving it from overlay_ready to compacting. Returns CBM_STORE_NOT_FOUND when + * no ready overlay generation exists for the project. */ +int cbm_store_claim_ready_overlay_generation(cbm_store_t *s, const char *project, + int64_t *out_overlay_generation, + int64_t *out_base_generation); /* Publish one file's replacement facts into overlay storage. This does not * mutate canonical nodes/edges; active read paths decide later how to combine diff --git a/tests/test_store_nodes.c b/tests/test_store_nodes.c index e69402123..a4a04af9c 100644 --- a/tests/test_store_nodes.c +++ b/tests/test_store_nodes.c @@ -1288,6 +1288,91 @@ TEST(store_overlay_generation_rejects_invalid_inputs) { PASS(); } +TEST(store_claim_ready_overlay_generation_claims_oldest_once) { + enum { BASE_GENERATION = 9 }; + 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); + + int64_t first = 0; + int64_t second = 0; + ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION, &first), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION + 1, + &second), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_set_overlay_generation_status(s, "test", first, + CBM_STORE_OVERLAY_STATUS_READY), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_set_overlay_generation_status(s, "test", second, + CBM_STORE_OVERLAY_STATUS_READY), + CBM_STORE_OK); + + int64_t claimed = 0; + int64_t base = 0; + ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base), + CBM_STORE_OK); + ASSERT_EQ(claimed, first); + ASSERT_EQ(base, BASE_GENERATION); + ASSERT_EQ(store_count_overlay_generation_row(s, "test", first, BASE_GENERATION, + CBM_STORE_OVERLAY_STATUS_COMPACTING), + 1); + + ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base), + CBM_STORE_OK); + ASSERT_EQ(claimed, second); + ASSERT_EQ(base, BASE_GENERATION + 1); + ASSERT_EQ(store_count_overlay_generation_row(s, "test", second, BASE_GENERATION + 1, + CBM_STORE_OVERLAY_STATUS_COMPACTING), + 1); + + claimed = -1; + base = -1; + ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base), + CBM_STORE_NOT_FOUND); + ASSERT_EQ(claimed, 0); + ASSERT_EQ(base, 0); + + cbm_store_close(s); + PASS(); +} + +TEST(store_claim_ready_overlay_generation_ignores_nonready_and_validates_outputs) { + enum { BASE_GENERATION = 3, SENTINEL = 42 }; + 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); + + int64_t reserved = 0; + int64_t failed = 0; + ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION, + &reserved), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_reserve_overlay_generation(s, "test", BASE_GENERATION, + &failed), + CBM_STORE_OK); + ASSERT_EQ(cbm_store_set_overlay_generation_status(s, "test", failed, + CBM_STORE_OVERLAY_STATUS_FAILED), + CBM_STORE_OK); + + int64_t claimed = SENTINEL; + int64_t base = SENTINEL; + ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, &base), + CBM_STORE_NOT_FOUND); + ASSERT_EQ(claimed, 0); + ASSERT_EQ(base, 0); + ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", NULL, &base), + CBM_STORE_ERR); + ASSERT_EQ(base, 0); + claimed = SENTINEL; + ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed, NULL), + CBM_STORE_ERR); + ASSERT_EQ(claimed, 0); + + cbm_store_close(s); + PASS(); +} + TEST(store_overlay_file_delta_publish_rows_and_tombstone) { enum { BASE_GENERATION = 3 }; cbm_store_t *s = cbm_store_open_memory(); @@ -1874,6 +1959,13 @@ TEST(store_compact_overlay_generation_promotes_metadata_and_cleans_overlay) { ASSERT_EQ(cbm_store_reserve_index_generation(s, "test", NULL, NULL, &generation), CBM_STORE_OK); ASSERT_EQ(generation, COMPACT_GENERATION); + int64_t claimed_overlay = 0; + int64_t claimed_base = 0; + ASSERT_EQ(cbm_store_claim_ready_overlay_generation(s, "test", &claimed_overlay, + &claimed_base), + CBM_STORE_OK); + ASSERT_EQ(claimed_overlay, overlay_generation); + ASSERT_EQ(claimed_base, BASE_GENERATION); ASSERT_EQ(cbm_store_compact_overlay_generation(s, "test", overlay_generation, generation), CBM_STORE_OK); @@ -5251,6 +5343,8 @@ SUITE(store_nodes) { RUN_TEST(store_index_generation_finish_failed_and_invalid_status); RUN_TEST(store_overlay_generation_reservation_status_and_counts); RUN_TEST(store_overlay_generation_rejects_invalid_inputs); + RUN_TEST(store_claim_ready_overlay_generation_claims_oldest_once); + RUN_TEST(store_claim_ready_overlay_generation_ignores_nonready_and_validates_outputs); RUN_TEST(store_overlay_file_delta_publish_rows_and_tombstone); RUN_TEST(store_delete_project_clears_overlay_fts); RUN_TEST(store_overlay_file_delta_publish_rejects_invalid_delta_without_rows);