From d7dde018ecfc887b8f9f275b3e461b5f653cd882 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Fri, 28 Aug 2026 18:01:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(wal):=20a=20stale=20compaction=20temp=20is?= =?UTF-8?q?=20removed=20at=20open=20=E2=80=94=20T2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit databasev2 3, task 2. - wo_wal_open removes `.compact` before reading anything. The only way one exists is a crash before the rename, which means its records were never authoritative - deleted rather than ignored, deliberately: a file full of well-formed records sitting beside the log is exactly what a future reader mistakes for data Test uses PLAUSIBLE content, not garbage — a byte copy of a real log — because garbage would be rejected by the CRC anyway and would prove nothing. It asserts the temp is present before the open, gone after, and that the live log still replays to exactly what it said. RED was an assertion failure (`access(tmp, F_OK) != 0` unmet), not a compile error, so the test was proven to exercise the behaviour before the behaviour existed. Verified: just wovm-test — 36 suites 0 fail, test_wal 340 pass, cli_smoke OK. Co-Authored-By: Claude Opus 5 (1M context) --- database/src/wal.c | 11 +++++++ runtime/test/test_wal.c | 64 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/database/src/wal.c b/database/src/wal.c index 28efec8..c37dbcf 100644 --- a/database/src/wal.c +++ b/database/src/wal.c @@ -304,6 +304,17 @@ int wo_wal_open(wo_wal *w, const char *path, uint64_t prealloc) { w->fd = open(path, O_RDWR | O_CREAT, 0644); if (w->fd < 0) return -1; w->path = strdup(path); /* NULL is tolerated: the diagnostic degrades */ + /* databasev2 3: remove a stale compaction temp before doing anything else. + * The only way one exists is a crash before the rename, which means its + * records were never authoritative — the live log below is the truth. It is + * deleted rather than ignored because a file full of well-formed records + * sitting beside the log is exactly the thing a future reader mistakes for + * data. */ + { + char tmp[4096]; + if ((size_t)snprintf(tmp, sizeof tmp, "%s%s", path, WO_WAL_TMP_SUFFIX) < sizeof tmp) + (void)unlink(tmp); + } if (prealloc) { /* best-effort: a filesystem without fallocate still works */ (void)posix_fallocate(w->fd, 0, (off_t)prealloc); diff --git a/runtime/test/test_wal.c b/runtime/test/test_wal.c index bdc9be7..d94a148 100644 --- a/runtime/test/test_wal.c +++ b/runtime/test/test_wal.c @@ -234,6 +234,69 @@ static void test_compact_shortens_and_replays_equal(void) { wo_rt_destroy(&rt); } +/* databasev2 3 Task 2: a stale temp file is the one input that could be + * mistaken for data — a crash before the rename leaves one behind, full of + * well-formed records that are NOT yet authoritative. So the fixture uses + * plausible records (a byte copy of a real log), not garbage: garbage would be + * rejected by the CRC anyway and would prove nothing. */ +static void test_stale_compact_temp_is_removed(void) { + char path[128], tmp[160]; + snprintf(path, sizeof path, "%s/stale.wal", g_dir); + snprintf(tmp, sizeof tmp, "%s%s", path, WO_WAL_TMP_SUFFIX); + wo_rt rt; + T_EQ(wo_rt_init(&rt, 1 << 20, CLASSES, 1), 0); + wo_db db; + T_EQ(wo_db_init(&db, CLASSES, 1, 0, 1), 0); + wo_wal w; + T_EQ(wo_wal_open(&w, path, 1 << 16), 0); + const char *msg = ""; + + /* two live rows in the REAL log */ + uint64_t ids[2]; + for (int i = 0; i < 2; i++) { + wo_str *s = wo_str_new(&rt, "abc", 3); + uint64_t vals[2] = {(uint64_t)(i + 1), (uint64_t)(uintptr_t)s}; + ids[i] = wo_row_insert(&db, 0, vals, &msg, NULL); + T_EQ(wo_wal_append_insert(&w, &db, 0, ids[i]), 0); + T_EQ(wo_wal_commit(&w), 0); + } + wo_wal_close(&w); + + /* forge a plausible stale temp: a byte copy of the real log */ + { + int src = open(path, O_RDONLY); + int dst = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644); + T_CHECK(src >= 0 && dst >= 0); + char buf[8192]; + ssize_t n; + while ((n = read(src, buf, sizeof buf)) > 0) T_CHECK(write(dst, buf, (size_t)n) == n); + close(src); + close(dst); + T_EQ(access(tmp, F_OK), 0); /* it really is there before we open */ + } + + wo_wal w2; + T_EQ(wo_wal_open(&w2, path, 1 << 16), 0); + T_CHECK(access(tmp, F_OK) != 0); /* gone, and never consulted */ + wo_wal_close(&w2); + + /* and the live log still says exactly what it said */ + wo_db db2; + T_EQ(wo_db_init(&db2, CLASSES, 1, 0, 1), 0); + T_EQ(wo_wal_replay(path, &db2), 2); + uint64_t out[2]; + T_EQ(wo_row_read(&db2, &rt, 0, ids[0], out, &msg), 0); + T_CHECK(out[0] == 1); + wo_str_free(&rt, (wo_str *)(uintptr_t)out[1]); + T_EQ(wo_row_read(&db2, &rt, 0, ids[1], out, &msg), 0); + T_CHECK(out[0] == 2); + wo_str_free(&rt, (wo_str *)(uintptr_t)out[1]); + + wo_db_destroy(&db2); + wo_db_destroy(&db); + wo_rt_destroy(&rt); +} + static void test_torn_tail(void) { char path[128]; snprintf(path, sizeof path, "%s/torn.wal", g_dir); @@ -446,6 +509,7 @@ int main(void) { test_roundtrip_replay(); test_commit_failure_detected(); test_compact_shortens_and_replays_equal(); + test_stale_compact_temp_is_removed(); test_torn_tail(); test_float_bytes_replay(); test_crash_battery();