use std::{
ffi::{c_char, c_int, c_uint, CStr, CString},
fs::File,
io::{self, Read, Write},
path::Path,
ptr,
time::{Duration, Instant},
};
use libsql_ffi::{
libsql_wal_disable_checkpoint, libsql_wal_frame_count, libsql_wal_get_frame,
libsql_wal_insert_begin, libsql_wal_insert_end, libsql_wal_insert_frame, sqlite3,
sqlite3_close, sqlite3_column_count, sqlite3_column_text, sqlite3_errmsg, sqlite3_exec,
sqlite3_file, sqlite3_file_control, sqlite3_finalize, sqlite3_free, sqlite3_int64,
sqlite3_open_v2, sqlite3_prepare_v2, sqlite3_step, SQLITE_BUSY, SQLITE_DONE, SQLITE_ERROR,
SQLITE_FCNTL_FILE_POINTER, SQLITE_MISMATCH, SQLITE_MISUSE, SQLITE_OK, SQLITE_OPEN_CREATE,
SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_READWRITE, SQLITE_ROW, SQLITE_TOOBIG,
};
use crate::{Error, Result};
pub const FRAME_HEADER: usize = 24;
const WAL_HEADER: usize = 32;
const COPY_CHUNK: usize = 1 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Checkpoint {
pub busy: bool,
pub log: u32,
pub checkpointed: u32,
}
impl Checkpoint {
#[must_use]
pub fn complete(&self) -> bool {
!self.busy && self.log == self.checkpointed
}
}
pub struct RawDb {
handle: *mut sqlite3,
}
unsafe impl Send for RawDb {}
impl Drop for RawDb {
fn drop(&mut self) {
unsafe { sqlite3_close(self.handle) };
}
}
impl std::fmt::Debug for RawDb {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RawDb").finish_non_exhaustive()
}
}
impl RawDb {
pub fn open(path: &Path, busy_timeout: Duration) -> Result<Self> {
let db = Self::open_with_flags(path, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)?;
db.exec(&format!("PRAGMA busy_timeout={}", busy_timeout.as_millis()))?;
let mode = db.query_text("PRAGMA journal_mode=WAL")?;
if mode != "wal" {
return Err(Error::Sqlite {
context: format!("enabling WAL on {}", path.display()),
code: SQLITE_ERROR,
message: format!("journal_mode is {mode}"),
});
}
Ok(db)
}
fn open_with_flags(path: &Path, flags: c_int) -> Result<Self> {
let c_path = CString::new(path.as_os_str().as_encoded_bytes())
.map_err(|_| Error::Nul("database path"))?;
let mut handle = ptr::null_mut();
let rc = unsafe {
sqlite3_open_v2(
c_path.as_ptr(),
&raw mut handle,
flags | SQLITE_OPEN_FULLMUTEX,
ptr::null(),
)
};
if rc != SQLITE_OK {
let message = if handle.is_null() {
"out of memory".to_owned()
} else {
let m = unsafe { CStr::from_ptr(sqlite3_errmsg(handle)) }
.to_string_lossy()
.into_owned();
unsafe { sqlite3_close(handle) };
m
};
return Err(Error::Sqlite {
context: format!("opening {}", path.display()),
code: rc,
message,
});
}
Ok(Self { handle })
}
#[must_use]
pub fn as_ptr(&self) -> *mut sqlite3 {
self.handle
}
fn check(&self, rc: c_int, context: impl FnOnce() -> String) -> Result<()> {
if rc == SQLITE_OK {
Ok(())
} else {
Err(self.error(rc, context()))
}
}
fn error(&self, rc: c_int, context: String) -> Error {
Error::Sqlite {
context,
code: rc,
message: self.errmsg(),
}
}
fn errmsg(&self) -> String {
unsafe { CStr::from_ptr(sqlite3_errmsg(self.handle)) }
.to_string_lossy()
.into_owned()
}
pub fn disable_checkpoint(&self) -> Result<()> {
let rc = unsafe { libsql_wal_disable_checkpoint(self.handle) };
self.check(rc, || "wal_disable_checkpoint".into())
}
pub fn exec(&self, sql: &str) -> Result<()> {
let c_sql = CString::new(sql).map_err(|_| Error::Nul("SQL"))?;
let mut err: *mut c_char = ptr::null_mut();
let rc = unsafe {
sqlite3_exec(
self.handle,
c_sql.as_ptr(),
None,
ptr::null_mut(),
&raw mut err,
)
};
if rc == SQLITE_OK {
return Ok(());
}
let message = if err.is_null() {
self.errmsg()
} else {
let m = unsafe { CStr::from_ptr(err) }
.to_string_lossy()
.into_owned();
unsafe { sqlite3_free(err.cast()) };
m
};
Err(Error::Sqlite {
context: format!("exec `{sql}`"),
code: rc,
message,
})
}
pub fn query_row(&self, sql: &str) -> Result<Vec<String>> {
let c_sql = CString::new(sql).map_err(|_| Error::Nul("SQL"))?;
let mut stmt = ptr::null_mut();
let rc = unsafe {
sqlite3_prepare_v2(
self.handle,
c_sql.as_ptr(),
-1,
&raw mut stmt,
ptr::null_mut(),
)
};
self.check(rc, || format!("prepare `{sql}`"))?;
let result = match unsafe { sqlite3_step(stmt) } {
SQLITE_ROW => {
let n = unsafe { sqlite3_column_count(stmt) };
Ok((0..n)
.map(|i| {
let p = unsafe { sqlite3_column_text(stmt, i) };
if p.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(p.cast()) }
.to_string_lossy()
.into_owned()
}
})
.collect())
}
SQLITE_DONE => Ok(Vec::new()),
rc => Err(self.error(rc, format!("step `{sql}`"))),
};
unsafe { sqlite3_finalize(stmt) };
result
}
pub fn query_text(&self, sql: &str) -> Result<String> {
Ok(self.query_row(sql)?.into_iter().next().unwrap_or_default())
}
pub fn query_u32(&self, sql: &str) -> Result<u32> {
let text = self.query_text(sql)?;
text.parse().map_err(|_| Error::Sqlite {
context: format!("query `{sql}`"),
code: SQLITE_MISMATCH,
message: format!("expected a number, got {text:?}"),
})
}
pub fn page_size(&self) -> Result<u32> {
self.query_u32("PRAGMA page_size")
}
pub fn try_frame_count(&self) -> Result<Option<u32>> {
let mut n: c_uint = 0;
match unsafe { libsql_wal_frame_count(self.handle, &raw mut n) } {
SQLITE_OK => Ok(Some(n)),
SQLITE_BUSY => Ok(None),
rc => Err(self.error(rc, "wal_frame_count".into())),
}
}
pub fn head(&self, wait: Duration) -> Result<Option<u32>> {
self.query_row("SELECT count(*) FROM sqlite_master")?;
let start = Instant::now();
loop {
if let Some(n) = self.try_frame_count()? {
return Ok(Some(n));
}
if start.elapsed() >= wait {
return Ok(None);
}
std::thread::sleep(Duration::from_micros(500));
}
}
pub fn get_frame(&self, no: u32, page_size: u32) -> Result<Vec<u8>> {
if !(512..=65536).contains(&page_size) {
return Err(Error::Sqlite {
context: format!("wal_get_frame {no}"),
code: SQLITE_MISUSE,
message: format!("{page_size} isn't a SQLite page size"),
});
}
let mut buf = vec![0u8; FRAME_HEADER + page_size as usize];
let len = buf.len() as c_uint;
let rc = unsafe { libsql_wal_get_frame(self.handle, no, buf.as_mut_ptr().cast(), len) };
self.check(rc, || format!("wal_get_frame {no}"))?;
Ok(buf)
}
pub fn insert_begin(&self) -> Result<()> {
let rc = unsafe { libsql_wal_insert_begin(self.handle) };
self.check(rc, || "wal_insert_begin".into())
}
pub fn insert_end(&self) -> Result<()> {
let rc = unsafe { libsql_wal_insert_end(self.handle) };
self.check(rc, || "wal_insert_end".into())
}
pub fn insert_frame(&self, no: u32, frame: &[u8]) -> Result<()> {
let len = c_uint::try_from(frame.len()).map_err(|_| Error::Sqlite {
context: format!("wal_insert_frame {no}"),
code: SQLITE_TOOBIG,
message: "frame too large".into(),
})?;
let mut conflict: c_int = 0;
let rc = unsafe {
libsql_wal_insert_frame(
self.handle,
no,
frame.as_ptr().cast_mut().cast(),
len,
&raw mut conflict,
)
};
if conflict != 0 {
return Err(Error::Conflict { frame: no });
}
self.check(rc, || format!("wal_insert_frame {no}"))
}
pub fn checkpoint_truncate(&self) -> Result<Checkpoint> {
self.checkpoint("TRUNCATE")
}
pub fn checkpoint_passive(&self) -> Result<Checkpoint> {
self.checkpoint("PASSIVE")
}
fn checkpoint(&self, mode: &str) -> Result<Checkpoint> {
let row = self.query_row(&format!("PRAGMA wal_checkpoint({mode})"))?;
let bad = || Error::Sqlite {
context: format!("wal_checkpoint({mode})"),
code: SQLITE_ERROR,
message: format!("unexpected result {row:?}"),
};
let [busy, log, checkpointed] = row.as_slice() else {
return Err(bad());
};
let num = |s: &str| s.parse::<i64>().map_err(|_| bad());
let count = |s: &str| num(s).map(|n| u32::try_from(n.max(0)).unwrap_or(u32::MAX));
Ok(Checkpoint {
busy: num(busy)? != 0,
log: count(log)?,
checkpointed: count(checkpointed)?,
})
}
pub fn copy_main_file(&self, dest: &Path) -> Result<u64> {
let mut file: *mut sqlite3_file = ptr::null_mut();
let rc = unsafe {
sqlite3_file_control(
self.handle,
c"main".as_ptr(),
SQLITE_FCNTL_FILE_POINTER,
ptr::from_mut(&mut file).cast(),
)
};
self.check(rc, || "file_control(FILE_POINTER)".into())?;
let not_open = || Error::Sqlite {
context: "copying the main file".into(),
code: SQLITE_MISUSE,
message: "the main database file isn't open".into(),
};
let methods = unsafe { file.as_ref() }
.map(|f| f.pMethods)
.filter(|m| !m.is_null())
.ok_or_else(not_open)?;
let (Some(x_read), Some(x_file_size)) =
(unsafe { ((*methods).xRead, (*methods).xFileSize) })
else {
return Err(not_open());
};
let mut size: sqlite3_int64 = 0;
let rc = unsafe { x_file_size(file, &raw mut size) };
self.check(rc, || "xFileSize".into())?;
let mut out = File::create(dest)?;
let mut buf = vec![0u8; COPY_CHUNK];
let mut offset: sqlite3_int64 = 0;
while offset < size {
let n = usize::try_from(size - offset).map_or(buf.len(), |left| left.min(buf.len()));
let rc = unsafe { x_read(file, buf.as_mut_ptr().cast(), n as c_int, offset) };
self.check(rc, || format!("xRead of {n} bytes at {offset}"))?;
out.write_all(&buf[..n])?;
offset += n as sqlite3_int64;
}
out.sync_all()?;
Ok(size.unsigned_abs())
}
}
pub unsafe fn disable_checkpoint(db: *mut sqlite3) -> Result<()> {
match unsafe { libsql_wal_disable_checkpoint(db) } {
SQLITE_OK => Ok(()),
rc => Err(Error::Sqlite {
context: "wal_disable_checkpoint".into(),
code: rc,
message: unsafe { CStr::from_ptr(sqlite3_errmsg(db)) }
.to_string_lossy()
.into_owned(),
}),
}
}
#[must_use]
pub fn size_after(frame: &[u8]) -> u32 {
u32::from_be_bytes(frame[4..8].try_into().unwrap())
}
pub fn wal_salt(wal_path: &Path) -> io::Result<Option<[u8; 8]>> {
let mut file = match File::open(wal_path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(e),
};
let mut header = [0u8; WAL_HEADER];
match file.read_exact(&mut header) {
Ok(()) => Ok(Some(header[16..24].try_into().unwrap())),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(None),
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
const SECOND: Duration = Duration::from_secs(1);
fn scratch(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("walship-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn locked_from_another_process(path: &Path) -> bool {
const SHARED_FIRST: libc::off_t = 0x4000_0000 + 2;
const SHARED_SIZE: libc::off_t = 510;
let c_path = CString::new(path.as_os_str().as_encoded_bytes()).unwrap();
match unsafe { libc::fork() } {
-1 => panic!("fork failed"),
0 => {
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_RDWR) };
if fd < 0 {
unsafe { libc::_exit(2) };
}
let mut lock: libc::flock = unsafe { std::mem::zeroed() };
lock.l_type = libc::F_WRLCK as _;
lock.l_whence = libc::SEEK_SET as _;
lock.l_start = SHARED_FIRST;
lock.l_len = SHARED_SIZE;
let rc = unsafe { libc::fcntl(fd, libc::F_SETLK, &mut lock) };
unsafe { libc::_exit(i32::from(rc != 0)) };
}
pid => {
let mut status = 0;
assert_eq!(unsafe { libc::waitpid(pid, &raw mut status, 0) }, pid);
assert!(libc::WIFEXITED(status), "lock probe was killed");
let code = libc::WEXITSTATUS(status);
assert!(code < 2, "lock probe couldn't open {}", path.display());
code == 1
}
}
}
#[test]
fn copying_the_main_file_keeps_the_lock() {
let dir = scratch("copy");
let path = dir.join("live.db");
let snapshot = dir.join("live.snapshot");
let db = RawDb::open(&path, SECOND).unwrap();
db.disable_checkpoint().unwrap();
db.exec("CREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT)")
.unwrap();
db.exec("INSERT INTO t(v) VALUES ('a'),('b')").unwrap();
assert!(locked_from_another_process(&path));
assert!(db.checkpoint_truncate().unwrap().complete());
let n = db.copy_main_file(&snapshot).unwrap();
assert_eq!(n, std::fs::metadata(&path).unwrap().len());
assert!(locked_from_another_process(&path), "copy dropped the lock");
let bytes = std::fs::read(&snapshot).unwrap();
assert_eq!((bytes[18], bytes[19]), (2, 2));
let copy = RawDb::open(&snapshot, SECOND).unwrap();
assert_eq!(copy.query_text("PRAGMA integrity_check").unwrap(), "ok");
assert_eq!(copy.query_u32("SELECT count(*) FROM t").unwrap(), 2);
drop(copy);
assert!(locked_from_another_process(&path));
db.exec("INSERT INTO t(v) VALUES ('c')").unwrap();
assert!(db.head(SECOND).unwrap().unwrap() > 0);
assert_eq!(db.query_u32("SELECT count(*) FROM t").unwrap(), 3);
let live = std::fs::read(&path).unwrap();
assert!(
!locked_from_another_process(&path),
"a plain open and close should have dropped the lock"
);
assert_eq!(bytes, live);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn frames_round_trip_between_two_handles() {
let dir = scratch("ship");
let src = RawDb::open(&dir.join("p.db"), SECOND).unwrap();
src.disable_checkpoint().unwrap();
let page_size = src.page_size().unwrap();
src.exec("CREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT)")
.unwrap();
src.exec("INSERT INTO t(v) VALUES ('a'),('b')").unwrap();
src.exec("BEGIN").unwrap();
for i in 0..2000 {
src.exec(&format!(
"INSERT INTO t(v) VALUES ('{}')",
"x".repeat(200 + i % 7)
))
.unwrap();
}
src.exec("COMMIT").unwrap();
let head = src.head(SECOND).unwrap().unwrap();
assert!(head > 10, "head={head}");
let frames: Vec<Vec<u8>> = (1..=head)
.map(|n| src.get_frame(n, page_size).unwrap())
.collect();
let frame = |n: u32| &frames[n as usize - 1];
assert!(size_after(frame(head)) > 0, "last frame should commit");
let commits: Vec<u32> = (1..=head).filter(|&n| size_after(frame(n)) > 0).collect();
let last_small = commits[commits.len() - 2];
let cut = last_small + 3;
assert!(cut < head && size_after(frame(cut)) == 0);
{
let dst = RawDb::open(&dir.join("r.db"), SECOND).unwrap();
dst.disable_checkpoint().unwrap();
dst.insert_begin().unwrap();
for n in 1..=cut {
dst.insert_frame(n, frame(n)).unwrap();
if size_after(frame(n)) > 0 {
dst.insert_end().unwrap();
dst.insert_begin().unwrap();
}
}
dst.insert_end().unwrap();
assert_eq!(dst.head(SECOND).unwrap(), Some(last_small));
}
let dst = RawDb::open(&dir.join("r.db"), SECOND).unwrap();
dst.disable_checkpoint().unwrap();
assert_eq!(dst.head(SECOND).unwrap(), Some(last_small));
assert_eq!(dst.query_u32("SELECT count(*) FROM t").unwrap(), 2);
dst.insert_begin().unwrap();
for n in last_small + 1..=head {
dst.insert_frame(n, frame(n)).unwrap();
}
dst.insert_end().unwrap();
assert_eq!(dst.head(SECOND).unwrap(), Some(head));
assert_eq!(dst.query_u32("SELECT count(*) FROM t").unwrap(), 2002);
assert_eq!(dst.query_text("PRAGMA integrity_check").unwrap(), "ok");
dst.insert_begin().unwrap();
dst.insert_frame(head, frame(head)).unwrap();
let mut bad = frame(head).clone();
bad[FRAME_HEADER + 100] ^= 0xff;
assert!(matches!(
dst.insert_frame(head, &bad),
Err(Error::Conflict { frame }) if frame == head
));
dst.insert_end().unwrap();
let wal = dir.join("p.db-wal");
let salt = wal_salt(&wal).unwrap().unwrap();
assert!(src.checkpoint_truncate().unwrap().complete());
assert_eq!(wal_salt(&wal).unwrap(), None);
src.exec("INSERT INTO t(v) VALUES ('z')").unwrap();
assert_eq!(src.head(SECOND).unwrap(), Some(1));
assert_ne!(wal_salt(&wal).unwrap().unwrap(), salt);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn head_waits_for_a_writer() {
let dir = scratch("busy");
let path = dir.join("p.db");
let reader = RawDb::open(&path, SECOND).unwrap();
reader.exec("CREATE TABLE t(v)").unwrap();
let writer = RawDb::open(&path, SECOND).unwrap();
writer.exec("BEGIN IMMEDIATE").unwrap();
writer.exec("INSERT INTO t VALUES (1)").unwrap();
assert_eq!(reader.try_frame_count().unwrap(), None);
assert_eq!(reader.head(Duration::from_millis(50)).unwrap(), None);
let commit = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
writer.exec("COMMIT").unwrap();
});
let n = reader.head(Duration::from_secs(2)).unwrap();
commit.join().unwrap();
assert!(n.unwrap() >= 2, "{n:?}");
let _ = std::fs::remove_dir_all(&dir);
}
fn primary_with(dir: &Path, n: usize) -> (RawDb, Vec<Vec<u8>>) {
let src = RawDb::open(&dir.join("p.db"), SECOND).unwrap();
src.disable_checkpoint().unwrap();
src.exec("CREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT)")
.unwrap();
for i in 0..n {
src.exec(&format!("INSERT INTO t(v) VALUES ('row {i}')"))
.unwrap();
}
let page_size = src.page_size().unwrap();
let head = src.head(SECOND).unwrap().unwrap();
let frames = (1..=head)
.map(|no| src.get_frame(no, page_size).unwrap())
.collect();
(src, frames)
}
#[test]
fn the_frame_count_is_cached_until_a_schema_read() {
let dir = scratch("stale");
let path = dir.join("p.db");
let reader = RawDb::open(&path, SECOND).unwrap();
reader.disable_checkpoint().unwrap();
reader.exec("CREATE TABLE t(v)").unwrap();
let before = reader.try_frame_count().unwrap().unwrap();
let writer = RawDb::open(&path, SECOND).unwrap();
writer.disable_checkpoint().unwrap();
writer.exec("INSERT INTO t VALUES (1)").unwrap();
assert_eq!(reader.try_frame_count().unwrap(), Some(before));
reader.query_text("SELECT 1").unwrap();
assert_eq!(reader.try_frame_count().unwrap(), Some(before));
assert!(reader.head(SECOND).unwrap().unwrap() > before);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn an_insert_past_the_end_is_appended_without_complaint() {
let dir = scratch("gap");
let (_src, frames) = primary_with(&dir, 4);
let commits: Vec<u32> = (1..)
.zip(&frames)
.filter(|(_, f)| size_after(f) > 0)
.map(|(no, _)| no)
.collect();
let first = commits[0];
assert_eq!(&commits[1..3], &[first + 1, first + 2], "{commits:?}");
let dst = RawDb::open(&dir.join("r.db"), SECOND).unwrap();
dst.disable_checkpoint().unwrap();
dst.insert_begin().unwrap();
for (no, frame) in (1..=first).zip(&frames) {
dst.insert_frame(no, frame).unwrap();
}
dst.insert_end().unwrap();
assert_eq!(dst.head(SECOND).unwrap(), Some(first));
let skip = first + 2;
dst.insert_begin().unwrap();
dst.insert_frame(skip, &frames[skip as usize - 1]).unwrap();
dst.insert_end().unwrap();
assert_eq!(dst.head(SECOND).unwrap(), Some(first + 1));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_reader_sees_a_transaction_once_its_commit_frame_is_in() {
let dir = scratch("visible");
let (_src, frames) = primary_with(&dir, 3);
let path = dir.join("r.db");
let dst = RawDb::open(&path, SECOND).unwrap();
dst.disable_checkpoint().unwrap();
dst.insert_begin().unwrap();
for (no, frame) in (1..).zip(&frames) {
dst.insert_frame(no, frame).unwrap();
}
let reader = RawDb::open(&path, SECOND).unwrap();
assert_eq!(reader.query_u32("SELECT count(*) FROM t").unwrap(), 3);
dst.insert_end().unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_full_passive_fold_restarts_the_log_on_the_next_write() {
let dir = scratch("fold");
let (src, _) = primary_with(&dir, 5);
let wal = dir.join("p.db-wal");
let salt = wal_salt(&wal).unwrap().unwrap();
let ck = src.checkpoint_passive().unwrap();
assert!(!ck.busy && ck.log == ck.checkpointed, "{ck:?}");
src.exec("INSERT INTO t(v) VALUES ('after')").unwrap();
assert_eq!(src.head(SECOND).unwrap(), Some(1));
assert_ne!(wal_salt(&wal).unwrap().unwrap(), salt);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn frames_onto_a_vacuumed_copy_go_in_and_give_wrong_rows() {
let dir = scratch("vacuum");
let src = RawDb::open(&dir.join("p.db"), SECOND).unwrap();
src.disable_checkpoint().unwrap();
src.exec("CREATE TABLE t(id INTEGER PRIMARY KEY, v TEXT)")
.unwrap();
src.exec("CREATE INDEX t_v ON t(v)").unwrap();
for i in 0..300 {
src.exec(&format!(
"INSERT INTO t(v) VALUES ('{i}-{}')",
"x".repeat(i % 90)
))
.unwrap();
}
src.exec("DELETE FROM t WHERE id % 3 = 0").unwrap();
assert!(src.checkpoint_truncate().unwrap().complete());
let copy = dir.join("r.db");
src.exec(&format!("VACUUM INTO '{}'", copy.display()))
.unwrap();
for i in 0..50 {
src.exec(&format!("INSERT INTO t(v) VALUES ('late {i}')"))
.unwrap();
}
let page_size = src.page_size().unwrap();
let head = src.head(SECOND).unwrap().unwrap();
let dst = RawDb::open(©, SECOND).unwrap();
dst.disable_checkpoint().unwrap();
dst.insert_begin().unwrap();
for no in 1..=head {
dst.insert_frame(no, &src.get_frame(no, page_size).unwrap())
.unwrap();
}
dst.insert_end().unwrap();
assert_eq!(dst.head(SECOND).unwrap(), Some(head));
assert_eq!(dst.query_text("PRAGMA integrity_check").unwrap(), "ok");
assert_ne!(
dst.query_u32("SELECT count(*) FROM t").unwrap(),
src.query_u32("SELECT count(*) FROM t").unwrap()
);
let _ = std::fs::remove_dir_all(&dir);
}
}