190 lines
6.8 KiB
C
190 lines
6.8 KiB
C
/*
|
|
* test_store_checkpoint.c — Tests for WAL checkpoint behavior.
|
|
*
|
|
* Verifies that cbm_store_checkpoint() does not truncate the on-disk
|
|
* WAL file. SQLITE_CHECKPOINT_TRUNCATE shrinks the WAL via ftruncate(fd, 0)
|
|
* on success; on macOS this can raise SIGBUS in a sibling process that
|
|
* has the DB mmap'd through SQLite when it next faults a page in the
|
|
* now-shorter region. SQLITE_CHECKPOINT_PASSIVE marks frames as
|
|
* checkpointed in the WAL header without changing the file size — disk
|
|
* space is reclaimed on the next write cycle, not on every checkpoint.
|
|
*/
|
|
#include "../src/foundation/compat.h"
|
|
#include "test_framework.h"
|
|
#include "test_helpers.h"
|
|
#include <store/store.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
TEST(checkpoint_does_not_truncate_wal) {
|
|
enum { N_ROWS = 100, PATH_BUF = 256, PATH_BUF_EXT = 300 };
|
|
char db_path[PATH_BUF];
|
|
snprintf(db_path, sizeof(db_path), "%s/cbm_test_ckpt_%d.db", cbm_tmpdir(), (int)getpid());
|
|
char wal_path[PATH_BUF_EXT];
|
|
snprintf(wal_path, sizeof(wal_path), "%s-wal", db_path);
|
|
char shm_path[PATH_BUF_EXT];
|
|
snprintf(shm_path, sizeof(shm_path), "%s-shm", db_path);
|
|
unlink(db_path);
|
|
unlink(wal_path);
|
|
unlink(shm_path);
|
|
|
|
cbm_store_t *s = cbm_store_open_path(db_path);
|
|
ASSERT(s != NULL);
|
|
|
|
/* Grow WAL beyond zero bytes via direct SQL. */
|
|
int rc_sql = cbm_store_exec(
|
|
s,
|
|
"INSERT OR IGNORE INTO projects(name, indexed_at, root_path) "
|
|
"VALUES('p', '2026-01-01', '/tmp/p');");
|
|
ASSERT_EQ(rc_sql, 0);
|
|
for (int i = 0; i < N_ROWS; i++) {
|
|
char sql[256];
|
|
snprintf(sql, sizeof(sql),
|
|
"INSERT INTO nodes(project, label, name, qualified_name, file_path) "
|
|
"VALUES('p', 'Function', 'fn', 'p.module.fn_%d', 'f.c');",
|
|
i);
|
|
rc_sql = cbm_store_exec(s, sql);
|
|
ASSERT_EQ(rc_sql, 0);
|
|
}
|
|
|
|
/* WAL must exist and be non-empty before the checkpoint call. */
|
|
struct stat st_before;
|
|
int rc_stat = stat(wal_path, &st_before);
|
|
ASSERT_EQ(rc_stat, 0);
|
|
ASSERT(st_before.st_size > 0);
|
|
|
|
/* Under SQLITE_CHECKPOINT_TRUNCATE the WAL would be ftruncate()d to 0
|
|
* bytes on success. Under SQLITE_CHECKPOINT_PASSIVE the file size is
|
|
* preserved (frames marked, not removed). */
|
|
int rc_ckpt = cbm_store_checkpoint(s);
|
|
ASSERT_EQ(rc_ckpt, 0); /* CBM_STORE_OK */
|
|
|
|
struct stat st_after;
|
|
rc_stat = stat(wal_path, &st_after);
|
|
ASSERT_EQ(rc_stat, 0);
|
|
ASSERT(st_after.st_size > 0);
|
|
|
|
cbm_store_close(s);
|
|
unlink(db_path);
|
|
unlink(wal_path);
|
|
unlink(shm_path);
|
|
PASS();
|
|
}
|
|
|
|
|
|
/* #897: any code path installing a fresh DB file must delete the
|
|
* destination's -wal/-shm first. SQLite decides whether to replay a WAL
|
|
* purely from the sidecar's own header/checksums — a leftover WAL from a
|
|
* crashed previous session is recovered ON TOP of the freshly installed
|
|
* file at the next open, splicing old-generation pages into it (short
|
|
* indexes, btreeInitPage failures, or resurrected stale rows).
|
|
*
|
|
* Repro (per the issue): hot-copy a live WAL aside, close cleanly, restore
|
|
* the copy as the crashed-session leftover, install a fresh generation via
|
|
* cbm_store_dump_to_file, reopen — the stale generation's row must NOT be
|
|
* visible and the fresh row must be. */
|
|
static int tsc_copy_file(const char *src, const char *dst) {
|
|
FILE *in = fopen(src, "rb");
|
|
if (!in) {
|
|
return -1;
|
|
}
|
|
FILE *out = fopen(dst, "wb");
|
|
if (!out) {
|
|
(void)fclose(in);
|
|
return -1;
|
|
}
|
|
char buf[4096];
|
|
size_t n;
|
|
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
|
|
if (fwrite(buf, 1, n, out) != n) {
|
|
(void)fclose(in);
|
|
(void)fclose(out);
|
|
return -1;
|
|
}
|
|
}
|
|
(void)fclose(in);
|
|
(void)fclose(out);
|
|
return 0;
|
|
}
|
|
|
|
TEST(dump_install_ignores_stale_wal_sidecar) {
|
|
char *td = th_mktempdir("cbm_stalewal");
|
|
char db_path[512];
|
|
char wal_path[512];
|
|
char stale_copy[512];
|
|
snprintf(db_path, sizeof(db_path), "%s/gen.db", td);
|
|
snprintf(wal_path, sizeof(wal_path), "%s-wal", db_path);
|
|
snprintf(stale_copy, sizeof(stale_copy), "%s/stale.wal", td);
|
|
|
|
/* Generation 1: file-backed store with a marker row living in the WAL. */
|
|
cbm_store_t *s1 = cbm_store_open_path(db_path);
|
|
ASSERT_NOT_NULL(s1);
|
|
cbm_store_upsert_project(s1, "walgen", "/tmp/walgen");
|
|
cbm_node_t stale = {.project = "walgen",
|
|
.label = "Function",
|
|
.name = "stale_gen_node",
|
|
.qualified_name = "walgen.mod.stale_gen_node",
|
|
.file_path = "mod.py",
|
|
.start_line = 1,
|
|
.end_line = 2};
|
|
ASSERT_TRUE(cbm_store_upsert_node(s1, &stale) > 0);
|
|
|
|
/* Hot-copy the live WAL (must be non-empty or the repro is vacuous). */
|
|
struct stat st_wal = {0};
|
|
ASSERT_EQ(stat(wal_path, &st_wal), 0);
|
|
ASSERT_TRUE(st_wal.st_size > 0);
|
|
ASSERT_EQ(tsc_copy_file(wal_path, stale_copy), 0);
|
|
cbm_store_close(s1); /* clean close checkpoints + removes the WAL */
|
|
|
|
/* Simulate the crashed previous session's leftover sidecar. */
|
|
ASSERT_EQ(tsc_copy_file(stale_copy, wal_path), 0);
|
|
|
|
/* Generation 2: fresh store installed over db_path. */
|
|
cbm_store_t *s2 = cbm_store_open_memory();
|
|
ASSERT_NOT_NULL(s2);
|
|
cbm_store_upsert_project(s2, "walgen", "/tmp/walgen");
|
|
cbm_node_t fresh = {.project = "walgen",
|
|
.label = "Function",
|
|
.name = "fresh_gen_node",
|
|
.qualified_name = "walgen.mod.fresh_gen_node",
|
|
.file_path = "mod.py",
|
|
.start_line = 1,
|
|
.end_line = 2};
|
|
ASSERT_TRUE(cbm_store_upsert_node(s2, &fresh) > 0);
|
|
ASSERT_EQ(cbm_store_dump_to_file(s2, db_path), CBM_STORE_OK);
|
|
cbm_store_close(s2);
|
|
|
|
/* Reader: the stale WAL must not have been replayed onto gen 2. */
|
|
cbm_store_t *s3 = cbm_store_open_path(db_path);
|
|
ASSERT_NOT_NULL(s3);
|
|
cbm_node_t *hits = NULL;
|
|
int hit_count = 0;
|
|
ASSERT_EQ(cbm_store_find_nodes_by_name(s3, "walgen", "fresh_gen_node", &hits, &hit_count),
|
|
CBM_STORE_OK);
|
|
ASSERT_TRUE(hit_count >= 1);
|
|
cbm_store_free_nodes(hits, hit_count);
|
|
hits = NULL;
|
|
hit_count = 0;
|
|
ASSERT_EQ(cbm_store_find_nodes_by_name(s3, "walgen", "stale_gen_node", &hits, &hit_count),
|
|
CBM_STORE_OK);
|
|
ASSERT_EQ(hit_count, 0);
|
|
cbm_store_free_nodes(hits, hit_count);
|
|
cbm_store_close(s3);
|
|
|
|
unlink(wal_path);
|
|
char shm_path[512];
|
|
snprintf(shm_path, sizeof(shm_path), "%s-shm", db_path);
|
|
unlink(shm_path);
|
|
unlink(db_path);
|
|
unlink(stale_copy);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(store_checkpoint) {
|
|
RUN_TEST(checkpoint_does_not_truncate_wal);
|
|
RUN_TEST(dump_install_ignores_stale_wal_sidecar);
|
|
}
|