Skip to main content

clt_database/storage/
wal.rs

1#![allow(clippy::not_unsafe_ptr_arg_deref)]
2
3use crate::io::FileSyncType;
4use crate::sync::Mutex;
5use crate::sync::OnceLock;
6use crate::{turso_assert, turso_assert_greater_than, turso_debug_assert};
7use branches::mark_unlikely;
8use rustc_hash::{FxHashMap, FxHashSet};
9use std::array;
10use std::borrow::Cow;
11use std::collections::BTreeMap;
12use std::num::NonZeroUsize;
13use strum::EnumString;
14use tracing::{instrument, Level};
15
16use crate::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
17use crate::sync::RwLock;
18use bitflags::bitflags;
19use std::fmt::{Debug, Formatter};
20use std::{fmt, sync::Arc};
21
22use super::buffer_pool::BufferPool;
23use super::pager::{PageRef, Pager};
24use super::sqlite3_ondisk::{
25    self, checksum_wal, DatabaseHeader, WalHeader, WAL_MAGIC_BE, WAL_MAGIC_LE,
26};
27use crate::fast_lock::SpinLock;
28use crate::io::clock::MonotonicInstant;
29use crate::io::CompletionGroup;
30use crate::io::{File, IO};
31use crate::storage::database::{DatabaseStorage, EncryptionOrChecksum};
32#[cfg(host_shared_wal)]
33use crate::storage::shared_wal_coordination::SharedWalCoordinationOpenMode;
34#[cfg(host_shared_wal)]
35use crate::storage::shared_wal_coordination::{
36    MappedSharedWalCoordination, SharedOwnerRecord, SharedReaderSlot, SharedWalCoordinationHeader,
37};
38use crate::storage::sqlite3_ondisk::{
39    begin_read_wal_frame, begin_read_wal_frame_raw, finish_read_page, prepare_wal_frame,
40    write_pages_vectored, PageSize, WAL_FRAME_HEADER_SIZE, WAL_HEADER_SIZE,
41};
42use crate::types::{IOCompletions, IOResult};
43use crate::util::IOExt as _;
44use crate::{
45    bail_corrupt_error, io_yield_one, Buffer, Completion, CompletionError, IOContext, LimboError,
46    Result,
47};
48
49/// this contains the frame to rollback to and its associated checksum.
50#[derive(Debug, Clone)]
51pub struct RollbackTo {
52    pub frame: u64,
53    pub checksum: (u32, u32),
54    /// WAL checkpoint sequence (generation) the position was captured in;
55    /// asserted against the current generation on rollback.
56    pub checkpoint_seq: u32,
57}
58
59#[derive(Debug, Clone, Default)]
60pub struct CheckpointResult {
61    /// max frame in the WAL after checkpoint
62    /// note, that as we TRUNCATE wal outside of the main checkpoint routine - this field will be set to non-zero number even for TRUNCATE mode
63    pub wal_max_frame: u64,
64    /// total amount of frames backfilled to the DB file after checkpoint
65    pub wal_total_backfilled: u64,
66    /// amount of new frames backfilled to the DB file during this checkpoint procedure
67    pub wal_checkpoint_backfilled: u64,
68    /// In the case of everything backfilled, we need to hold the locks until the db
69    /// file is truncated.
70    maybe_guard: Option<CheckpointLocks>,
71    pub db_truncate_sent: bool,
72    pub db_sync_sent: bool,
73    /// Whether WAL truncation I/O has been submitted (for TRUNCATE checkpoint mode)
74    pub wal_truncate_sent: bool,
75    /// Whether WAL sync I/O has been submitted after truncation
76    pub wal_sync_sent: bool,
77}
78
79impl Drop for CheckpointResult {
80    fn drop(&mut self) {
81        let _ = self.maybe_guard.take();
82    }
83}
84
85impl CheckpointResult {
86    pub fn new(
87        wal_max_frame: u64,
88        wal_total_backfilled: u64,
89        wal_checkpoint_backfilled: u64,
90    ) -> Self {
91        Self {
92            wal_max_frame,
93            wal_total_backfilled,
94            wal_checkpoint_backfilled,
95            maybe_guard: None,
96            db_sync_sent: false,
97            db_truncate_sent: false,
98            wal_truncate_sent: false,
99            wal_sync_sent: false,
100        }
101    }
102
103    pub const fn everything_backfilled(&self) -> bool {
104        self.wal_max_frame == self.wal_total_backfilled
105    }
106    pub fn should_truncate(&self) -> bool {
107        // TRUNCATE should also clear any stale WAL bytes when the log was restarted
108        // (wal_max_frame=0) but the file still contains old frames.
109        self.everything_backfilled()
110    }
111    pub fn release_guard(&mut self) {
112        let _ = self.maybe_guard.take();
113    }
114}
115
116#[cfg(host_shared_wal)]
117pub(crate) fn coordination_path_for_wal_path(wal_path: &str) -> String {
118    if let Some(db_path) = wal_path.strip_suffix("-wal") {
119        format!("{db_path}-tshm")
120    } else {
121        format!("{wal_path}-tshm")
122    }
123}
124
125bitflags! {
126    /// Automatic WAL maintenance actions a caller permits the engine to take
127    /// during routine operations (begin write tx, commit, shutdown).
128    ///
129    /// Callers that manage WAL state out-of-band — e.g. the sync engine,
130    /// which keeps its own watermarks across the WAL header — pass an
131    /// explicit subset so unrelated bookkeeping remains untouched. The
132    /// previous single `wal_auto_checkpoint_disabled` boolean conflated both
133    /// auto-checkpoint and WAL header restart; spelling them out separately
134    /// avoids breaking sync-engine assumptions whenever one of the two is
135    /// disabled.
136    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
137    pub struct WalAutoActions: u8 {
138        /// Run an auto-checkpoint after commit when `should_checkpoint()`
139        /// is true, and the truncate-checkpoint on connection shutdown.
140        const Checkpoint = 0b01;
141        /// Restart the WAL header in `try_restart_log_before_write` when
142        /// every frame has been backfilled, before starting a write tx.
143        const Restart    = 0b10;
144    }
145}
146
147impl WalAutoActions {
148    /// Default policy for ordinary connections: every auto action allowed.
149    pub const fn all_enabled() -> Self {
150        Self::from_bits_truncate(Self::Checkpoint.bits() | Self::Restart.bits())
151    }
152}
153
154#[derive(Debug, Copy, Clone, PartialEq, EnumString)]
155#[strum(ascii_case_insensitive)]
156pub enum CheckpointMode {
157    /// Checkpoint as many frames as possible without waiting for any database readers or writers to finish, then sync the database file if all frames in the log were checkpointed.
158    /// Passive never blocks readers or writers, only ensures (like all modes do) that there are no other checkpointers.
159    ///
160    /// Optional upper_bound_inclusive parameter can be set in order to checkpoint frames with number no larger than the parameter
161    Passive { upper_bound_inclusive: Option<u64> },
162    /// This mode blocks until there is no database writer and all readers are reading from the most recent database snapshot. It then checkpoints all frames in the log file and syncs the database file. This mode blocks new database writers while it is pending, but new database readers are allowed to continue unimpeded.
163    Full,
164    /// This mode works the same way as `Full` with the addition that after checkpointing the log file it blocks (calls the busy-handler callback) until all readers are reading from the database file only. This ensures that the next writer will restart the log file from the beginning. Like `Full`, this mode blocks new database writer attempts while it is pending, but does not impede readers.
165    Restart,
166    /// This mode works the same way as `Restart` with the addition that it also truncates the log file to zero bytes just prior to a successful return.
167    ///
168    /// Extra parameter can be set in order to perform conditional TRUNCATE: database will be checkpointed and truncated only if max_frames equals to the parameter value
169    /// this behaviour used by sync-engine which consolidate WAL before checkpoint and needs to be sure that no frames will be missed
170    Truncate { upper_bound_inclusive: Option<u64> },
171}
172
173impl CheckpointMode {
174    pub(crate) fn should_restart_log(&self) -> bool {
175        matches!(
176            self,
177            CheckpointMode::Truncate { .. } | CheckpointMode::Restart
178        )
179    }
180    /// All modes other than Passive require a complete backfilling of all available frames
181    /// from `shared.metadata.nbackfills + 1 -> shared.metadata.max_frame`
182    fn require_all_backfilled(&self) -> bool {
183        !matches!(self, CheckpointMode::Passive { .. })
184    }
185}
186
187/// Immutable view of the WAL metadata a connection snapshots from shared state.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189struct WalSnapshot {
190    max_frame: u64,
191    nbackfills: u64,
192    last_checksum: (u32, u32),
193    checkpoint_seq: u32,
194    transaction_count: u64,
195}
196
197impl WalSnapshot {
198    /// First frame that is still visible in the WAL after checkpoint backfill.
199    const fn min_frame(self) -> u64 {
200        self.nbackfills + 1
201    }
202}
203
204/// Which read-mark, if any, currently protects this connection's snapshot.
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206enum ReadGuardKind {
207    None,
208    DbFile,
209    ReadMark(NonZeroUsize),
210}
211
212impl ReadGuardKind {
213    /// Convert the lock index stored on `WalFile` into a semantic guard kind.
214    const fn from_lock_index(lock_index: usize) -> Self {
215        match lock_index {
216            NO_LOCK_HELD => Self::None,
217            0 => Self::DbFile,
218            idx => Self::ReadMark(NonZeroUsize::new(idx).expect("idx checked to be non-zero")),
219        }
220    }
221
222    /// Convert the semantic guard kind back into the legacy lock index representation.
223    fn lock_index(self) -> usize {
224        match self {
225            Self::None => NO_LOCK_HELD,
226            Self::DbFile => 0,
227            Self::ReadMark(idx) => idx.into(),
228        }
229    }
230}
231
232/// Connection-local WAL state derived from a shared snapshot plus a held read guard.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234struct WalConnectionState {
235    snapshot: WalSnapshot,
236    read_guard: ReadGuardKind,
237}
238
239impl WalConnectionState {
240    /// Build a new connection-local WAL state bundle.
241    const fn new(snapshot: WalSnapshot, read_guard: ReadGuardKind) -> Self {
242        Self {
243            snapshot,
244            read_guard,
245        }
246    }
247
248    /// Replace just the shared snapshot while preserving the current read guard.
249    const fn with_snapshot(self, snapshot: WalSnapshot) -> Self {
250        Self {
251            snapshot,
252            read_guard: self.read_guard,
253        }
254    }
255}
256
257#[repr(transparent)]
258#[derive(Debug, Default)]
259/// A 64-bit read-write lock with embedded 32-bit value storage.
260/// Using a single Atomic allows the reader count and lock state are updated
261/// atomically together while sitting in a single cpu cache line.
262///
263/// # Memory Layout:
264/// ```ignore
265/// [63:32] Value bits    - 32 bits for stored value
266/// [31:1]  Reader count  - 31 bits for reader count
267/// [0]     Writer bit    - 1 bit indicating exclusive write lock
268/// ```
269///
270/// # Synchronization Guarantees:
271/// - Acquire semantics on lock acquisition ensure visibility of all writes
272///   made by the previous lock holder
273/// - Release semantics on unlock ensure all writes made while holding the
274///   lock are visible to the next acquirer
275/// - The embedded value can be atomically read without holding any lock
276pub struct TursoRwLock(AtomicU64);
277
278pub const READMARK_NOT_USED: u32 = 0xffffffff;
279const NO_LOCK_HELD: usize = usize::MAX;
280
281impl TursoRwLock {
282    /// Bit 0: Writer flag
283    const WRITER: u64 = 0b1;
284
285    /// Reader increment value (bit 1)
286    const READER_INC: u64 = 0b10;
287
288    /// Reader count starts at bit 1
289    const READER_SHIFT: u32 = 1;
290
291    /// Mask for 31 reader bits [31:1]
292    const READER_COUNT_MASK: u64 = 0x7fff_ffffu64 << Self::READER_SHIFT;
293
294    /// Value starts at bit 32
295    const VALUE_SHIFT: u32 = 32;
296
297    /// Mask for 32 value bits [63:32]
298    const VALUE_MASK: u64 = 0xffff_ffffu64 << Self::VALUE_SHIFT;
299
300    #[inline]
301    pub const fn new() -> Self {
302        Self(AtomicU64::new(0))
303    }
304
305    const fn has_writer(val: u64) -> bool {
306        val & Self::WRITER != 0
307    }
308
309    const fn has_readers(val: u64) -> bool {
310        val & Self::READER_COUNT_MASK != 0
311    }
312
313    #[inline]
314    /// Try to acquire a shared read lock.
315    pub fn read(&self) -> bool {
316        let mut count = 0;
317        // Bounded loop to avoid infinite loops
318        // Retry on Reader contention (should hopefully be spurious)
319        while count < 1_000_000 {
320            let cur = self.0.load(Ordering::Acquire);
321            // If a writer is present we cannot proceed.
322            if Self::has_writer(cur) {
323                return false;
324            }
325            // 2 billion readers is a high enough number where we will skip the branch
326            // and assume that we are not overflowing :)
327            let desired = cur.wrapping_add(Self::READER_INC);
328            // for success, Acquire establishes happens-before relationship with the previous Release from unlock
329            // for failure we only care about reading it for the next iteration so we can use Relaxed.
330            let res = self
331                .0
332                .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed);
333            if res.is_err() {
334                count += 1;
335                crate::thread::spin_loop();
336                continue;
337            }
338            return true;
339        }
340        // Too much reader contention return Busy
341        false
342    }
343
344    /// Try to take an exclusive lock. Succeeds if no readers and no writer.
345    #[inline]
346    pub fn write(&self) -> bool {
347        let cur = self.0.load(Ordering::Acquire);
348        // exclusive lock, so require no readers and no writer
349        if Self::has_writer(cur) || Self::has_readers(cur) {
350            return false;
351        }
352        let desired = cur | Self::WRITER;
353        self.0 // Safety: Failure here can be Relaxed as we will read again on next iteration.
354            .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed)
355            .is_ok()
356    }
357
358    /// upgrade read lock to the write lock
359    /// only possible if there is exactly single reader at the moment
360    /// return true if lock was upgraded succesfully - and false otherwise
361    #[inline]
362    pub fn upgrade(&self) -> bool {
363        let cur = self.0.load(Ordering::Acquire);
364        // Check for single reader: exactly one reader, any value
365        if (cur & !Self::VALUE_MASK) != Self::READER_INC {
366            return false;
367        }
368        // Preserve value bits, replace reader with writer
369        let desired = (cur & Self::VALUE_MASK) | Self::WRITER;
370        self.0
371            .compare_exchange(cur, desired, Ordering::Acquire, Ordering::Relaxed)
372            .is_ok()
373    }
374
375    /// downgrade write lock to the read lock
376    /// MUST be called for a lock acquired by the writer
377    #[inline]
378    pub fn downgrade(&self) {
379        let cur = self.0.load(Ordering::Acquire);
380        turso_debug_assert!(Self::has_writer(cur));
381        // Preserve value bits, replace writer with one reader
382        let desired = (cur & Self::VALUE_MASK) | Self::READER_INC;
383        #[cfg(debug_assertions)]
384        {
385            let prev = self
386                .0
387                .compare_exchange(cur, desired, Ordering::AcqRel, Ordering::Relaxed);
388            turso_debug_assert!(
389                prev.is_ok(),
390                "downgrade CAS failed — lock was mutated concurrently"
391            );
392        }
393        #[cfg(not(debug_assertions))]
394        {
395            self.0.store(desired, Ordering::Release);
396        }
397    }
398
399    #[inline]
400    /// Unlock whatever lock is currently held.
401    /// For write lock: clear writer bit
402    /// For read lock: decrement reader count
403    pub fn unlock(&self) {
404        let cur = self.0.load(Ordering::Acquire);
405        if (cur & Self::WRITER) != 0 {
406            // Clear writer bit, preserve everything else (including value)
407            // Release ordering ensures all our writes are visible to next acquirer
408            let cur = self.0.fetch_and(!Self::WRITER, Ordering::Release);
409            turso_assert!(!Self::has_readers(cur), "write lock was held with readers");
410        } else {
411            turso_assert!(
412                Self::has_readers(cur),
413                "unlock called with no readers or writers"
414            );
415            self.0.fetch_sub(Self::READER_INC, Ordering::Release);
416        }
417    }
418
419    #[inline]
420    /// Read the embedded 32-bit value atomically regardless of slot occupancy.
421    pub fn get_value(&self) -> u32 {
422        (self.0.load(Ordering::Acquire) >> Self::VALUE_SHIFT) as u32
423    }
424
425    #[inline]
426    /// The embedded read-mark value, but only if a reader currently holds this slot
427    /// (otherwise the value is stale from a past holder). Lock-free single-load; used to
428    /// find the minimum frame any active reader is pinned at without mutating the slot.
429    pub fn held_value(&self) -> Option<u32> {
430        let cur = self.0.load(Ordering::Acquire);
431        if Self::has_readers(cur) {
432            Some((cur >> Self::VALUE_SHIFT) as u32)
433        } else {
434            None
435        }
436    }
437
438    #[inline]
439    /// Set the embedded value while holding the write lock.
440    pub fn set_value_exclusive(&self, v: u32) {
441        // Must be called only while WRITER bit is set
442        let cur = self.0.load(Ordering::Acquire);
443        turso_assert!(Self::has_writer(cur), "must hold exclusive lock");
444        let desired = (cur & !Self::VALUE_MASK) | ((v as u64) << Self::VALUE_SHIFT);
445        self.0.store(desired, Ordering::Release);
446    }
447}
448
449/// Represents a batch of WAL frames which will be appended to the log
450/// with a `pwritev` call and then sync'd to disk.
451pub struct PreparedFrames {
452    /// File offset for the first frame
453    pub offset: u64,
454    /// Serialized frame buffers
455    pub bufs: Vec<Arc<Buffer>>,
456    /// Per-frame metadata: (page_ref, frame_id, cumulative_checksum)
457    pub metadata: Vec<(PageRef, u64, (u32, u32))>,
458    /// Checksum after all frames in this batch
459    pub final_checksum: (u32, u32),
460    /// Max frame ID after this batch
461    pub final_max_frame: u64,
462    /// Epoch at preparation time
463    pub epoch: u32,
464}
465
466/// Metadata published by the coordination backend once a WAL commit becomes visible.
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468struct WalCommitState {
469    max_frame: u64,
470    last_checksum: (u32, u32),
471    transaction_count: u64,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475enum CoordinationCheckpointGuardKind {
476    Read0,
477    Writer,
478}
479
480/// Coordination operations that back the WAL's authoritative state.
481trait WalCoordination: Debug + Send + Sync {
482    /// Load the current authoritative WAL snapshot.
483    fn load_snapshot(&self) -> WalSnapshot;
484
485    /// Ensure any process-local fallback cache is complete for `snapshot`.
486    fn ensure_local_frame_cache_covers(
487        &self,
488        _io: &Arc<dyn IO>,
489        _snapshot: WalSnapshot,
490    ) -> Result<()> {
491        Ok(())
492    }
493
494    /// Publish a newly committed WAL state snapshot.
495    fn publish_commit(&self, commit: WalCommitState);
496
497    /// Publish the highest frame durably backfilled during checkpoint.
498    fn publish_backfill(&self, max_frame: u64);
499
500    /// Install any backend-specific durable proof before publishing backfill.
501    /// Returns an optional completion that must finish before `publish_backfill`.
502    fn install_durable_backfill_proof(
503        &self,
504        max_frame: u64,
505        db_size_pages: u32,
506        db_header_crc32c: u32,
507        sync_type: FileSyncType,
508    ) -> Result<Option<Completion>>;
509
510    /// Find the newest frame for `page_id` within the caller's visible range.
511    fn find_frame(
512        &self,
513        page_id: u64,
514        min_frame: u64,
515        max_frame: u64,
516        frame_watermark: Option<u64>,
517    ) -> Option<u64>;
518
519    /// Enumerate the latest visible frame per page in the requested frame range.
520    fn iter_latest_frames(&self, min_frame: u64, max_frame: u64) -> Vec<(u64, u64)>;
521
522    /// Read the current checkpoint epoch used to tag cached WAL pages.
523    fn checkpoint_epoch(&self) -> u32;
524
525    /// Advance the checkpoint epoch after checkpoint or restart invalidates cached pages.
526    fn bump_checkpoint_epoch(&self) -> u32;
527
528    /// Try to acquire the reader protection needed for `snapshot`.
529    fn try_begin_read_tx(&self, snapshot: WalSnapshot) -> Option<ReadGuardKind>;
530
531    /// Release a read guard previously returned by `try_begin_read_tx`.
532    fn end_read_tx(&self, guard: ReadGuardKind);
533
534    /// Try to acquire the WAL writer guard.
535    fn try_begin_write_tx(&self) -> bool;
536
537    /// Release a previously acquired WAL writer guard.
538    fn end_write_tx(&self);
539
540    /// Acquire the checkpoint-related locks needed for `mode`.
541    fn acquire_checkpoint_guard(
542        &self,
543        mode: CheckpointMode,
544    ) -> Result<CoordinationCheckpointGuardKind>;
545
546    /// Acquire the remaining checkpoint-related locks for VACUUM when the
547    /// caller already owns the raw process-local checkpoint lock.
548    ///
549    /// On error, implementations must release that held checkpoint lock before
550    /// returning.
551    ///
552    fn acquire_vacuum_checkpoint_guard_from_held_lock(
553        &self,
554    ) -> Result<CoordinationCheckpointGuardKind>;
555
556    /// Release the checkpoint-related locks previously acquired for `guard`.
557    fn release_checkpoint_guard(&self, guard: CoordinationCheckpointGuardKind);
558
559    /// Compute the highest frame a checkpoint may safely backfill and refresh read marks.
560    fn determine_max_safe_checkpoint_frame(&self, max_frame: u64) -> u64;
561
562    /// Lowest read-mark frame any reader is currently pinned at, or `None` if none. Read-only.
563    fn min_pinned_read_frame(&self) -> Option<u64>;
564
565    /// Begin a restart while the caller holds the required external checkpoint/write guards.
566    fn begin_restart(&self, io: &dyn IO) -> Result<WalSnapshot>;
567
568    /// Release any restart-only coordination state held by `begin_restart`.
569    fn end_restart(&self);
570
571    /// Attempt the restart path used by a writer holding read-mark 0.
572    fn try_restart_log_for_write(&self, io: &dyn IO) -> Result<Option<WalSnapshot>>;
573
574    /// Mark the WAL uninitialized before truncation and return the WAL file handle.
575    fn prepare_truncate(&self) -> Result<Arc<dyn File>>;
576
577    /// Return the current WAL header snapshot.
578    fn wal_header(&self) -> WalHeader;
579
580    /// Return the WAL file used for durable reads and writes.
581    fn wal_file(&self) -> Result<Arc<dyn File>>;
582
583    /// Clone the shared WAL state backing this coordination backend.
584    fn shared_wal_state(&self) -> Arc<RwLock<WalFileShared>>;
585
586    /// Report whether the WAL header has already been written and synced.
587    fn wal_is_initialized(&self) -> bool;
588
589    /// Initialize or refresh the WAL header before the first append after restart/truncate.
590    fn prepare_wal_header(&self, io: &dyn IO, page_size: PageSize) -> Option<WalHeader>;
591
592    /// Mark the WAL header durable after the header sync completes.
593    fn mark_initialized(&self);
594
595    /// Record a newly appended frame in the backend's page-to-frame lookup state.
596    fn cache_frame(&self, page_id: u64, frame_id: u64);
597
598    /// Drop any cached frame mappings newer than `max_frame`.
599    fn rollback_cache(&self, max_frame: u64);
600
601    /// Whether a process-local "last connection" close may run shutdown checkpointing.
602    fn should_checkpoint_on_close(&self) -> bool;
603
604    #[cfg(clt_turso_tests)]
605    fn backend_name(&self) -> &'static str;
606
607    #[cfg(clt_turso_tests)]
608    fn shared_ptr(&self) -> usize;
609
610    #[cfg(clt_turso_tests)]
611    fn open_mode_name(&self) -> Option<&'static str> {
612        None
613    }
614}
615
616/// Write-ahead log (WAL).
617#[aristo::intent("The WAL subsystem maintains LSN monotonicity, frame commitment ordering, recovery idempotency, checkpoint safety, and group commit atomicity.", id = "wal_protocol_correctness", verify = "neural")]
618pub trait Wal: Debug + Send + Sync {
619    /// Begin a read transaction.
620    /// Returns whether the database state has changed since the last read transaction.
621    fn begin_read_tx(&self) -> Result<bool>;
622    /// MVCC helper: check if WAL state changed without starting a read tx.
623    fn mvcc_refresh_if_db_changed(&self) -> bool;
624
625    /// Begin a write transaction.
626    ///
627    /// `allowed_auto_actions` controls which automatic WAL maintenance
628    /// actions are permitted within this call — currently only
629    /// `WalAutoActions::Restart` is consulted (it gates
630    /// `try_restart_log_before_write`). Callers that own WAL state
631    /// externally (e.g. the sync engine) pass an empty set to opt out.
632    fn begin_write_tx(&self, allowed_auto_actions: WalAutoActions) -> Result<()>;
633
634    /// End a read transaction.
635    fn end_read_tx(&self);
636
637    /// End a write transaction.
638    fn end_write_tx(&self);
639
640    /// Returns true if this WAL instance currently holds a read lock.
641    fn holds_read_lock(&self) -> bool;
642
643    /// Returns true if this WAL instance currently holds the write lock.
644    fn holds_write_lock(&self) -> bool;
645
646    /// Whether shutdown checkpointing is valid when this process closes its last connection.
647    fn should_checkpoint_on_close(&self) -> bool;
648
649    /// Find the latest frame containing a page.
650    ///
651    /// optional frame_watermark parameter can be passed to force WAL to find frame not larger than watermark value
652    /// caller must guarantee, that frame_watermark must be greater than last checkpointed frame, otherwise method will panic
653    fn find_frame(&self, page_id: u64, frame_watermark: Option<u64>) -> Result<Option<u64>>;
654
655    /// Read a frame from the WAL.
656    fn read_frame(
657        &self,
658        frame_id: u64,
659        page: PageRef,
660        buffer_pool: Arc<BufferPool>,
661    ) -> Result<Completion>;
662
663    /// Read a contiguous run of WAL frames with a single `pread`.
664    /// For each `i`, `pages[i]` receives the decoded page body of frame
665    /// `start_frame + i`. This method is a batched version of `read_frame`.
666    ///
667    /// If `scratch_buf` is `Some`, it is used as the pread destination (must
668    /// have length exactly `(page_size + WAL_FRAME_HEADER_SIZE) * pages.len()`).
669    /// Otherwise a fresh temporary buffer is allocated. VACUUM passes a
670    /// pre-allocated buffer to amortize the ~batch-size allocation across
671    /// batches.
672    fn read_frames_batch(
673        &self,
674        start_frame: u64,
675        pages: &[PageRef],
676        buffer_pool: Arc<BufferPool>,
677        scratch_buf: Option<Arc<Buffer>>,
678    ) -> Result<Completion>;
679
680    /// Read a raw frame (header included) from the WAL.
681    fn read_frame_raw(&self, frame_id: u64, frame: &mut [u8]) -> Result<Completion>;
682
683    /// Write a raw frame (header included) from the WAL.
684    /// Note, that turso-db will use page_no and size_after fields from the header, but will overwrite checksum with proper value
685    fn write_frame_raw(
686        &self,
687        buffer_pool: Arc<BufferPool>,
688        frame_id: u64,
689        page_id: u64,
690        db_size: u64,
691        page: &[u8],
692        sync_type: FileSyncType,
693    ) -> Result<()>;
694
695    /// Prepare WAL header for the future append
696    /// Most of the time this method will return Ok(None)
697    fn prepare_wal_start(&self, page_sz: PageSize) -> Result<Option<Completion>>;
698
699    fn prepare_wal_finish(&self, sync_type: FileSyncType) -> Result<Completion>;
700
701    /// Prepare a batch of WAL frames for durable commit/append to the log.
702    fn prepare_frames(
703        &self,
704        pages: &[PageRef],
705        page_sz: PageSize,
706        db_size_on_commit: Option<u32>,
707        prev: Option<&PreparedFrames>,
708    ) -> Result<PreparedFrames>;
709
710    /// For each prepared frame, update in-memory WAL index and rolling checksum
711    /// and advance max_frame to make committed frames visible to readers.
712    fn commit_prepared_frames(&self, prepared: &[PreparedFrames]);
713
714    /// Mark in-memory pages clean and set WAL tags after durable commit.
715    fn finalize_committed_pages(&self, prepared: &[PreparedFrames]);
716
717    /// Return a handle to the underlying File.
718    fn wal_file(&self) -> Result<Arc<dyn File>>;
719
720    /// Write a bunch of frames to the WAL.
721    /// db_size is the database size in pages after the transaction finishes.
722    /// db_size is set  -> last frame written in transaction
723    /// db_size is none -> non-last frame written in transaction
724    fn append_frames_vectored(&self, pages: Vec<PageRef>, page_sz: PageSize) -> Result<Completion>;
725
726    /// Complete append of frames by updating shared wal state. Before this
727    /// all changes were stored locally.
728    fn finish_append_frames_commit(&self) -> Result<()>;
729
730    fn should_checkpoint(&self) -> bool;
731    fn checkpoint(&self, pager: &Pager, mode: CheckpointMode)
732        -> Result<IOResult<CheckpointResult>>;
733    fn install_durable_backfill_proof(
734        &self,
735        max_frame: u64,
736        db_size_pages: u32,
737        db_header_crc32c: u32,
738        sync_type: FileSyncType,
739    ) -> Result<Option<Completion>>;
740    fn publish_backfill(&self, max_frame: u64);
741    fn sync(&self, sync_type: FileSyncType) -> Result<Completion>;
742    fn is_syncing(&self) -> bool;
743    /// Whether the WAL file is dirty: frames were appended that no successful
744    /// WAL fsync has covered yet. A dirty WAL owes an fsync before a commit
745    /// may be reported durable, even when the committer itself has no dirty
746    /// pages to write (e.g. frames inserted through [Wal::write_frame_raw]).
747    fn is_dirty(&self) -> bool;
748    fn get_max_frame_in_wal(&self) -> u64;
749    fn get_checkpoint_seq(&self) -> u32;
750    fn get_max_frame(&self) -> u64;
751    /// This connection's frozen `(checkpoint_seq, max_frame)`: for a reader it is the WAL read
752    /// mark installed at `begin_read_tx`; for a writer it is the position after its last commit.
753    /// Used by MVCC to gate btree reads on physical reachability (a materialization at WAL
754    /// position `P` is reachable iff `P <= this`, lexicographically). See `RootEntry`.
755    fn connection_wal_pos(&self) -> (u32, u64);
756    /// The lowest WAL frame any active reader is currently pinned at (across the read-mark
757    /// slots), or `None` if no reader holds a slot. This is the authoritative set of pinned
758    /// readers — it includes a reader that has called `begin_read_tx` but not yet published an
759    /// MVCC transaction — so the MVCC checkpoint uses it as the version-store GC floor (a row
760    /// whose btree page was materialized past a pinned reader's frame is invisible in that
761    /// reader's snapshot, so its version-store copy must be retained).
762    fn min_pinned_read_frame(&self) -> Option<u64>;
763    fn get_min_frame(&self) -> u64;
764    /// The shared backfill boundary: WAL frames at or below this are durably copied into the DB
765    /// file, so a version materialized there is reachable by EVERY snapshot (including a db-file
766    /// reader pinned at the boundary). Used as the passive-checkpoint version-store GC floor.
767    fn backfill_frame(&self) -> u64;
768    fn rollback(&self, rollback_to: Option<RollbackTo>);
769    fn abort_checkpoint(&self);
770    fn get_last_checksum(&self) -> (u32, u32);
771
772    /// Return unique set of pages changed **after** frame_watermark position and until current WAL session max_frame_no
773    fn changed_pages_after(&self, frame_watermark: u64) -> Result<Vec<u32>>;
774
775    fn set_io_context(&self, ctx: IOContext);
776
777    /// Update the max frame to the current shared max frame.
778    /// Currently this is only used for MVCC as it takes care of write conflicts on its own.
779    /// This should't be used with regular WAL mode.
780    fn update_max_frame(&self);
781
782    /// Truncate WAL file to zero and sync it. This is called AFTER the DB file has been
783    /// synced during TRUNCATE checkpoint mode, ensuring data durability.
784    /// The result parameter is used to track I/O progress (wal_truncate_sent, wal_sync_sent).
785    fn truncate_wal(
786        &self,
787        result: &mut CheckpointResult,
788        sync_type: FileSyncType,
789    ) -> Result<IOResult<()>>;
790
791    /// Try to acquire the checkpoint serialization lock. Returns `Busy` if
792    /// another checkpointer or VACUUM already holds it. Used by plain VACUUM
793    /// to fail fast if a concurrent checkpoint would block later.
794    fn try_begin_vacuum_checkpoint_lock(&self) -> Result<()>;
795
796    /// Release the checkpoint serialization lock acquired by
797    /// `try_begin_vacuum_checkpoint_lock`.
798    fn release_vacuum_checkpoint_lock(&self);
799
800    /// Acquire exclusive WAL access. This will block all new readers and writers. Also,
801    /// this routine succeeds only if no other transactions are active. This is used by
802    /// VACUUM routine.
803    ///
804    ///
805    /// VACUUM: take `vacuum_lock` exclusively, take the WAL write lock, and install
806    /// the source snapshot that VACUUM will copy from.
807    ///
808    /// This does not acquire a physical read-mark lock. The exclusive snapshot
809    /// is protected by `vacuum_lock`: normal readers hold that lock shared for
810    /// their read transaction, so once the exclusive lock is acquired no new
811    /// normal reader or writer can enter.
812    fn begin_vacuum_blocking_tx(&self) -> Result<()>;
813
814    /// Checkpoint using a checkpoint lock already held by the caller. The
815    /// method consumes that raw checkpoint-lock ownership: on success the guard
816    /// is held by the checkpoint state machine, and on early failure it is
817    /// released before returning.
818    fn vacuum_checkpoint_with_held_lock(&self, pager: &Pager)
819        -> Result<IOResult<CheckpointResult>>;
820
821    /// Release the exclusive VACUUM lock acquired by `begin_vacuum_blocking_tx`.
822    /// VACUUM calls this once done, after which new
823    /// readers and writers may proceed again.
824    fn release_vacuum_lock(&self);
825
826    #[cfg(any(clt_turso_tests, debug_assertions))]
827    fn as_any(&self) -> &dyn std::any::Any;
828}
829
830#[derive(Debug)]
831struct InProcessWalCoordination {
832    shared: Arc<RwLock<WalFileShared>>,
833}
834
835impl InProcessWalCoordination {
836    /// Build the in-process coordination backend over the existing shared WAL state.
837    fn new(shared: Arc<RwLock<WalFileShared>>) -> Self {
838        Self { shared }
839    }
840
841    fn try_read_mark_shared(&self, slot: usize) -> bool {
842        self.shared.read().runtime.read_locks[slot].read()
843    }
844
845    fn try_read_mark_exclusive(&self, slot: usize) -> bool {
846        self.shared.read().runtime.read_locks[slot].write()
847    }
848
849    fn unlock_read_mark(&self, slot: usize) {
850        self.shared.read().runtime.read_locks[slot].unlock();
851    }
852
853    fn read_mark_value(&self, slot: usize) -> u32 {
854        self.shared.read().runtime.read_locks[slot].get_value()
855    }
856
857    /// Lowest read-mark frame across slots currently held by a reader (1..5; slot 0 is the
858    /// db-file read mark), or `None` if no reader holds a slot. Read-only / lock-free.
859    fn min_pinned_read_frame_inner(&self) -> Option<u64> {
860        let shared = self.shared.read();
861        let mut min: Option<u64> = None;
862        for slot in 1..5 {
863            if let Some(v) = shared.runtime.read_locks[slot].held_value() {
864                if v != READMARK_NOT_USED {
865                    let f = v as u64;
866                    min = Some(min.map_or(f, |m: u64| m.min(f)));
867                }
868            }
869        }
870        min
871    }
872
873    fn set_read_mark_value_exclusive(&self, slot: usize, value: u32) {
874        self.shared.read().runtime.read_locks[slot].set_value_exclusive(value);
875    }
876
877    fn try_upgrade_read_mark(&self, slot: usize) -> bool {
878        self.shared.read().runtime.read_locks[slot].upgrade()
879    }
880
881    fn downgrade_read_mark(&self, slot: usize) {
882        self.shared.read().runtime.read_locks[slot].downgrade();
883    }
884
885    fn try_write_lock(&self) -> bool {
886        self.shared.read().runtime.write_lock.write()
887    }
888
889    fn unlock_write_lock(&self) {
890        self.shared.read().runtime.write_lock.unlock();
891    }
892
893    fn try_checkpoint_lock(&self) -> bool {
894        self.shared.read().runtime.checkpoint_lock.write()
895    }
896
897    fn unlock_checkpoint_lock(&self) {
898        self.shared.read().runtime.checkpoint_lock.unlock();
899    }
900}
901
902impl WalCoordination for InProcessWalCoordination {
903    fn load_snapshot(&self) -> WalSnapshot {
904        let shared = self.shared.read();
905        let checkpoint_seq = shared.metadata.wal_header.lock().checkpoint_seq;
906        WalSnapshot {
907            max_frame: shared.metadata.max_frame.load(Ordering::Acquire),
908            nbackfills: shared.metadata.nbackfills.load(Ordering::Acquire),
909            last_checksum: shared.metadata.last_checksum,
910            checkpoint_seq,
911            transaction_count: shared.metadata.transaction_count.load(Ordering::Acquire),
912        }
913    }
914
915    fn publish_commit(&self, commit: WalCommitState) {
916        let mut shared = self.shared.write();
917        shared
918            .metadata
919            .max_frame
920            .store(commit.max_frame, Ordering::Release);
921        shared.metadata.last_checksum = commit.last_checksum;
922        shared
923            .metadata
924            .transaction_count
925            .store(commit.transaction_count, Ordering::Release);
926    }
927
928    fn publish_backfill(&self, max_frame: u64) {
929        self.shared
930            .write()
931            .metadata
932            .nbackfills
933            .store(max_frame, Ordering::Release);
934    }
935
936    fn install_durable_backfill_proof(
937        &self,
938        _max_frame: u64,
939        _db_size_pages: u32,
940        _db_header_crc32c: u32,
941        _sync_type: FileSyncType,
942    ) -> Result<Option<Completion>> {
943        Ok(None)
944    }
945
946    fn find_frame(
947        &self,
948        page_id: u64,
949        min_frame: u64,
950        max_frame: u64,
951        frame_watermark: Option<u64>,
952    ) -> Option<u64> {
953        let shared = self.shared.read();
954        let frame_cache = shared.runtime.frame_cache.lock();
955        let range = frame_watermark
956            .map(|x| 0..=x)
957            .unwrap_or(min_frame..=max_frame);
958        let result = frame_cache.get(&page_id).and_then(|frames| {
959            frames
960                .iter()
961                .rfind(|&&frame| range.contains(&frame))
962                .copied()
963        });
964        result
965    }
966
967    fn iter_latest_frames(&self, min_frame: u64, max_frame: u64) -> Vec<(u64, u64)> {
968        let shared = self.shared.read();
969        let frame_cache = shared.runtime.frame_cache.lock();
970        let mut list = Vec::with_capacity(frame_cache.len());
971        for (&page_id, frames) in frame_cache.iter() {
972            if let Some(&frame_id) = frames
973                .iter()
974                .rfind(|&&frame| (min_frame..=max_frame).contains(&frame))
975            {
976                list.push((page_id, frame_id));
977            }
978        }
979        list.sort_unstable_by_key(|&(page_id, _)| page_id);
980        list
981    }
982
983    fn checkpoint_epoch(&self) -> u32 {
984        self.shared.read().runtime.epoch.load(Ordering::Acquire)
985    }
986
987    fn bump_checkpoint_epoch(&self) -> u32 {
988        self.shared
989            .read()
990            .runtime
991            .epoch
992            .fetch_add(1, Ordering::Release)
993    }
994
995    fn try_begin_read_tx(&self, snapshot: WalSnapshot) -> Option<ReadGuardKind> {
996        turso_assert!(
997            snapshot.max_frame <= u32::MAX as u64,
998            "max_frame exceeds u32 read mark range"
999        );
1000        if snapshot.max_frame == snapshot.nbackfills {
1001            if !self.try_read_mark_shared(0) {
1002                return None;
1003            }
1004            if self.load_snapshot() != snapshot {
1005                self.unlock_read_mark(0);
1006                return None;
1007            }
1008            return Some(ReadGuardKind::DbFile);
1009        }
1010
1011        let mut best_idx: i64 = -1;
1012        let mut best_mark: u32 = 0;
1013        for idx in 1..5 {
1014            let mark = self.read_mark_value(idx);
1015            if mark != READMARK_NOT_USED && mark <= snapshot.max_frame as u32 && mark > best_mark {
1016                best_mark = mark;
1017                best_idx = idx as i64;
1018            }
1019        }
1020
1021        if best_idx == -1 || (best_mark as u64) < snapshot.max_frame {
1022            for idx in 1..5 {
1023                if !self.try_read_mark_exclusive(idx) {
1024                    continue;
1025                }
1026                self.set_read_mark_value_exclusive(idx, snapshot.max_frame as u32);
1027                best_idx = idx as i64;
1028                best_mark = snapshot.max_frame as u32;
1029                self.unlock_read_mark(idx);
1030                break;
1031            }
1032        }
1033
1034        if best_idx == -1 || !self.try_read_mark_shared(best_idx as usize) {
1035            return None;
1036        }
1037
1038        let snapshot_after_lock = self.load_snapshot();
1039        let current_slot_mark = self.read_mark_value(best_idx as usize);
1040        if current_slot_mark != best_mark || snapshot_after_lock != snapshot {
1041            self.unlock_read_mark(best_idx as usize);
1042            return None;
1043        }
1044
1045        Some(ReadGuardKind::ReadMark(
1046            NonZeroUsize::new(best_idx as usize)
1047                .expect("best_idx checked to be non-negative and non-zero"),
1048        ))
1049    }
1050
1051    fn end_read_tx(&self, guard: ReadGuardKind) {
1052        match guard {
1053            ReadGuardKind::None => {}
1054            ReadGuardKind::DbFile => self.unlock_read_mark(0),
1055            ReadGuardKind::ReadMark(slot) => self.unlock_read_mark(slot.into()),
1056        }
1057    }
1058
1059    fn try_begin_write_tx(&self) -> bool {
1060        self.try_write_lock()
1061    }
1062
1063    fn end_write_tx(&self) {
1064        self.unlock_write_lock();
1065    }
1066
1067    fn acquire_checkpoint_guard(
1068        &self,
1069        mode: CheckpointMode,
1070    ) -> Result<CoordinationCheckpointGuardKind> {
1071        if !self.try_checkpoint_lock() {
1072            tracing::trace!("CheckpointGuard::new: checkpoint lock failed, returning Busy");
1073            return Err(LimboError::Busy);
1074        }
1075        match mode {
1076            CheckpointMode::Passive { .. } => {
1077                if !self.try_read_mark_exclusive(0) {
1078                    self.unlock_checkpoint_lock();
1079                    tracing::trace!("CheckpointGuard: read0 lock failed, returning Busy");
1080                    return Err(LimboError::Busy);
1081                }
1082                Ok(CoordinationCheckpointGuardKind::Read0)
1083            }
1084            CheckpointMode::Full => {
1085                if !self.try_read_mark_exclusive(0) {
1086                    self.unlock_checkpoint_lock();
1087                    tracing::trace!("CheckpointGuard: read0 lock failed (Full), Busy");
1088                    return Err(LimboError::Busy);
1089                }
1090                if !self.try_write_lock() {
1091                    self.unlock_read_mark(0);
1092                    self.unlock_checkpoint_lock();
1093                    tracing::trace!("CheckpointGuard: write lock failed (Full), Busy");
1094                    return Err(LimboError::Busy);
1095                }
1096                Ok(CoordinationCheckpointGuardKind::Writer)
1097            }
1098            CheckpointMode::Restart | CheckpointMode::Truncate { .. } => {
1099                if !self.try_read_mark_exclusive(0) {
1100                    self.unlock_checkpoint_lock();
1101                    tracing::trace!("CheckpointGuard: read0 lock failed, returning Busy");
1102                    return Err(LimboError::Busy);
1103                }
1104                if !self.try_write_lock() {
1105                    self.unlock_checkpoint_lock();
1106                    self.unlock_read_mark(0);
1107                    tracing::trace!("CheckpointGuard: write lock failed, returning Busy");
1108                    return Err(LimboError::Busy);
1109                }
1110                Ok(CoordinationCheckpointGuardKind::Writer)
1111            }
1112        }
1113    }
1114
1115    fn acquire_vacuum_checkpoint_guard_from_held_lock(
1116        &self,
1117    ) -> Result<CoordinationCheckpointGuardKind> {
1118        if !self.try_read_mark_exclusive(0) {
1119            self.unlock_checkpoint_lock();
1120            tracing::trace!("CheckpointGuard: held VACUUM read0 lock failed, returning Busy");
1121            return Err(LimboError::Busy);
1122        }
1123        if !self.try_write_lock() {
1124            self.unlock_read_mark(0);
1125            self.unlock_checkpoint_lock();
1126            tracing::trace!("CheckpointGuard: held VACUUM write lock failed, returning Busy");
1127            return Err(LimboError::Busy);
1128        }
1129        Ok(CoordinationCheckpointGuardKind::Writer)
1130    }
1131
1132    fn release_checkpoint_guard(&self, guard: CoordinationCheckpointGuardKind) {
1133        match guard {
1134            CoordinationCheckpointGuardKind::Writer => {
1135                self.unlock_write_lock();
1136                self.unlock_read_mark(0);
1137                self.unlock_checkpoint_lock();
1138            }
1139            CoordinationCheckpointGuardKind::Read0 => {
1140                self.unlock_read_mark(0);
1141                self.unlock_checkpoint_lock();
1142            }
1143        }
1144    }
1145
1146    fn determine_max_safe_checkpoint_frame(&self, max_frame: u64) -> u64 {
1147        turso_assert!(
1148            max_frame <= u32::MAX as u64,
1149            "max_frame exceeds u32 read mark range"
1150        );
1151        let mut max_safe_frame = max_frame;
1152        for read_lock_idx in 1..5 {
1153            let this_mark = self.read_mark_value(read_lock_idx);
1154            if this_mark < max_safe_frame as u32 {
1155                let busy = !self.try_read_mark_exclusive(read_lock_idx);
1156                if !busy {
1157                    let val = if read_lock_idx == 1 {
1158                        max_safe_frame as u32
1159                    } else {
1160                        READMARK_NOT_USED
1161                    };
1162                    self.set_read_mark_value_exclusive(read_lock_idx, val);
1163                    self.unlock_read_mark(read_lock_idx);
1164                } else {
1165                    max_safe_frame = this_mark as u64;
1166                }
1167            }
1168        }
1169        max_safe_frame
1170    }
1171
1172    fn min_pinned_read_frame(&self) -> Option<u64> {
1173        self.min_pinned_read_frame_inner()
1174    }
1175
1176    fn begin_restart(&self, io: &dyn IO) -> Result<WalSnapshot> {
1177        for idx in 1..5 {
1178            if !self.try_read_mark_exclusive(idx) {
1179                for j in 1..idx {
1180                    self.unlock_read_mark(j);
1181                }
1182                return Err(LimboError::Busy);
1183            }
1184            self.set_read_mark_value_exclusive(idx, READMARK_NOT_USED);
1185        }
1186        let mut shared = self.shared.write();
1187        shared.restart_wal_header(io);
1188        let checkpoint_seq = shared.metadata.wal_header.lock().checkpoint_seq;
1189        Ok(WalSnapshot {
1190            max_frame: shared.metadata.max_frame.load(Ordering::Acquire),
1191            nbackfills: shared.metadata.nbackfills.load(Ordering::Acquire),
1192            last_checksum: shared.metadata.last_checksum,
1193            checkpoint_seq,
1194            transaction_count: shared.metadata.transaction_count.load(Ordering::Acquire),
1195        })
1196    }
1197
1198    fn end_restart(&self) {
1199        for idx in 1..5 {
1200            self.unlock_read_mark(idx);
1201        }
1202    }
1203
1204    fn try_restart_log_for_write(&self, io: &dyn IO) -> Result<Option<WalSnapshot>> {
1205        if !self.try_upgrade_read_mark(0) {
1206            return Ok(None);
1207        }
1208        let result = self.begin_restart(io);
1209        self.downgrade_read_mark(0);
1210        match result {
1211            Ok(snapshot) => {
1212                self.end_restart();
1213                Ok(Some(snapshot))
1214            }
1215            Err(err) => Err(err),
1216        }
1217    }
1218
1219    fn prepare_truncate(&self) -> Result<Arc<dyn File>> {
1220        let shared = self.shared.read();
1221        turso_assert!(
1222            shared.metadata.enabled.load(Ordering::Relaxed),
1223            "WAL must be enabled"
1224        );
1225        shared.metadata.initialized.store(false, Ordering::Release);
1226        shared.runtime.file.as_ref().cloned().ok_or_else(|| {
1227            mark_unlikely();
1228            LimboError::InternalError("WAL file not open".into())
1229        })
1230    }
1231
1232    fn wal_header(&self) -> WalHeader {
1233        *self.shared.read().metadata.wal_header.lock()
1234    }
1235
1236    fn wal_file(&self) -> Result<Arc<dyn File>> {
1237        let shared = self.shared.read();
1238        turso_assert!(
1239            shared.metadata.enabled.load(Ordering::Relaxed),
1240            "WAL must be enabled"
1241        );
1242        shared.runtime.file.as_ref().cloned().ok_or_else(|| {
1243            mark_unlikely();
1244            LimboError::InternalError("WAL file not open".into())
1245        })
1246    }
1247
1248    fn wal_is_initialized(&self) -> bool {
1249        self.shared
1250            .read()
1251            .metadata
1252            .initialized
1253            .load(Ordering::Acquire)
1254    }
1255
1256    fn prepare_wal_header(&self, io: &dyn IO, page_size: PageSize) -> Option<WalHeader> {
1257        let mut shared: crate::sync::RwLockWriteGuard<'_, WalFileShared> = self.shared.write();
1258        if shared.metadata.initialized.load(Ordering::Acquire) {
1259            return None;
1260        }
1261
1262        let (header, checksum) = {
1263            let mut hdr = shared.metadata.wal_header.lock();
1264            hdr.magic = if cfg!(target_endian = "big") {
1265                WAL_MAGIC_BE
1266            } else {
1267                WAL_MAGIC_LE
1268            };
1269            if hdr.page_size == 0 {
1270                hdr.page_size = page_size.get();
1271            }
1272            if hdr.salt_1 == 0 && hdr.salt_2 == 0 {
1273                hdr.salt_1 = io.generate_random_number() as u32;
1274                hdr.salt_2 = io.generate_random_number() as u32;
1275            }
1276
1277            let prefix = &hdr.as_bytes()[..WAL_HEADER_SIZE - 8];
1278            let use_native = (hdr.magic & 1) != 0;
1279            let (c1, c2) = checksum_wal(prefix, &hdr, (0, 0), use_native);
1280            hdr.checksum_1 = c1;
1281            hdr.checksum_2 = c2;
1282            (*hdr, (c1, c2))
1283        };
1284        shared.metadata.last_checksum = checksum;
1285        Some(header)
1286    }
1287
1288    fn mark_initialized(&self) {
1289        self.shared
1290            .read()
1291            .metadata
1292            .initialized
1293            .store(true, Ordering::Release);
1294    }
1295
1296    fn cache_frame(&self, page_id: u64, frame_id: u64) {
1297        let shared = self.shared.read();
1298        let mut frame_cache = shared.runtime.frame_cache.lock();
1299        // Frame-slot reuse / append-position rewind guard. Within a WAL
1300        // generation frames are appended with strictly increasing numbers, so
1301        // a `frame_id` that does not exceed the current high-water means the
1302        // slots from `frame_id` upward are being overwritten: by frames from a
1303        // prior uncommitted/aborted append that was never rolled back out of
1304        // the cache, or by another connection reusing the slots after a
1305        // rewind. Drop every stale `page -> frame` mapping for those slots
1306        // before recording the new one, otherwise `find_frame` can return a
1307        // frame slot that now physically holds a different page (corruption).
1308        // (Per-page frame lists are kept ascending, so popping the tail
1309        // `>= frame_id` removes exactly the overwritten suffix.)
1310        let high_water = shared
1311            .runtime
1312            .frame_cache_high_water
1313            .load(Ordering::Acquire);
1314        if frame_id <= high_water {
1315            frame_cache.retain(|_page_id, frames| {
1316                while frames.last().is_some_and(|&frame| frame >= frame_id) {
1317                    frames.pop();
1318                }
1319                !frames.is_empty()
1320            });
1321        }
1322        match frame_cache.get_mut(&page_id) {
1323            Some(frames) => {
1324                frames.push(frame_id);
1325            }
1326            None => {
1327                frame_cache.insert(page_id, vec![frame_id]);
1328            }
1329        }
1330        shared
1331            .runtime
1332            .frame_cache_high_water
1333            .store(frame_id, Ordering::Release);
1334    }
1335
1336    fn rollback_cache(&self, max_frame: u64) {
1337        let shared = self.shared.read();
1338        let mut frame_cache = shared.runtime.frame_cache.lock();
1339        frame_cache.retain(|_page_id, frames| {
1340            while frames.last().is_some_and(|&frame| frame > max_frame) {
1341                frames.pop();
1342            }
1343            !frames.is_empty()
1344        });
1345        // Keep the high-water consistent with the truncation so a subsequent
1346        // append at `max_frame + 1` is not misread as a rewind.
1347        if shared
1348            .runtime
1349            .frame_cache_high_water
1350            .load(Ordering::Acquire)
1351            > max_frame
1352        {
1353            shared
1354                .runtime
1355                .frame_cache_high_water
1356                .store(max_frame, Ordering::Release);
1357        }
1358    }
1359
1360    fn should_checkpoint_on_close(&self) -> bool {
1361        true
1362    }
1363
1364    #[cfg(clt_turso_tests)]
1365    fn backend_name(&self) -> &'static str {
1366        "in_process"
1367    }
1368
1369    #[cfg(clt_turso_tests)]
1370    fn shared_ptr(&self) -> usize {
1371        Arc::as_ptr(&self.shared) as usize
1372    }
1373
1374    fn shared_wal_state(&self) -> Arc<RwLock<WalFileShared>> {
1375        self.shared.clone()
1376    }
1377}
1378
1379/// Per-connection WAL coordination that delegates to the mmap'd tshm authority.
1380///
1381/// One instance exists per `WalFile` (i.e. per `Connection`). All instances
1382/// within a process share the same `Arc<MappedSharedWalCoordination>` and the
1383/// same `SharedOwnerRecord` (derived from the authority at construction time).
1384///
1385/// `fallback` provides the process-local read-mark / write-lock layer (the
1386/// same locks used in single-process mode). `authority` provides the
1387/// cross-process shared state (reader slots, frame index, snapshot metadata).
1388/// Both are consulted: the fallback serializes same-process connections, the
1389/// authority serializes across processes.
1390#[cfg(host_shared_wal)]
1391#[derive(Debug)]
1392struct ShmWalCoordination {
1393    shared: Arc<RwLock<WalFileShared>>,
1394    fallback: InProcessWalCoordination,
1395    authority: Arc<MappedSharedWalCoordination>,
1396    /// This connection's currently held reader slot, if any.
1397    active_reader: Mutex<Option<SharedReaderSlot>>,
1398    /// Copied from `authority.owner_record()` at construction — all connections
1399    /// in the same process share the same owner identity.
1400    owner: SharedOwnerRecord,
1401}
1402
1403#[cfg(host_shared_wal)]
1404impl ShmWalCoordination {
1405    fn overflow_fallback_covers(
1406        &self,
1407        snapshot: SharedWalCoordinationHeader,
1408        max_frame: u64,
1409    ) -> bool {
1410        self.shared
1411            .read()
1412            .runtime
1413            .overflow_fallback_coverage
1414            .lock()
1415            .covers(snapshot, max_frame)
1416    }
1417
1418    fn clear_overflow_fallback_coverage(&self) {
1419        self.shared
1420            .read()
1421            .runtime
1422            .overflow_fallback_coverage
1423            .lock()
1424            .clear();
1425    }
1426
1427    fn local_authority_snapshot_from_shared(
1428        shared: &WalFileShared,
1429        authority_snapshot: SharedWalCoordinationHeader,
1430    ) -> SharedWalCoordinationHeader {
1431        let header = shared.metadata.wal_header.lock();
1432        SharedWalCoordinationHeader {
1433            max_frame: shared.metadata.max_frame.load(Ordering::Acquire),
1434            nbackfills: shared.metadata.nbackfills.load(Ordering::Acquire),
1435            transaction_count: shared.metadata.transaction_count.load(Ordering::Acquire),
1436            visibility_generation: authority_snapshot.visibility_generation,
1437            checkpoint_seq: header.checkpoint_seq,
1438            checkpoint_epoch: shared.runtime.epoch.load(Ordering::Acquire),
1439            page_size: header.page_size,
1440            salt_1: header.salt_1,
1441            salt_2: header.salt_2,
1442            checksum_1: shared.metadata.last_checksum.0,
1443            checksum_2: shared.metadata.last_checksum.1,
1444            reader_slot_count: authority_snapshot.reader_slot_count,
1445        }
1446    }
1447
1448    fn new(
1449        shared: Arc<RwLock<WalFileShared>>,
1450        authority: Arc<MappedSharedWalCoordination>,
1451    ) -> Self {
1452        let fallback = InProcessWalCoordination::new(shared.clone());
1453        let coordination = Self {
1454            shared,
1455            fallback,
1456            owner: authority.owner_record(),
1457            authority,
1458            active_reader: Mutex::new(None),
1459        };
1460        coordination.seed_or_sync_authority();
1461        coordination
1462    }
1463
1464    fn authority_is_uninitialized(snapshot: SharedWalCoordinationHeader) -> bool {
1465        snapshot.max_frame == 0
1466            && snapshot.nbackfills == 0
1467            && snapshot.transaction_count == 0
1468            && snapshot.visibility_generation == 0
1469            && snapshot.checkpoint_seq == 0
1470            && snapshot.checkpoint_epoch == 0
1471            && snapshot.page_size == 0
1472            && snapshot.salt_1 == 0
1473            && snapshot.salt_2 == 0
1474            && snapshot.checksum_1 == 0
1475            && snapshot.checksum_2 == 0
1476    }
1477
1478    fn local_authority_snapshot(&self) -> SharedWalCoordinationHeader {
1479        let authority_snapshot = self.authority.snapshot();
1480        let shared = self.shared.read();
1481        Self::local_authority_snapshot_from_shared(&shared, authority_snapshot)
1482    }
1483
1484    fn install_local_snapshot(
1485        shared: &mut WalFileShared,
1486        snapshot: SharedWalCoordinationHeader,
1487        install_header: bool,
1488    ) {
1489        shared
1490            .metadata
1491            .max_frame
1492            .store(snapshot.max_frame, Ordering::Release);
1493        shared
1494            .metadata
1495            .nbackfills
1496            .store(snapshot.nbackfills, Ordering::Release);
1497        shared.metadata.last_checksum = (snapshot.checksum_1, snapshot.checksum_2);
1498        shared
1499            .metadata
1500            .transaction_count
1501            .store(snapshot.transaction_count, Ordering::Release);
1502        shared
1503            .runtime
1504            .epoch
1505            .store(snapshot.checkpoint_epoch, Ordering::Release);
1506        if install_header {
1507            let mut header = shared.metadata.wal_header.lock();
1508            header.checkpoint_seq = snapshot.checkpoint_seq;
1509            header.page_size = snapshot.page_size;
1510            header.salt_1 = snapshot.salt_1;
1511            header.salt_2 = snapshot.salt_2;
1512            header.checksum_1 = snapshot.checksum_1;
1513            header.checksum_2 = snapshot.checksum_2;
1514        }
1515    }
1516
1517    fn sync_local_from_authority(&self, snapshot: SharedWalCoordinationHeader) {
1518        let mut shared = self.shared.write();
1519        Self::install_local_snapshot(&mut shared, snapshot, snapshot.page_size != 0);
1520    }
1521
1522    fn sync_authority_from_local(&self) {
1523        self.authority
1524            .install_snapshot(self.local_authority_snapshot());
1525    }
1526
1527    fn sync_local_to_zero_frame_authority(&self, snapshot: SharedWalCoordinationHeader) {
1528        let mut shared = self.shared.write();
1529        Self::install_local_snapshot(&mut shared, snapshot, true);
1530        shared.metadata.initialized.store(false, Ordering::Release);
1531        shared.runtime.frame_cache.lock().clear();
1532        shared
1533            .runtime
1534            .frame_cache_high_water
1535            .store(0, Ordering::Release);
1536        shared.runtime.overflow_fallback_coverage.lock().clear();
1537    }
1538
1539    fn sync_authority_frames_from_local(&self) {
1540        let entries = {
1541            let shared = self.shared.read();
1542            let frame_cache = shared.runtime.frame_cache.lock();
1543            let mut entries = Vec::new();
1544            for (&page_id, frames) in frame_cache.iter() {
1545                for &frame_id in frames {
1546                    entries.push((frame_id, page_id));
1547                }
1548            }
1549            entries
1550        };
1551        let mut entries = entries;
1552        entries.sort_unstable();
1553        for (frame_id, page_id) in entries {
1554            self.authority.record_frame(page_id, frame_id);
1555        }
1556    }
1557
1558    fn repair_or_reseed_authority_from_local_disk_scan(
1559        &self,
1560        mut authority_snapshot: SharedWalCoordinationHeader,
1561    ) {
1562        self.authority.repair_transient_state_for_exclusive_open();
1563        if authority_snapshot.nbackfills != 0 {
1564            // A local WAL scan can rebuild the visible WAL tail, but it cannot
1565            // prove that positive checkpoint progress is durable in the main DB
1566            // file. Stay on the conservative reopen path until we implement a
1567            // SQLite-equivalent recovery protocol for trusting partial-checkpoint state.
1568            authority_snapshot.nbackfills = 0;
1569            self.authority.install_snapshot(authority_snapshot);
1570        }
1571        let local_snapshot = self.local_authority_snapshot();
1572        if Self::local_scan_predates_zero_frame_authority(authority_snapshot, local_snapshot) {
1573            self.sync_local_to_zero_frame_authority(authority_snapshot);
1574            return;
1575        }
1576        if Self::local_scan_cannot_disprove_zero_frame_authority(authority_snapshot, local_snapshot)
1577        {
1578            self.sync_local_from_authority(authority_snapshot);
1579            return;
1580        }
1581        if Self::local_scan_cannot_disprove_positive_authority(authority_snapshot, local_snapshot) {
1582            self.sync_local_from_authority(authority_snapshot);
1583            return;
1584        }
1585        if Self::authority_matches_local_wal_scan(authority_snapshot, local_snapshot) {
1586            self.sync_local_from_authority(authority_snapshot);
1587            // Matching header metadata is not enough to trust the durable
1588            // frame index. A restart or interrupted reopen can leave stale or
1589            // empty page->frame mappings behind while max_frame/checksums
1590            // still match the scanned WAL. When both snapshots describe the
1591            // same visible WAL generation, compare the latest per-page
1592            // mappings directly and rebuild if they diverge.
1593            if self.authority.frame_index_overflowed()
1594                || (self.authority.open_mode() == SharedWalCoordinationOpenMode::Exclusive
1595                    && !self.authority_frame_index_matches_local_wal_scan(local_snapshot.max_frame))
1596            {
1597                self.authority
1598                    .discard_durable_frame_index_for_exclusive_rebuild();
1599                self.sync_authority_frames_from_local();
1600            }
1601            return;
1602        }
1603        // The authority and disk scan are from the same generation (matching
1604        // checkpoint_seq/salts) but disagree on max_frame or checksums. This
1605        // happens when a concurrent write advances the authority between the
1606        // snapshot read and the disk scan.  Or the authority is from a strictly
1607        // newer generation (higher checkpoint_seq) because the WAL was
1608        // restarted but the on-disk header hasn't been rewritten yet.
1609        //
1610        // In both cases the authority's header fields are at least as current
1611        // as the disk, so adopt them.  As above, preserve the authority's
1612        // frame index — it is maintained by writers and must not be replaced
1613        // with a potentially incomplete reconstruction.
1614        if Self::authority_is_same_or_newer_generation(authority_snapshot, local_snapshot) {
1615            self.sync_local_from_authority(authority_snapshot);
1616            if authority_snapshot.checkpoint_seq == local_snapshot.checkpoint_seq
1617                && self.authority.frame_index_overflowed()
1618            {
1619                self.authority
1620                    .discard_durable_frame_index_for_exclusive_rebuild();
1621                self.sync_authority_frames_from_local();
1622            }
1623            return;
1624        }
1625
1626        self.authority
1627            .discard_durable_frame_index_for_exclusive_rebuild();
1628        self.sync_authority_from_local();
1629        self.sync_authority_frames_from_local();
1630    }
1631
1632    fn local_scan_cannot_disprove_zero_frame_authority(
1633        authority_snapshot: SharedWalCoordinationHeader,
1634        local_snapshot: SharedWalCoordinationHeader,
1635    ) -> bool {
1636        !Self::authority_is_uninitialized(authority_snapshot)
1637            && authority_snapshot.max_frame == 0
1638            && local_snapshot.max_frame == 0
1639    }
1640
1641    fn local_scan_predates_zero_frame_authority(
1642        authority_snapshot: SharedWalCoordinationHeader,
1643        local_snapshot: SharedWalCoordinationHeader,
1644    ) -> bool {
1645        !Self::authority_is_uninitialized(authority_snapshot)
1646            && authority_snapshot.max_frame == 0
1647            && local_snapshot.max_frame > 0
1648            && local_snapshot.checkpoint_seq < authority_snapshot.checkpoint_seq
1649    }
1650
1651    fn local_scan_cannot_disprove_positive_authority(
1652        authority_snapshot: SharedWalCoordinationHeader,
1653        local_snapshot: SharedWalCoordinationHeader,
1654    ) -> bool {
1655        authority_snapshot.max_frame > 0
1656            && local_snapshot.max_frame == 0
1657            && local_snapshot.checkpoint_seq == authority_snapshot.checkpoint_seq
1658            && local_snapshot.page_size == authority_snapshot.page_size
1659            && local_snapshot.salt_1 == authority_snapshot.salt_1
1660            && local_snapshot.salt_2 == authority_snapshot.salt_2
1661    }
1662
1663    /// The authority is from a strictly newer WAL generation (higher
1664    /// checkpoint_seq), OR from the same generation with at least as many
1665    /// frames.  In either case the authority's header fields were updated
1666    /// atomically by writers and are at least as current as a point-in-time
1667    /// disk scan of the WAL file.
1668    ///
1669    /// When the generations match but the authority has a *lower* max_frame,
1670    /// the authority was likely rolled back or corrupted; the disk scan's
1671    /// higher max_frame is more accurate, so we must NOT match here.
1672    fn authority_is_same_or_newer_generation(
1673        authority_snapshot: SharedWalCoordinationHeader,
1674        local_snapshot: SharedWalCoordinationHeader,
1675    ) -> bool {
1676        if Self::authority_is_uninitialized(authority_snapshot) {
1677            return false;
1678        }
1679        // Strictly newer generation — always trust authority.
1680        if authority_snapshot.checkpoint_seq > local_snapshot.checkpoint_seq {
1681            return true;
1682        }
1683        // Same generation: the authority is atomically updated by writers,
1684        // so its max_frame is at least as current as what the disk scan
1685        // observed.  Only match when authority.max_frame >= local to
1686        // avoid masking a genuinely rolled-back authority.
1687        authority_snapshot.checkpoint_seq == local_snapshot.checkpoint_seq
1688            && authority_snapshot.salt_1 == local_snapshot.salt_1
1689            && authority_snapshot.salt_2 == local_snapshot.salt_2
1690            && authority_snapshot.max_frame >= local_snapshot.max_frame
1691    }
1692
1693    fn authority_matches_local_wal_scan(
1694        authority_snapshot: SharedWalCoordinationHeader,
1695        local_snapshot: SharedWalCoordinationHeader,
1696    ) -> bool {
1697        authority_snapshot.max_frame == local_snapshot.max_frame
1698            && authority_snapshot.checkpoint_seq == local_snapshot.checkpoint_seq
1699            && authority_snapshot.page_size == local_snapshot.page_size
1700            && authority_snapshot.salt_1 == local_snapshot.salt_1
1701            && authority_snapshot.salt_2 == local_snapshot.salt_2
1702            && authority_snapshot.checksum_1 == local_snapshot.checksum_1
1703            && authority_snapshot.checksum_2 == local_snapshot.checksum_2
1704    }
1705
1706    fn authority_frame_index_matches_local_wal_scan(&self, max_frame: u64) -> bool {
1707        self.authority.iter_latest_frames(0, max_frame)
1708            == self.fallback.iter_latest_frames(0, max_frame)
1709    }
1710
1711    fn local_zero_frame_generation_is_initialized(
1712        &self,
1713        authority_snapshot: SharedWalCoordinationHeader,
1714    ) -> bool {
1715        let shared = self.shared.read();
1716        if !shared.metadata.initialized.load(Ordering::Acquire) {
1717            return false;
1718        }
1719        Self::local_zero_frame_generation_matches_authority_snapshot(authority_snapshot, &shared)
1720    }
1721
1722    fn local_zero_frame_generation_matches_authority_snapshot(
1723        authority_snapshot: SharedWalCoordinationHeader,
1724        shared: &WalFileShared,
1725    ) -> bool {
1726        let header = shared.metadata.wal_header.lock();
1727        header.checkpoint_seq == authority_snapshot.checkpoint_seq
1728            && header.page_size == authority_snapshot.page_size
1729            && header.salt_1 == authority_snapshot.salt_1
1730            && header.salt_2 == authority_snapshot.salt_2
1731    }
1732
1733    fn authority_needs_local_header_seed(snapshot: SharedWalCoordinationHeader) -> bool {
1734        Self::authority_is_uninitialized(snapshot) || snapshot.page_size == 0
1735    }
1736
1737    /// Called once at `ShmWalCoordination` construction to reconcile the
1738    /// process-local WAL view (built from a WAL file scan or inherited from
1739    /// a previous connection) with the shared tshm authority.
1740    ///
1741    /// Three cases:
1742    ///
1743    /// 1. **Authority uninitialized** (fresh tshm): seed it from our local
1744    ///    WAL scan — we are the first process.
1745    ///
1746    /// 2. **Authority initialized and we opened from a local disk scan**
1747    ///    (writer/checkpoint locks acquired): repair transient reader
1748    ///    state first. If the scan only sees an empty WAL and the durable
1749    ///    authority is already at frame 0, keep the durable authority because
1750    ///    the scan cannot prove newer header metadata. Otherwise, if the
1751    ///    local scan agrees with the WAL-provable subset of the durable
1752    ///    snapshot, keep the durable authority. If not, discard the durable
1753    ///    frame index and rebuild it from the local scan.
1754    ///
1755    /// 3. **Authority initialized and trustworthy**: adopt the authority's
1756    ///    snapshot as our local state without modifying the shared index.
1757    fn seed_or_sync_authority(&self) {
1758        // A disk scan is evidence for one reconciliation only. Once shared
1759        // metadata adopts a peer's commit, our local frame cache still lacks
1760        // that peer's frames. Reusing the scan flag would make the next
1761        // connection publish that stale cache under the newer header.
1762        let reconciliation = self.shared.read().runtime.authority_reconciliation.clone();
1763        let _reconciliation = reconciliation.lock();
1764        let snapshot = self.authority.snapshot();
1765        let local_wal_view_loaded_from_disk = self
1766            .shared
1767            .read()
1768            .metadata
1769            .loaded_from_disk_scan
1770            .swap(false, Ordering::AcqRel);
1771        let recovery_guard =
1772            if Self::authority_is_uninitialized(snapshot) || local_wal_view_loaded_from_disk {
1773                self.authority.try_recovery_guard()
1774            } else {
1775                None
1776            };
1777        if let Some(_guard) = recovery_guard {
1778            // A peer may have committed since the first snapshot. Reload only
1779            // after acquiring both locks and keep them through index publication.
1780            let snapshot = self.authority.snapshot();
1781            if Self::authority_is_uninitialized(snapshot) {
1782                self.sync_authority_from_local();
1783                self.sync_authority_frames_from_local();
1784            } else {
1785                self.repair_or_reseed_authority_from_local_disk_scan(snapshot);
1786            }
1787        } else {
1788            let snapshot = self.authority.snapshot();
1789            let needs_zero_frame_header_rewrite = snapshot.max_frame == 0 && {
1790                let shared = self.shared.read();
1791                !shared.metadata.initialized.load(Ordering::Acquire)
1792                    || !Self::local_zero_frame_generation_matches_authority_snapshot(
1793                        snapshot, &shared,
1794                    )
1795            };
1796            self.sync_local_from_authority(snapshot);
1797            if needs_zero_frame_header_rewrite {
1798                self.shared
1799                    .read()
1800                    .metadata
1801                    .initialized
1802                    .store(false, Ordering::Release);
1803            }
1804        }
1805    }
1806
1807    fn restart_snapshot_from_authority(
1808        &self,
1809        snapshot: SharedWalCoordinationHeader,
1810        io: &dyn IO,
1811    ) -> WalSnapshot {
1812        let checkpoint_seq = snapshot.checkpoint_seq.wrapping_add(1);
1813        let salt_1 = snapshot.salt_1.wrapping_add(1);
1814        let salt_2 = io.generate_random_number() as u32;
1815        let restarted = SharedWalCoordinationHeader {
1816            max_frame: 0,
1817            nbackfills: 0,
1818            transaction_count: snapshot.transaction_count,
1819            visibility_generation: snapshot.visibility_generation,
1820            checkpoint_seq,
1821            checkpoint_epoch: snapshot.checkpoint_epoch,
1822            page_size: snapshot.page_size,
1823            salt_1,
1824            salt_2,
1825            checksum_1: snapshot.checksum_1,
1826            checksum_2: snapshot.checksum_2,
1827            reader_slot_count: snapshot.reader_slot_count,
1828        };
1829
1830        {
1831            let mut shared = self.shared.write();
1832            Self::install_local_snapshot(&mut shared, restarted, true);
1833            shared.metadata.initialized.store(false, Ordering::Release);
1834            shared.runtime.frame_cache.lock().clear();
1835            shared
1836                .runtime
1837                .frame_cache_high_water
1838                .store(0, Ordering::Release);
1839            shared.runtime.overflow_fallback_coverage.lock().clear();
1840            shared.runtime.read_locks[0].set_value_exclusive(0);
1841            shared.runtime.read_locks[1].set_value_exclusive(0);
1842            for lock in &shared.runtime.read_locks[2..] {
1843                lock.set_value_exclusive(READMARK_NOT_USED);
1844            }
1845        }
1846
1847        self.authority.rollback_frames(0);
1848        self.authority.install_snapshot(restarted);
1849
1850        WalSnapshot {
1851            max_frame: restarted.max_frame,
1852            nbackfills: restarted.nbackfills,
1853            last_checksum: (restarted.checksum_1, restarted.checksum_2),
1854            checkpoint_seq: restarted.checkpoint_seq,
1855            transaction_count: restarted.transaction_count,
1856        }
1857    }
1858
1859    fn ensure_local_frame_cache_covers_snapshot(
1860        &self,
1861        io: &Arc<dyn IO>,
1862        required_snapshot: WalSnapshot,
1863    ) -> Result<()> {
1864        if required_snapshot.max_frame == 0 || !self.authority.frame_index_overflowed() {
1865            return Ok(());
1866        }
1867
1868        let authority_snapshot = self.authority.snapshot();
1869        if authority_snapshot.checkpoint_seq != required_snapshot.checkpoint_seq {
1870            return Err(LimboError::Busy);
1871        }
1872        if self.overflow_fallback_covers(authority_snapshot, required_snapshot.max_frame) {
1873            return Ok(());
1874        }
1875
1876        let _ = io;
1877        tracing::debug!(
1878            required_max_frame = required_snapshot.max_frame,
1879            authority_max_frame = authority_snapshot.max_frame,
1880            authority_checkpoint_seq = authority_snapshot.checkpoint_seq,
1881            "refusing live overflow fallback refresh on a read path because it would require blocking WAL scan I/O"
1882        );
1883        Err(LimboError::Busy)
1884    }
1885}
1886
1887#[cfg(host_shared_wal)]
1888impl WalCoordination for ShmWalCoordination {
1889    fn load_snapshot(&self) -> WalSnapshot {
1890        let snapshot = self.authority.snapshot();
1891        WalSnapshot {
1892            max_frame: snapshot.max_frame,
1893            nbackfills: snapshot.nbackfills,
1894            last_checksum: (snapshot.checksum_1, snapshot.checksum_2),
1895            checkpoint_seq: snapshot.checkpoint_seq,
1896            transaction_count: snapshot.transaction_count,
1897        }
1898    }
1899
1900    fn ensure_local_frame_cache_covers(
1901        &self,
1902        io: &Arc<dyn IO>,
1903        snapshot: WalSnapshot,
1904    ) -> Result<()> {
1905        self.ensure_local_frame_cache_covers_snapshot(io, snapshot)
1906    }
1907
1908    fn publish_commit(&self, commit: WalCommitState) {
1909        {
1910            let mut shared = self.shared.write();
1911            shared
1912                .metadata
1913                .max_frame
1914                .store(commit.max_frame, Ordering::Release);
1915            shared.metadata.last_checksum = commit.last_checksum;
1916            shared
1917                .metadata
1918                .transaction_count
1919                .store(commit.transaction_count, Ordering::Release);
1920            let mut header = shared.metadata.wal_header.lock();
1921            header.checksum_1 = commit.last_checksum.0;
1922            header.checksum_2 = commit.last_checksum.1;
1923        }
1924        self.authority.publish_commit(
1925            commit.max_frame,
1926            commit.last_checksum.0,
1927            commit.last_checksum.1,
1928            commit.transaction_count,
1929        );
1930        if self.authority.frame_index_overflowed() {
1931            let snapshot = self.authority.snapshot();
1932            let shared = self.shared.read();
1933            let mut coverage = shared.runtime.overflow_fallback_coverage.lock();
1934            if coverage.covers(snapshot, commit.max_frame.saturating_sub(1)) {
1935                coverage.record_snapshot(snapshot, commit.max_frame);
1936            }
1937        }
1938    }
1939
1940    fn publish_backfill(&self, max_frame: u64) {
1941        self.shared
1942            .write()
1943            .metadata
1944            .nbackfills
1945            .store(max_frame, Ordering::Release);
1946        self.authority.publish_backfill(max_frame);
1947    }
1948
1949    fn install_durable_backfill_proof(
1950        &self,
1951        nbackfills: u64,
1952        db_size_pages: u32,
1953        db_header_crc32c: u32,
1954        sync_type: FileSyncType,
1955    ) -> Result<Option<Completion>> {
1956        let snapshot = self.authority.snapshot();
1957        turso_assert!(
1958            (snapshot.nbackfills..=snapshot.max_frame).contains(&nbackfills),
1959            "durable backfill proof requires nbackfills within the authoritative WAL range",
1960            {
1961                "nbackfills": nbackfills,
1962                "authority_nbackfills": snapshot.nbackfills,
1963                "authority_max_frame": snapshot.max_frame
1964            }
1965        );
1966        let proof_snapshot = SharedWalCoordinationHeader {
1967            nbackfills,
1968            ..snapshot
1969        };
1970        self.authority
1971            .install_backfill_proof(proof_snapshot, db_size_pages, db_header_crc32c);
1972        Ok(Some(self.authority.begin_sync(sync_type)?))
1973    }
1974
1975    fn find_frame(
1976        &self,
1977        page_id: u64,
1978        min_frame: u64,
1979        max_frame: u64,
1980        frame_watermark: Option<u64>,
1981    ) -> Option<u64> {
1982        // Exhausting the reserved shared index space leaves the authority
1983        // incomplete. Fall back to the local scanned cache rather than trusting
1984        // a truncated shared index.
1985        if self.authority.frame_index_overflowed() {
1986            return self
1987                .fallback
1988                .find_frame(page_id, min_frame, max_frame, frame_watermark);
1989        }
1990        self.authority
1991            .find_frame(page_id, min_frame, max_frame, frame_watermark)
1992    }
1993
1994    fn iter_latest_frames(&self, min_frame: u64, max_frame: u64) -> Vec<(u64, u64)> {
1995        // Same trade-off as find_frame(): if the reserved shared index space is
1996        // exhausted, keep correctness by consulting the local scanned cache.
1997        if self.authority.frame_index_overflowed() {
1998            return self.fallback.iter_latest_frames(min_frame, max_frame);
1999        }
2000        self.authority.iter_latest_frames(min_frame, max_frame)
2001    }
2002
2003    fn checkpoint_epoch(&self) -> u32 {
2004        self.authority.checkpoint_epoch()
2005    }
2006
2007    fn bump_checkpoint_epoch(&self) -> u32 {
2008        let prev = self.authority.bump_checkpoint_epoch();
2009        self.shared
2010            .write()
2011            .runtime
2012            .epoch
2013            .store(prev + 1, Ordering::Release);
2014        prev
2015    }
2016
2017    fn try_begin_read_tx(&self, snapshot: WalSnapshot) -> Option<ReadGuardKind> {
2018        turso_assert!(
2019            snapshot.max_frame <= u32::MAX as u64,
2020            "max_frame exceeds u32 read mark range"
2021        );
2022        let shared = self.shared.read();
2023        let read_locks = &shared.runtime.read_locks;
2024
2025        if snapshot.max_frame == snapshot.nbackfills {
2026            if !read_locks[0].read() {
2027                return None;
2028            }
2029            if self.load_snapshot() != snapshot {
2030                read_locks[0].unlock();
2031                return None;
2032            }
2033            return Some(ReadGuardKind::DbFile);
2034        }
2035
2036        let mut best_idx: i64 = -1;
2037        let mut best_mark: u32 = 0;
2038        for (idx, lock) in read_locks.iter().enumerate().take(5).skip(1) {
2039            let mark = lock.get_value();
2040            if mark != READMARK_NOT_USED && mark <= snapshot.max_frame as u32 && mark > best_mark {
2041                best_mark = mark;
2042                best_idx = idx as i64;
2043            }
2044        }
2045
2046        if best_idx == -1 || (best_mark as u64) < snapshot.max_frame {
2047            for (idx, lock) in read_locks.iter().enumerate().take(5).skip(1) {
2048                if !lock.write() {
2049                    continue;
2050                }
2051                lock.set_value_exclusive(snapshot.max_frame as u32);
2052                best_idx = idx as i64;
2053                best_mark = snapshot.max_frame as u32;
2054                read_locks[idx].unlock();
2055                break;
2056            }
2057        }
2058
2059        if best_idx == -1 || !read_locks[best_idx as usize].read() {
2060            return None;
2061        }
2062
2063        let current_slot_mark = read_locks[best_idx as usize].get_value();
2064        if current_slot_mark != best_mark || self.load_snapshot() != snapshot {
2065            read_locks[best_idx as usize].unlock();
2066            return None;
2067        }
2068
2069        let read_mark_index =
2070            NonZeroUsize::new(best_idx as usize).expect("best_idx checked to be positive");
2071        let reader = self
2072            .authority
2073            .register_reader_for_snapshot(self.owner, snapshot.max_frame)?;
2074        if self.load_snapshot() != snapshot {
2075            self.authority.unregister_reader_for_snapshot(reader);
2076            read_locks[best_idx as usize].unlock();
2077            return None;
2078        }
2079
2080        let mut active_reader = self.active_reader.lock();
2081        turso_assert!(active_reader.is_none(), "shared reader registration leaked");
2082        *active_reader = Some(reader);
2083        Some(ReadGuardKind::ReadMark(read_mark_index))
2084    }
2085
2086    fn end_read_tx(&self, guard: ReadGuardKind) {
2087        if let Some(reader) = self.active_reader.lock().take() {
2088            self.authority.unregister_reader_for_snapshot(reader);
2089        }
2090        self.fallback.end_read_tx(guard);
2091    }
2092
2093    fn try_begin_write_tx(&self) -> bool {
2094        if !self.authority.try_acquire_writer(self.owner) {
2095            return false;
2096        }
2097        if !self.fallback.try_write_lock() {
2098            self.authority.release_writer(self.owner);
2099            return false;
2100        }
2101        true
2102    }
2103
2104    fn end_write_tx(&self) {
2105        self.fallback.unlock_write_lock();
2106        self.authority.release_writer(self.owner);
2107    }
2108
2109    fn acquire_checkpoint_guard(
2110        &self,
2111        mode: CheckpointMode,
2112    ) -> Result<CoordinationCheckpointGuardKind> {
2113        if !self.authority.try_acquire_checkpoint(self.owner) {
2114            return Err(LimboError::Busy);
2115        }
2116        let needs_writer = !matches!(mode, CheckpointMode::Passive { .. });
2117        if needs_writer && !self.authority.try_acquire_writer(self.owner) {
2118            self.authority.release_checkpoint(self.owner);
2119            return Err(LimboError::Busy);
2120        }
2121        if !self.fallback.try_checkpoint_lock() {
2122            if needs_writer {
2123                self.authority.release_writer(self.owner);
2124            }
2125            self.authority.release_checkpoint(self.owner);
2126            return Err(LimboError::Busy);
2127        }
2128        match mode {
2129            CheckpointMode::Passive { .. } => {
2130                if !self.fallback.try_read_mark_exclusive(0) {
2131                    self.fallback.unlock_checkpoint_lock();
2132                    if needs_writer {
2133                        self.authority.release_writer(self.owner);
2134                    }
2135                    self.authority.release_checkpoint(self.owner);
2136                    return Err(LimboError::Busy);
2137                }
2138                Ok(CoordinationCheckpointGuardKind::Read0)
2139            }
2140            CheckpointMode::Full | CheckpointMode::Restart | CheckpointMode::Truncate { .. } => {
2141                if !self.fallback.try_read_mark_exclusive(0) {
2142                    self.fallback.unlock_checkpoint_lock();
2143                    self.authority.release_writer(self.owner);
2144                    self.authority.release_checkpoint(self.owner);
2145                    return Err(LimboError::Busy);
2146                }
2147                if !self.fallback.try_write_lock() {
2148                    self.fallback.unlock_read_mark(0);
2149                    self.fallback.unlock_checkpoint_lock();
2150                    self.authority.release_writer(self.owner);
2151                    self.authority.release_checkpoint(self.owner);
2152                    return Err(LimboError::Busy);
2153                }
2154                Ok(CoordinationCheckpointGuardKind::Writer)
2155            }
2156        }
2157    }
2158
2159    fn acquire_vacuum_checkpoint_guard_from_held_lock(
2160        &self,
2161    ) -> Result<CoordinationCheckpointGuardKind> {
2162        if !self.authority.try_acquire_checkpoint(self.owner) {
2163            self.fallback.unlock_checkpoint_lock();
2164            return Err(LimboError::Busy);
2165        }
2166        if !self.authority.try_acquire_writer(self.owner) {
2167            self.authority.release_checkpoint(self.owner);
2168            self.fallback.unlock_checkpoint_lock();
2169            return Err(LimboError::Busy);
2170        }
2171        if !self.fallback.try_read_mark_exclusive(0) {
2172            self.fallback.unlock_checkpoint_lock();
2173            self.authority.release_writer(self.owner);
2174            self.authority.release_checkpoint(self.owner);
2175            return Err(LimboError::Busy);
2176        }
2177        if !self.fallback.try_write_lock() {
2178            self.fallback.unlock_read_mark(0);
2179            self.fallback.unlock_checkpoint_lock();
2180            self.authority.release_writer(self.owner);
2181            self.authority.release_checkpoint(self.owner);
2182            return Err(LimboError::Busy);
2183        }
2184        Ok(CoordinationCheckpointGuardKind::Writer)
2185    }
2186
2187    fn release_checkpoint_guard(&self, guard: CoordinationCheckpointGuardKind) {
2188        match guard {
2189            CoordinationCheckpointGuardKind::Writer => {
2190                self.fallback.unlock_write_lock();
2191                self.fallback.unlock_read_mark(0);
2192                self.fallback.unlock_checkpoint_lock();
2193                self.authority.release_writer(self.owner);
2194                self.authority.release_checkpoint(self.owner);
2195            }
2196            CoordinationCheckpointGuardKind::Read0 => {
2197                self.fallback.unlock_read_mark(0);
2198                self.fallback.unlock_checkpoint_lock();
2199                self.authority.release_checkpoint(self.owner);
2200            }
2201        }
2202    }
2203
2204    fn determine_max_safe_checkpoint_frame(&self, max_frame: u64) -> u64 {
2205        turso_assert!(
2206            max_frame <= u32::MAX as u64,
2207            "max_frame exceeds u32 read mark range"
2208        );
2209        let mut max_safe_frame = max_frame;
2210        for read_lock_idx in 1..5 {
2211            let this_mark = self.fallback.read_mark_value(read_lock_idx);
2212            if this_mark < max_safe_frame as u32 {
2213                let busy = !self.fallback.try_read_mark_exclusive(read_lock_idx);
2214                if !busy {
2215                    let val = if read_lock_idx == 1 {
2216                        max_safe_frame as u32
2217                    } else {
2218                        READMARK_NOT_USED
2219                    };
2220                    self.fallback
2221                        .set_read_mark_value_exclusive(read_lock_idx, val);
2222                    self.fallback.unlock_read_mark(read_lock_idx);
2223                } else {
2224                    max_safe_frame = this_mark as u64;
2225                }
2226            }
2227        }
2228        match self.authority.min_active_reader_frame() {
2229            Some(shared_min) => max_safe_frame.min(shared_min),
2230            None => max_safe_frame,
2231        }
2232    }
2233
2234    fn min_pinned_read_frame(&self) -> Option<u64> {
2235        // Combine this process's local read marks with cross-process readers tracked by the
2236        // shared authority.
2237        let local = self.fallback.min_pinned_read_frame_inner();
2238        match (local, self.authority.min_active_reader_frame()) {
2239            (Some(a), Some(b)) => Some(a.min(b)),
2240            (Some(a), None) => Some(a),
2241            (None, b) => b,
2242        }
2243    }
2244
2245    fn begin_restart(&self, io: &dyn IO) -> Result<WalSnapshot> {
2246        for idx in 1..5 {
2247            if !self.fallback.try_read_mark_exclusive(idx) {
2248                for held_idx in 1..idx {
2249                    self.fallback.unlock_read_mark(held_idx);
2250                }
2251                return Err(LimboError::Busy);
2252            }
2253        }
2254        // In multi-process mode, readers register with the authority (tshm shared
2255        // memory), not with fallback OFD byte-range locks. We must also check for
2256        // active cross-process readers before proceeding with the WAL restart,
2257        // otherwise we reset the shared WAL state while another process still has
2258        // an active read transaction, leading to data loss.
2259        if self.authority.min_active_reader_frame().is_some() {
2260            for idx in 1..5 {
2261                self.fallback.unlock_read_mark(idx);
2262            }
2263            return Err(LimboError::Busy);
2264        }
2265        Ok(self.restart_snapshot_from_authority(self.authority.snapshot(), io))
2266    }
2267
2268    fn end_restart(&self) {
2269        self.fallback.end_restart();
2270    }
2271
2272    fn try_restart_log_for_write(&self, io: &dyn IO) -> Result<Option<WalSnapshot>> {
2273        if !self.fallback.try_upgrade_read_mark(0) {
2274            return Ok(None);
2275        }
2276        let result = self.begin_restart(io);
2277        self.fallback.downgrade_read_mark(0);
2278        match result {
2279            Ok(snapshot) => {
2280                self.end_restart();
2281                Ok(Some(snapshot))
2282            }
2283            Err(err) => Err(err),
2284        }
2285    }
2286
2287    fn prepare_truncate(&self) -> Result<Arc<dyn File>> {
2288        self.fallback.prepare_truncate()
2289    }
2290
2291    fn wal_header(&self) -> WalHeader {
2292        let snapshot = self.authority.snapshot();
2293        let mut header = self.fallback.wal_header();
2294        if snapshot.page_size == 0 {
2295            return header;
2296        }
2297        header.page_size = snapshot.page_size;
2298        header.checkpoint_seq = snapshot.checkpoint_seq;
2299        header.salt_1 = snapshot.salt_1;
2300        header.salt_2 = snapshot.salt_2;
2301        header.checksum_1 = snapshot.checksum_1;
2302        header.checksum_2 = snapshot.checksum_2;
2303        header
2304    }
2305
2306    fn wal_file(&self) -> Result<Arc<dyn File>> {
2307        self.fallback.wal_file()
2308    }
2309
2310    fn shared_wal_state(&self) -> Arc<RwLock<WalFileShared>> {
2311        self.shared.clone()
2312    }
2313
2314    fn wal_is_initialized(&self) -> bool {
2315        let authority_snapshot = self.authority.snapshot();
2316        if Self::authority_needs_local_header_seed(authority_snapshot) {
2317            return self.fallback.wal_is_initialized();
2318        }
2319        if authority_snapshot.max_frame > 0 {
2320            self.sync_local_from_authority(authority_snapshot);
2321            self.fallback.mark_initialized();
2322            return true;
2323        }
2324        if self.local_zero_frame_generation_is_initialized(authority_snapshot) {
2325            return true;
2326        }
2327
2328        self.sync_local_from_authority(authority_snapshot);
2329        self.shared
2330            .read()
2331            .metadata
2332            .initialized
2333            .store(false, Ordering::Release);
2334        false
2335    }
2336
2337    fn prepare_wal_header(&self, io: &dyn IO, page_size: PageSize) -> Option<WalHeader> {
2338        let authority_snapshot = self.authority.snapshot();
2339        // A zero-frame authority snapshot after RESTART/TRUNCATE is still
2340        // authoritative: it carries the latest transaction_count,
2341        // checkpoint_seq, salts, and checksums for readers. Sync from it
2342        // before preparing the header so the bytes written to disk belong to
2343        // the same generation as the authority snapshot.
2344        if Self::authority_needs_local_header_seed(authority_snapshot) {
2345            let header = self.fallback.prepare_wal_header(io, page_size);
2346            if header.is_some() {
2347                self.sync_authority_from_local();
2348            }
2349            return header;
2350        }
2351        self.sync_local_from_authority(authority_snapshot);
2352        let header = self.fallback.prepare_wal_header(io, page_size);
2353        if header.is_some() {
2354            self.sync_authority_from_local();
2355        }
2356        header
2357    }
2358
2359    fn mark_initialized(&self) {
2360        self.fallback.mark_initialized();
2361    }
2362
2363    fn cache_frame(&self, page_id: u64, frame_id: u64) {
2364        self.fallback.cache_frame(page_id, frame_id);
2365        self.authority.record_frame(page_id, frame_id);
2366    }
2367
2368    fn rollback_cache(&self, max_frame: u64) {
2369        self.fallback.rollback_cache(max_frame);
2370        self.authority.rollback_frames(max_frame);
2371        self.clear_overflow_fallback_coverage();
2372    }
2373
2374    fn should_checkpoint_on_close(&self) -> bool {
2375        self.authority.is_last_process_mapping()
2376    }
2377
2378    #[cfg(clt_turso_tests)]
2379    fn backend_name(&self) -> &'static str {
2380        "tshm"
2381    }
2382
2383    #[cfg(clt_turso_tests)]
2384    fn shared_ptr(&self) -> usize {
2385        Arc::as_ptr(&self.shared) as usize
2386    }
2387
2388    #[cfg(clt_turso_tests)]
2389    fn open_mode_name(&self) -> Option<&'static str> {
2390        Some(match self.authority.open_mode() {
2391            SharedWalCoordinationOpenMode::Exclusive => "exclusive",
2392            SharedWalCoordinationOpenMode::MultiProcess => "multiprocess",
2393        })
2394    }
2395}
2396
2397#[derive(Debug, Clone)]
2398pub enum CheckpointState {
2399    Start,
2400    Processing,
2401    /// Determine the checkpoint result: update nBackfills, restart log if needed.
2402    DetermineResult,
2403    /// Final cleanup: release locks, clear internal state, return result.
2404    /// WAL truncation (if needed) is handled by pager.rs via truncate_wal() AFTER the DB is synced.
2405    Finalize {
2406        checkpoint_result: Option<CheckpointResult>,
2407    },
2408}
2409
2410/// IOV_MAX is 1024 on most systems, lets use 512 to be safe
2411pub const CKPT_BATCH_PAGES: usize = 512;
2412
2413/// TODO: *ALL* of these need to be tuned for perf. It is tricky
2414/// trying to figure out the ideal numbers here to work together concurrently
2415const MIN_AVG_RUN_FOR_FLUSH: f32 = 32.0;
2416const MIN_BATCH_LEN_FOR_FLUSH: usize = 512;
2417const MAX_INFLIGHT_WRITES: usize = 64;
2418pub const MAX_INFLIGHT_READS: usize = 512;
2419pub const IOV_MAX: usize = 1024;
2420
2421type PageId = usize;
2422struct InflightRead {
2423    completion: Completion,
2424    page_id: PageId,
2425    /// Buffer slot to contain the page content from the WAL read.
2426    buf: Arc<SpinLock<Option<Arc<Buffer>>>>,
2427}
2428
2429/// WriteBatch is a collection of pages that are being checkpointed together. It is used to
2430/// aggregate contiguous pages into a single write operation to the database file.
2431#[derive(Default)]
2432struct WriteBatch {
2433    /// BTreeMap for sorting during insertion, helps create more efficient `writev` operations.
2434    items: BTreeMap<PageId, Arc<Buffer>>,
2435    /// total number of `runs`, each representing a contiguous group of `PageId`s
2436    run_count: usize,
2437}
2438
2439impl WriteBatch {
2440    fn new() -> Self {
2441        Self {
2442            items: BTreeMap::new(),
2443            run_count: 0,
2444        }
2445    }
2446
2447    #[inline]
2448    /// Add a pageId + Buffer to the batch of Writes to be submitted.
2449    fn insert(&mut self, page_id: PageId, buf: Arc<Buffer>) {
2450        if let std::collections::btree_map::Entry::Occupied(mut e) = self.items.entry(page_id) {
2451            e.insert(buf);
2452            return;
2453        }
2454        // Single range query to check neighbors
2455        let start = page_id.saturating_sub(1);
2456        let end = page_id.saturating_add(1);
2457        let mut has_left = false;
2458        let mut has_right = false;
2459
2460        for (k, _) in self.items.range(start..=end) {
2461            if *k == page_id.wrapping_sub(1) {
2462                has_left = true;
2463            }
2464            if *k == page_id.wrapping_add(1) {
2465                has_right = true;
2466            }
2467        }
2468        match (has_left, has_right) {
2469            (false, false) => self.run_count += 1,
2470            (true, true) => self.run_count = self.run_count.saturating_sub(1),
2471            _ => {}
2472        }
2473        self.items.insert(page_id, buf);
2474    }
2475
2476    #[inline]
2477    fn len(&self) -> usize {
2478        self.items.len()
2479    }
2480    #[inline]
2481    fn is_empty(&self) -> bool {
2482        self.items.is_empty()
2483    }
2484    #[inline]
2485    fn is_full(&self) -> bool {
2486        self.items.len() >= CKPT_BATCH_PAGES
2487    }
2488
2489    #[inline]
2490    fn avg_run_len(&self) -> f32 {
2491        if self.run_count == 0 {
2492            0.0
2493        } else {
2494            self.items.len() as f32 / self.run_count as f32
2495        }
2496    }
2497
2498    #[inline]
2499    fn take(&mut self) -> BTreeMap<PageId, Arc<Buffer>> {
2500        self.run_count = 0;
2501        std::mem::take(&mut self.items)
2502    }
2503
2504    #[inline]
2505    fn clear(&mut self) {
2506        self.items.clear();
2507        self.run_count = 0;
2508    }
2509}
2510
2511impl std::ops::Deref for WriteBatch {
2512    type Target = BTreeMap<PageId, Arc<Buffer>>;
2513    fn deref(&self) -> &Self::Target {
2514        &self.items
2515    }
2516}
2517impl std::ops::DerefMut for WriteBatch {
2518    fn deref_mut(&mut self) -> &mut Self::Target {
2519        &mut self.items
2520    }
2521}
2522
2523/// Information and structures for processing a checkpoint operation.
2524struct OngoingCheckpoint {
2525    /// Used for benchmarking/debugging a checkpoint operation.
2526    time: MonotonicInstant,
2527    /// minimum frame number to be backfilled by this checkpoint operation.
2528    min_frame: u64,
2529    /// maximum safe frame number that will be backfilled by this checkpoint operation.
2530    max_frame: u64,
2531    /// cursor used to iterate through all the pages that might have a frame in the safe range
2532    current_page: u64,
2533    /// State of the checkpoint
2534    state: CheckpointState,
2535    /// Batch repreesnts a collection of pages to be backfilled to the DB file.
2536    pending_writes: WriteBatch,
2537    /// Read operations currently ongoing.
2538    inflight_reads: Vec<InflightRead>,
2539    /// Array of atomic counters representing write operations that are currently in flight.
2540    inflight_writes: Vec<InflightWriteBatch>,
2541    /// List of all page_id + frame_id combinations to be backfilled
2542    pages_to_checkpoint: Vec<(u64, u64)>,
2543}
2544
2545struct InflightWriteBatch {
2546    done: Arc<AtomicBool>,
2547    err: Arc<crate::sync::OnceLock<CompletionError>>,
2548}
2549
2550impl OngoingCheckpoint {
2551    fn reset(&mut self) {
2552        self.min_frame = 0;
2553        self.max_frame = 0;
2554        self.current_page = 0;
2555        self.pages_to_checkpoint.clear();
2556        self.pending_writes.clear();
2557        self.inflight_reads.clear();
2558        self.inflight_writes.clear();
2559        self.state = CheckpointState::Start;
2560    }
2561
2562    #[inline]
2563    /// Whether or not new reads should be issued during checkpoint processing.
2564    fn should_issue_reads(&self) -> bool {
2565        (self.current_page as usize) < self.pages_to_checkpoint.len()
2566            && !self.pending_writes.is_full()
2567            && self.inflight_reads.len() < MAX_INFLIGHT_READS
2568    }
2569
2570    #[inline]
2571    /// Whether the backfilling/IO process is entirely completed during checkpoint processing.
2572    fn complete(&self) -> bool {
2573        (self.current_page as usize) >= self.pages_to_checkpoint.len()
2574            && self.inflight_reads.is_empty()
2575            && self.pending_writes.is_empty()
2576            && self.inflight_writes.is_empty()
2577    }
2578
2579    #[inline]
2580    /// Whether we should flush an exisitng batch of writes and begin concurrently aggregating a new one.
2581    fn should_flush_batch(&self) -> bool {
2582        self.pending_writes.is_full()
2583            || (self.pending_writes.len() >= MIN_BATCH_LEN_FOR_FLUSH
2584                && self.pending_writes.avg_run_len() >= MIN_AVG_RUN_FOR_FLUSH)
2585            || ((self.current_page as usize) >= self.pages_to_checkpoint.len()
2586                && self.inflight_reads.is_empty()
2587                && !self.pending_writes.is_empty())
2588    }
2589
2590    #[inline]
2591    /// Remove any completed write operations from `inflight_writes`,
2592    /// returns whether any progress was made.
2593    fn process_inflight_writes(&mut self) -> bool {
2594        let before_len = self.inflight_writes.len();
2595        self.inflight_writes
2596            .retain(|w| !w.done.load(Ordering::Acquire));
2597        before_len > self.inflight_writes.len()
2598    }
2599
2600    #[inline]
2601    /// Remove any completed read operations from `inflight_reads`
2602    /// returns whether any progress was made.
2603    fn process_pending_reads(&mut self) -> Result<bool> {
2604        let mut moved = false;
2605        let mut err: Option<CompletionError> = None;
2606
2607        self.inflight_reads.retain(|slot| {
2608            if !slot.completion.finished() {
2609                return true;
2610            }
2611            if slot.completion.succeeded() {
2612                if let Some(buf) = slot.buf.lock().take() {
2613                    self.pending_writes.insert(slot.page_id, buf);
2614                    moved = true;
2615                } else {
2616                    err = Some(CompletionError::IOError(std::io::ErrorKind::Other, "read"));
2617                }
2618            } else {
2619                err = Some(
2620                    slot.completion
2621                        .get_error()
2622                        .unwrap_or(CompletionError::IOError(std::io::ErrorKind::Other, "read")),
2623                );
2624            }
2625            false
2626        });
2627        if let Some(e) = err {
2628            return Err(LimboError::CompletionError(e));
2629        }
2630        Ok(moved)
2631    }
2632
2633    fn first_write_error(&self) -> Option<CompletionError>
2634    where
2635        CompletionError: Clone,
2636    {
2637        self.inflight_writes
2638            .iter()
2639            .find_map(|w| w.err.get().cloned())
2640    }
2641}
2642
2643impl InflightWriteBatch {
2644    #[inline]
2645    fn new() -> InflightWriteBatch {
2646        InflightWriteBatch {
2647            done: Arc::new(AtomicBool::new(false)),
2648            err: Arc::new(OnceLock::new()),
2649        }
2650    }
2651}
2652
2653impl fmt::Debug for OngoingCheckpoint {
2654    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2655        f.debug_struct("OngoingCheckpoint")
2656            .field("state", &self.state)
2657            .field("min_frame", &self.min_frame)
2658            .field("max_frame", &self.max_frame)
2659            .field("current_page", &self.current_page)
2660            .finish()
2661    }
2662}
2663
2664pub struct WalFile {
2665    io: Arc<dyn IO>,
2666    buffer_pool: Arc<BufferPool>,
2667    coordination: Arc<dyn WalCoordination>,
2668
2669    syncing: Arc<AtomicBool>,
2670    write_lock_held: AtomicBool,
2671
2672    ongoing_checkpoint: RwLock<OngoingCheckpoint>,
2673    checkpoint_threshold: usize,
2674    /// This is the index to the read_lock in WalFileShared that we are holding. This lock contains
2675    /// the max frame for this connection.
2676    max_frame_read_lock_index: AtomicUsize,
2677    /// Max frame allowed to lookup range=(minframe..max_frame)
2678    max_frame: AtomicU64,
2679    /// Start of range to look for frames range=(minframe..max_frame)
2680    min_frame: AtomicU64,
2681    /// Check of last frame in WAL, this is a cumulative checksum over all frames in the WAL
2682    last_checksum: RwLock<(u32, u32)>,
2683    checkpoint_seq: AtomicU32,
2684    transaction_count: AtomicU64,
2685
2686    /// Manages locks needed for checkpointing
2687    checkpoint_guard: RwLock<Option<CheckpointLocks>>,
2688    /// Manages locks needed for VACUUM. This is very much similar to `checkpoint_guard`
2689    /// This lock is to be held by all readers before they can begin. And VACUUM holds it
2690    /// exclusively. See `install_vacuum_lock_guard` for its lifecycle.
2691    vacuum_lock_guard: RwLock<Option<VacuumLockGuard>>,
2692
2693    io_ctx: RwLock<IOContext>,
2694
2695    /// The WAL file is dirty: frames were appended that no successful fsync
2696    /// has covered yet. Set whenever a frame is recorded via
2697    /// `complete_append_frame`, cleared when a WAL fsync completes
2698    /// successfully. A dirty WAL owes an fsync before a commit may be
2699    /// reported durable under synchronous=FULL.
2700    /// Shared with the fsync completion callback, hence the Arc.
2701    dirty: Arc<AtomicBool>,
2702}
2703
2704impl fmt::Debug for WalFile {
2705    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2706        f.debug_struct("WalFile")
2707            .field("syncing", &self.syncing.load(Ordering::Relaxed))
2708            .field("page_size", &self.page_size())
2709            .field("ongoing_checkpoint", &*self.ongoing_checkpoint.read())
2710            .field("checkpoint_threshold", &self.checkpoint_threshold)
2711            .field("max_frame_read_lock_index", &self.max_frame_read_lock_index)
2712            .field("max_frame", &self.max_frame)
2713            .field("min_frame", &self.min_frame)
2714            // Excluding other fields
2715            .finish()
2716    }
2717}
2718
2719/*
2720* sqlite3/src/wal.c
2721*
2722** nBackfill is the number of frames in the WAL that have been written
2723** back into the database. (We call the act of moving content from WAL to
2724** database "backfilling".)  The nBackfill number is never greater than
2725** WalIndexHdr.mxFrame.  nBackfill can only be increased by threads
2726** holding the WAL_CKPT_LOCK lock (which includes a recovery thread).
2727** However, a WAL_WRITE_LOCK thread can move the value of nBackfill from
2728** mxFrame back to zero when the WAL is reset.
2729**
2730** nBackfillAttempted is the largest value of nBackfill that a checkpoint
2731** has attempted to achieve.  Normally nBackfill==nBackfillAtempted, however
2732** the nBackfillAttempted is set before any backfilling is done and the
2733** nBackfill is only set after all backfilling completes.  So if a checkpoint
2734** crashes, nBackfillAttempted might be larger than nBackfill.  The
2735** WalIndexHdr.mxFrame must never be less than nBackfillAttempted.
2736**
2737** The aLock[] field is a set of bytes used for locking.  These bytes should
2738** never be read or written.
2739**
2740** There is one entry in aReadMark[] for each reader lock.  If a reader
2741** holds read-lock K, then the value in aReadMark[K] is no greater than
2742** the mxFrame for that reader.  The value READMARK_NOT_USED (0xffffffff)
2743** for any aReadMark[] means that entry is unused.  aReadMark[0] is
2744** a special case; its value is never used and it exists as a place-holder
2745** to avoid having to offset aReadMark[] indexes by one.  Readers holding
2746** WAL_READ_LOCK(0) always ignore the entire WAL and read all content
2747** directly from the database.
2748**
2749** The value of aReadMark[K] may only be changed by a thread that
2750** is holding an exclusive lock on WAL_READ_LOCK(K).  Thus, the value of
2751** aReadMark[K] cannot changed while there is a reader is using that mark
2752** since the reader will be holding a shared lock on WAL_READ_LOCK(K).
2753**
2754** The checkpointer may only transfer frames from WAL to database where
2755** the frame numbers are less than or equal to every aReadMark[] that is
2756** in use (that is, every aReadMark[j] for which there is a corresponding
2757** WAL_READ_LOCK(j)).  New readers (usually) pick the aReadMark[] with the
2758** largest value and will increase an unused aReadMark[] to mxFrame if there
2759** is not already an aReadMark[] equal to mxFrame.  The exception to the
2760** previous sentence is when nBackfill equals mxFrame (meaning that everything
2761** in the WAL has been backfilled into the database) then new readers
2762** will choose aReadMark[0] which has value 0 and hence such reader will
2763** get all their all content directly from the database file and ignore
2764** the WAL.
2765**
2766** Writers normally append new frames to the end of the WAL.  However,
2767** if nBackfill equals mxFrame (meaning that all WAL content has been
2768** written back into the database) and if no readers are using the WAL
2769** (in other words, if there are no WAL_READ_LOCK(i) where i>0) then
2770** the writer will first "reset" the WAL back to the beginning and start
2771** writing new content beginning at frame 1.
2772*/
2773
2774/// Authoritative WAL metadata currently shared by all connections in a process.
2775pub struct WalSharedMetadata {
2776    pub enabled: AtomicBool,
2777    pub wal_header: Arc<SpinLock<WalHeader>>,
2778    pub min_frame: AtomicU64,
2779    pub max_frame: AtomicU64,
2780    pub nbackfills: AtomicU64,
2781    pub transaction_count: AtomicU64,
2782    pub last_checksum: (u32, u32), // Check of last frame in WAL, this is a cumulative checksum over all frames in the WAL
2783    pub loaded: AtomicBool,
2784    pub loaded_from_disk_scan: AtomicBool,
2785    pub initialized: AtomicBool,
2786}
2787
2788/// Process-local coordination and caches layered around the shared WAL metadata.
2789pub struct WalSharedRuntime {
2790    /// Serialize connection-open reconciliation of a one-use local WAL scan.
2791    pub authority_reconciliation: Arc<Mutex<()>>,
2792    // Frame cache maps a Page to all the frames it has stored in WAL in ascending order.
2793    // This is to easily find the frame it must checkpoint each connection if a checkpoint is
2794    // necessary.
2795    // One difference between SQLite and limbo is that we will never support multi process, meaning
2796    // we don't need WAL's index file. So we can do stuff like this without shared memory.
2797    // TODO: this will need refactoring because this is incredible memory inefficient.
2798    pub frame_cache: Arc<SpinLock<FxHashMap<u64, Vec<u64>>>>,
2799    /// Highest frame number currently recorded in `frame_cache` for the active
2800    /// WAL generation. Used to detect frame-slot reuse / append-position
2801    /// rewinds: within a generation frames are appended with strictly
2802    /// increasing numbers, so caching a frame that is not above this watermark
2803    /// means the slots from that frame upward are being overwritten and any
2804    /// stale `page -> frame` mappings for them must be purged (otherwise
2805    /// `find_frame` can return a frame slot that now holds a different page).
2806    /// Only read/written while holding the `frame_cache` lock.
2807    pub frame_cache_high_water: AtomicU64,
2808    pub file: Option<Arc<dyn File>>,
2809    /// Read locks advertise the maximum WAL frame a reader may access.
2810    /// Slot 0 is special, when it is held (shared) the reader bypasses the WAL and uses the main DB file.
2811    /// When checkpointing, we must acquire the exclusive read lock 0 to ensure that no readers read
2812    /// from a partially checkpointed db file.
2813    /// Slots 1‑4 carry a frame‑number in value and may be shared by many readers. Slot 1 is the
2814    /// default read lock and is to contain the max_frame in WAL.
2815    pub read_locks: [TursoRwLock; 5],
2816    /// Lock used by in-place VACUUM to keep new read/write transactions out
2817    /// while VACUUM is in progress.
2818    /// Normal WAL transactions hold this shared for the lifetime of their
2819    /// transaction. VACUUM holds it exclusively until its final truncate
2820    /// checkpoint has completed.
2821    pub vacuum_lock: TursoRwLock,
2822    /// There is only one write allowed in WAL mode. This lock takes care of ensuring there is only
2823    /// one used.
2824    pub write_lock: TursoRwLock,
2825
2826    /// Serialises checkpointer threads, only one checkpoint can be in flight at any time. Blocking and exclusive only
2827    pub checkpoint_lock: TursoRwLock,
2828    /// Increments on each checkpoint, used to prevent stale cached pages being used for
2829    /// backfilling.
2830    pub epoch: AtomicU32,
2831    /// Tracks how far the process-local `frame_cache` is known to be complete
2832    /// for overflow fallback in the current WAL generation.
2833    pub overflow_fallback_coverage: Arc<SpinLock<OverflowFallbackCoverage>>,
2834}
2835
2836/// Drivable result of [`WalFileShared::open_shared_if_exists_begin`]. Either an
2837/// immediate no-op WAL (readonly, file absent) or an in-progress recovery scan
2838/// to be pumped via [`OpenSharedWal::poll`] until it returns `Done`.
2839pub enum OpenSharedWal {
2840    Noop(Arc<RwLock<WalFileShared>>),
2841    Build(sqlite3_ondisk::BuildSharedWal),
2842}
2843
2844impl OpenSharedWal {
2845    pub fn poll(&mut self) -> Result<IOResult<Arc<RwLock<WalFileShared>>>> {
2846        match self {
2847            OpenSharedWal::Noop(wal) => Ok(IOResult::Done(wal.clone())),
2848            OpenSharedWal::Build(driver) => driver.poll(),
2849        }
2850    }
2851}
2852
2853/// WalFileShared holds process-wide WAL metadata plus process-local coordination state.
2854pub struct WalFileShared {
2855    pub metadata: WalSharedMetadata,
2856    pub runtime: WalSharedRuntime,
2857}
2858
2859#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2860pub struct OverflowFallbackCoverage {
2861    checkpoint_seq: u32,
2862    salt_1: u32,
2863    salt_2: u32,
2864    max_frame: u64,
2865    valid: bool,
2866}
2867
2868impl OverflowFallbackCoverage {
2869    pub(crate) fn clear(&mut self) {
2870        *self = Self::default();
2871    }
2872
2873    pub(crate) fn record(&mut self, checkpoint_seq: u32, salt_1: u32, salt_2: u32, max_frame: u64) {
2874        if max_frame == 0 {
2875            self.clear();
2876            return;
2877        }
2878        self.checkpoint_seq = checkpoint_seq;
2879        self.salt_1 = salt_1;
2880        self.salt_2 = salt_2;
2881        self.max_frame = max_frame;
2882        self.valid = true;
2883    }
2884
2885    #[cfg(host_shared_wal)]
2886    pub(crate) fn record_snapshot(
2887        &mut self,
2888        snapshot: SharedWalCoordinationHeader,
2889        max_frame: u64,
2890    ) {
2891        self.record(
2892            snapshot.checkpoint_seq,
2893            snapshot.salt_1,
2894            snapshot.salt_2,
2895            max_frame,
2896        );
2897    }
2898
2899    #[cfg(host_shared_wal)]
2900    pub(crate) fn same_generation(&self, snapshot: SharedWalCoordinationHeader) -> bool {
2901        self.valid
2902            && self.checkpoint_seq == snapshot.checkpoint_seq
2903            && self.salt_1 == snapshot.salt_1
2904            && self.salt_2 == snapshot.salt_2
2905    }
2906
2907    #[cfg(host_shared_wal)]
2908    pub(crate) fn covers(&self, snapshot: SharedWalCoordinationHeader, max_frame: u64) -> bool {
2909        self.same_generation(snapshot) && self.max_frame >= max_frame
2910    }
2911}
2912
2913impl fmt::Debug for WalFileShared {
2914    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2915        f.debug_struct("WalFileShared")
2916            .field("enabled", &self.metadata.enabled.load(Ordering::Relaxed))
2917            .field("wal_header", &self.metadata.wal_header)
2918            .field("min_frame", &self.metadata.min_frame)
2919            .field("max_frame", &self.metadata.max_frame)
2920            .field("nbackfills", &self.metadata.nbackfills)
2921            .field("frame_cache", &self.runtime.frame_cache)
2922            .field("last_checksum", &self.metadata.last_checksum)
2923            // Excluding `file`, `read_locks`, and `write_lock`
2924            .finish()
2925    }
2926}
2927
2928#[derive(Debug)]
2929enum VacuumLockGuard {
2930    Read { ptr: Arc<RwLock<WalFileShared>> },
2931    Write { ptr: Arc<RwLock<WalFileShared>> },
2932}
2933
2934impl VacuumLockGuard {
2935    fn try_read(ptr: Arc<RwLock<WalFileShared>>) -> Option<Self> {
2936        let acquired = {
2937            let shared = ptr.read();
2938            shared.runtime.vacuum_lock.read()
2939        };
2940        if acquired {
2941            Some(Self::Read { ptr })
2942        } else {
2943            None
2944        }
2945    }
2946
2947    fn try_write(ptr: Arc<RwLock<WalFileShared>>) -> Option<Self> {
2948        let acquired = {
2949            let shared = ptr.read();
2950            shared.runtime.vacuum_lock.write()
2951        };
2952        if acquired {
2953            Some(Self::Write { ptr })
2954        } else {
2955            None
2956        }
2957    }
2958
2959    const fn is_read(&self) -> bool {
2960        matches!(self, Self::Read { .. })
2961    }
2962
2963    const fn is_write(&self) -> bool {
2964        matches!(self, Self::Write { .. })
2965    }
2966}
2967
2968impl Drop for VacuumLockGuard {
2969    fn drop(&mut self) {
2970        match self {
2971            Self::Read { ptr } | Self::Write { ptr } => {
2972                ptr.read().runtime.vacuum_lock.unlock();
2973            }
2974        }
2975    }
2976}
2977
2978#[derive(Clone, Debug)]
2979/// To manage and ensure that no locks are leaked during checkpointing in
2980/// the case of errors. It is held by the WalFile while checkpoint is ongoing
2981/// then transferred to the CheckpointResult if necessary.
2982enum CheckpointLocks {
2983    Writer {
2984        coordination: Arc<dyn WalCoordination>,
2985    },
2986    Read0 {
2987        coordination: Arc<dyn WalCoordination>,
2988    },
2989}
2990
2991/// CheckpointLockSource says whether the checkpoint state machine should acquire checkpoint_lock
2992/// itself or consume checkpoint_lock already held by the caller.
2993/// Most of the time, the default `Acquire` is used, except for VACUUM.
2994#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2995pub(crate) enum CheckpointLockSource {
2996    #[default]
2997    Acquire,
2998    HeldByCaller,
2999}
3000
3001/// Database checkpointers takes the following locks, in order:
3002/// The exclusive CHECKPOINTER lock.
3003/// The exclusive WRITER lock (FULL, RESTART and TRUNCATE only).
3004/// Exclusive lock on read-mark slots 1-N. These are immediately released after being taken.
3005/// Exclusive lock on read-mark 0.
3006/// Exclusive lock on read-mark slots 1-N again. These are immediately released after being taken (RESTART and TRUNCATE only).
3007/// All of the above use blocking locks.
3008impl CheckpointLocks {
3009    fn new(coordination: Arc<dyn WalCoordination>, mode: CheckpointMode) -> Result<Self> {
3010        let guard = coordination.acquire_checkpoint_guard(mode)?;
3011        Ok(match guard {
3012            CoordinationCheckpointGuardKind::Read0 => Self::Read0 { coordination },
3013            CoordinationCheckpointGuardKind::Writer => Self::Writer { coordination },
3014        })
3015    }
3016
3017    /// Build checkpoint ownership from a checkpoint_lock that VACUUM already
3018    /// holds. This consumes that raw lock ownership: on success the
3019    /// returned guard owns checkpoint/read0/write as appropriate, and on error
3020    /// the coordination backend releases the held checkpoint lock before
3021    /// returning.
3022    fn from_held_vacuum_checkpoint_lock(coordination: Arc<dyn WalCoordination>) -> Result<Self> {
3023        let guard = coordination.acquire_vacuum_checkpoint_guard_from_held_lock()?;
3024        Ok(match guard {
3025            CoordinationCheckpointGuardKind::Read0 => Self::Read0 { coordination },
3026            CoordinationCheckpointGuardKind::Writer => Self::Writer { coordination },
3027        })
3028    }
3029}
3030
3031impl Drop for CheckpointLocks {
3032    fn drop(&mut self) {
3033        match self {
3034            CheckpointLocks::Writer { coordination } => {
3035                coordination.release_checkpoint_guard(CoordinationCheckpointGuardKind::Writer);
3036            }
3037            CheckpointLocks::Read0 { coordination } => {
3038                coordination.release_checkpoint_guard(CoordinationCheckpointGuardKind::Read0);
3039            }
3040        }
3041    }
3042}
3043
3044/// Result of try_begin_read_tx - either success or a retriable condition.
3045enum TryBeginReadResult {
3046    /// Successfully started read transaction, returns whether DB changed
3047    Ok(bool),
3048    /// Transient condition, caller should retry immediately (like SQLite's WAL_RETRY)
3049    Retry,
3050    /// Non-retriable failure while preparing the local WAL view.
3051    Err(LimboError),
3052    /// We could get a lock / source snapshot for readers because WAL is exclusively held by
3053    /// other transaction.
3054    /// This usually happens during VACUUM when it holds the vacuum lock exclusively.
3055    /// Retrying will not help until VACUUM releases; caller should surface Busy
3056    /// to the client rather than spin.
3057    Busy,
3058}
3059
3060impl WalFile {
3061    /// Load the authoritative WAL snapshot through the coordination backend.
3062    fn load_coordination_snapshot(&self) -> WalSnapshot {
3063        self.coordination.load_snapshot()
3064    }
3065
3066    /// Reconstruct the connection-local WAL state stored on this `WalFile`.
3067    fn connection_state(&self) -> WalConnectionState {
3068        WalConnectionState::new(
3069            WalSnapshot {
3070                max_frame: self.max_frame.load(Ordering::Acquire),
3071                nbackfills: self.min_frame.load(Ordering::Acquire).saturating_sub(1),
3072                last_checksum: *self.last_checksum.read(),
3073                checkpoint_seq: self.checkpoint_seq.load(Ordering::Acquire),
3074                transaction_count: self.transaction_count.load(Ordering::Acquire),
3075            },
3076            ReadGuardKind::from_lock_index(self.max_frame_read_lock_index.load(Ordering::Acquire)),
3077        )
3078    }
3079
3080    /// Persist a connection-local WAL snapshot bundle back into the legacy fields on `WalFile`.
3081    fn install_connection_state(&self, state: WalConnectionState) {
3082        self.max_frame
3083            .store(state.snapshot.max_frame, Ordering::Release);
3084        self.min_frame
3085            .store(state.snapshot.min_frame(), Ordering::Release);
3086        *self.last_checksum.write() = state.snapshot.last_checksum;
3087        self.checkpoint_seq
3088            .store(state.snapshot.checkpoint_seq, Ordering::Release);
3089        self.transaction_count
3090            .store(state.snapshot.transaction_count, Ordering::Release);
3091        self.max_frame_read_lock_index
3092            .store(state.read_guard.lock_index(), Ordering::Release);
3093    }
3094
3095    /// Compare a freshly loaded shared snapshot against the connection's current snapshot.
3096    fn db_changed_against(&self, snapshot: WalSnapshot, local_state: WalConnectionState) -> bool {
3097        snapshot != local_state.snapshot
3098    }
3099
3100    fn has_vacuum_read_lock_guard(&self) -> bool {
3101        self.vacuum_lock_guard
3102            .read()
3103            .as_ref()
3104            .is_some_and(VacuumLockGuard::is_read)
3105    }
3106
3107    // VACUUM lock guard lifecycle:
3108    // - Normal readers install a read guard in `try_begin_read_tx` after the
3109    //   read-mark slot is selected; `end_read_tx` releases that guard through
3110    //   `release_vacuum_read_lock_guard`.
3111    // - Normal writers do not install their own VACUUM guard. They are an
3112    //   upgrade of an existing read transaction, so their guard is still the
3113    //   read guard owned by the read transaction.
3114    // - In-place VACUUM installs a write guard and takes the WAL write lock in
3115    //   `begin_vacuum_blocking_tx`. `end_write_tx` releases the WAL write lock, and
3116    //   `release_vacuum_lock` releases the write guard.
3117    fn install_vacuum_lock_guard(&self, guard: VacuumLockGuard) {
3118        let mut slot = self.vacuum_lock_guard.write();
3119        turso_assert!(slot.is_none(), "VACUUM lock guard is already installed");
3120        *slot = Some(guard);
3121    }
3122
3123    fn release_vacuum_read_lock_guard(&self) {
3124        let guard = {
3125            let mut slot = self.vacuum_lock_guard.write();
3126            turso_assert!(
3127                slot.as_ref().is_some_and(VacuumLockGuard::is_read),
3128                "VACUUM read lock guard is not held"
3129            );
3130            slot.take()
3131                .expect("VACUUM read lock guard should be present after kind check")
3132        };
3133        drop(guard);
3134    }
3135
3136    fn release_vacuum_write_lock_guard(&self) {
3137        let guard = {
3138            let mut slot = self.vacuum_lock_guard.write();
3139            turso_assert!(
3140                slot.as_ref().is_some_and(VacuumLockGuard::is_write),
3141                "VACUUM write lock guard is not held"
3142            );
3143            slot.take()
3144                .expect("VACUUM write lock guard should be present after kind check")
3145        };
3146        drop(guard);
3147    }
3148
3149    /// Try to begin a read transaction. Returns Retry for transient conditions
3150    /// that should be retried immediately, Ok for success.
3151    fn try_begin_read_tx(&self) -> TryBeginReadResult {
3152        turso_assert!(
3153            self.max_frame_read_lock_index
3154                .load(Ordering::Acquire)
3155                .eq(&NO_LOCK_HELD),
3156            "cannot start a new read tx without ending an existing one",
3157            { "lock_value": self.max_frame_read_lock_index.load(Ordering::Acquire), "expected": NO_LOCK_HELD }
3158        );
3159        turso_assert!(
3160            self.vacuum_lock_guard.read().is_none(),
3161            "VACUUM lock guard already held"
3162        );
3163
3164        // Before we can start the txn, we must first take read lock on the vacuum. If we cannot,
3165        // then vacuum is already in progress. Once we acquire a read lock, this would prevent
3166        // vacuum to run till the lock is released.
3167        let Some(vacuum_lock_guard) =
3168            VacuumLockGuard::try_read(self.coordination.shared_wal_state())
3169        else {
3170            tracing::debug!("begin_read_tx: VACUUM holds the vacuum lock, returning Busy");
3171            return TryBeginReadResult::Busy;
3172        };
3173
3174        // Snapshot the shared WAL state. We haven't taken a read lock yet, so we need
3175        // to validate these values later.
3176        let shared_snapshot = self.load_coordination_snapshot();
3177        turso_assert!(
3178            shared_snapshot.nbackfills <= shared_snapshot.max_frame,
3179            "WAL snapshot cannot have backfills beyond max frame",
3180            {
3181                "nbackfills": shared_snapshot.nbackfills,
3182                "max_frame": shared_snapshot.max_frame,
3183                "checkpoint_seq": shared_snapshot.checkpoint_seq
3184            }
3185        );
3186        tracing::debug!(
3187            "try_begin_read_tx: shared_max={}, nbackfills={}, last_checksum={:?}, checkpoint_seq={:?}, transaction_count={}",
3188            shared_snapshot.max_frame,
3189            shared_snapshot.nbackfills,
3190            shared_snapshot.last_checksum,
3191            shared_snapshot.checkpoint_seq,
3192            shared_snapshot.transaction_count
3193        );
3194        if let Err(err) = self
3195            .coordination
3196            .ensure_local_frame_cache_covers(&self.io, shared_snapshot)
3197        {
3198            return match err {
3199                LimboError::Busy => TryBeginReadResult::Retry,
3200                other => TryBeginReadResult::Err(other),
3201            };
3202        }
3203
3204        // Check if database changed since this connection's last read transaction.
3205        // If it has, the connection will invalidate its page cache.
3206        let db_changed = self.db_changed_against(shared_snapshot, self.connection_state());
3207
3208        tracing::debug!("try_begin_read_tx: db_changed={}", db_changed);
3209
3210        // If WAL is fully checkpointed (shared_max == nbackfills), readers can ignore
3211        // the WAL and read directly from the DB file by holding read_locks[0].
3212        if shared_snapshot.max_frame == shared_snapshot.nbackfills {
3213            tracing::debug!(
3214                "begin_read_tx: WAL fully checkpointed, shared_max={}, nbackfills={}",
3215                shared_snapshot.max_frame,
3216                shared_snapshot.nbackfills
3217            );
3218        }
3219
3220        let Some(read_guard) = self.coordination.try_begin_read_tx(shared_snapshot) else {
3221            return TryBeginReadResult::Retry;
3222        };
3223        self.install_vacuum_lock_guard(vacuum_lock_guard);
3224        self.install_connection_state(WalConnectionState::new(shared_snapshot, read_guard));
3225        tracing::debug!(
3226            "begin_read_tx(min={}, max={}, slot={}, max_frame_in_wal={})",
3227            self.min_frame.load(Ordering::Acquire),
3228            self.max_frame.load(Ordering::Acquire),
3229            read_guard.lock_index(),
3230            shared_snapshot.max_frame
3231        );
3232        TryBeginReadResult::Ok(db_changed)
3233    }
3234}
3235
3236impl Wal for WalFile {
3237    fn begin_read_tx(&self) -> Result<bool> {
3238        // Implement progressive backoff because transient lock contention
3239        // should resolve quickly, but under heavy contention busy-spinning wastes
3240        // CPU. SQLite uses quadratic backoff after 5 retries, with total delay
3241        // up to ~10 seconds before giving up, so we just mirror SQLite's implementation
3242        // here.
3243        let mut cnt = 0u32;
3244        loop {
3245            tracing::trace!("begin_read_tx: cnt={cnt}");
3246            match self.try_begin_read_tx() {
3247                TryBeginReadResult::Ok(changed) => return Ok(changed),
3248                TryBeginReadResult::Err(err) => return Err(err),
3249                TryBeginReadResult::Busy => return Err(LimboError::Busy),
3250                TryBeginReadResult::Retry => {
3251                    cnt += 1;
3252                    if cnt > 100 {
3253                        return Err(LimboError::Busy);
3254                    }
3255                    // Progressive backoff: first 5 retries are immediate, then we
3256                    // start yielding/sleeping with increasing delays.
3257                    if cnt > 5 {
3258                        if cnt < 10 {
3259                            // Retries 6-9: yield to scheduler (minimal delay)
3260                            self.io.yield_now();
3261                        } else {
3262                            // Retries 10+: quadratic backoff in microseconds
3263                            // Formula matches SQLite: (cnt-9)^2 * 39 microseconds
3264                            let delay_us = ((cnt - 9) * (cnt - 9) * 39) as u64;
3265                            self.io.sleep(std::time::Duration::from_micros(delay_us));
3266                        }
3267                    }
3268                    continue;
3269                }
3270            }
3271        }
3272    }
3273
3274    fn mvcc_refresh_if_db_changed(&self) -> bool {
3275        WalFile::mvcc_refresh_if_db_changed(self)
3276    }
3277
3278    /// End a read transaction.
3279    #[inline(always)]
3280    #[instrument(skip_all, level = Level::DEBUG)]
3281    fn end_read_tx(&self) {
3282        let slot = self.max_frame_read_lock_index.load(Ordering::Acquire);
3283        if slot != NO_LOCK_HELD {
3284            self.coordination
3285                .end_read_tx(ReadGuardKind::from_lock_index(slot));
3286            self.max_frame_read_lock_index
3287                .store(NO_LOCK_HELD, Ordering::Release);
3288            self.release_vacuum_read_lock_guard();
3289            tracing::debug!("end_read_tx(slot={slot})");
3290        } else {
3291            // if NO_LOCK_HELD, then we must not have vacuum lock either.
3292            turso_assert!(
3293                !self.has_vacuum_read_lock_guard(),
3294                "vacuum read lock guard held without setting lock slot NO_LOCK_HELD"
3295            );
3296            tracing::debug!("end_read_tx(slot=no_lock)");
3297        }
3298    }
3299
3300    /// Begin a write transaction
3301    #[instrument(skip_all, level = Level::DEBUG)]
3302    fn begin_write_tx(&self, allowed_auto_actions: WalAutoActions) -> Result<()> {
3303        tracing::debug!("begin_write_tx");
3304        let begin_write_result: Result<()> = {
3305            // sqlite/src/wal.c 3702
3306            // Cannot start a write transaction without first holding a read
3307            // transaction.
3308            // assert(pWal->readLock >= 0);
3309            // assert(pWal->writeLock == 0 && pWal->iReCksum == 0);
3310            turso_assert!(
3311                self.max_frame_read_lock_index.load(Ordering::Acquire) != NO_LOCK_HELD,
3312                "must have a read transaction to begin a write transaction"
3313            );
3314            turso_assert!(
3315                !self.holds_write_lock(),
3316                "write lock already held by this connection"
3317            );
3318            if !self.coordination.try_begin_write_tx() {
3319                return Err(LimboError::Busy);
3320            }
3321            let db_changed =
3322                self.db_changed_against(self.load_coordination_snapshot(), self.connection_state());
3323            if db_changed {
3324                // Snapshot is stale, give up and let caller retry from scratch.
3325                // Return BusySnapshot instead of Busy so the caller knows it must
3326                // restart the read transaction to get a fresh snapshot.
3327                // Retrying with busy_timeout will NEVER HELP.
3328                tracing::debug!(
3329                    "unable to upgrade transaction from read to write: snapshot is stale, give up and let caller retry from scratch, self.max_frame={}, shared_max={}",
3330                    self.max_frame.load(Ordering::Acquire),
3331                    self.load_coordination_snapshot().max_frame
3332                );
3333                self.coordination.end_write_tx();
3334                return Err(LimboError::BusySnapshot);
3335            }
3336
3337            Ok(())
3338        };
3339        begin_write_result?;
3340        if self
3341            .write_lock_held
3342            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
3343            .is_err()
3344        {
3345            self.coordination.end_write_tx();
3346            turso_assert!(
3347                false,
3348                "begin_write_tx called while write lock already held according to connection state"
3349            );
3350        }
3351
3352        if !allowed_auto_actions.contains(WalAutoActions::Restart) {
3353            return Ok(());
3354        }
3355
3356        let result = self.try_restart_log_before_write();
3357        if let Err(LimboError::Busy) | Ok(()) = &result {
3358            // it's fine if we were unable to restart WAL file due to Busy errors
3359            return Ok(());
3360        }
3361
3362        // don't forget to release the write-lock if
3363        self.coordination.end_write_tx();
3364        turso_assert!(
3365            self.write_lock_held
3366                .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
3367                .is_ok(),
3368            "end_write_tx called while write lock not held according to connection state"
3369        );
3370
3371        Err(result.expect_err("Ok case handled above"))
3372    }
3373
3374    /// End a write transaction
3375    #[instrument(skip_all, level = Level::DEBUG)]
3376    fn end_write_tx(&self) {
3377        turso_assert!(
3378            self.write_lock_held
3379                .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
3380                .is_ok(),
3381            "end_write_tx called while write lock not held according to connection state"
3382        );
3383        self.coordination.end_write_tx();
3384    }
3385
3386    /// Returns true if this WAL instance currently holds a read lock.
3387    fn holds_read_lock(&self) -> bool {
3388        self.max_frame_read_lock_index.load(Ordering::Acquire) != NO_LOCK_HELD
3389    }
3390
3391    /// Returns true if this WAL instance currently holds the write lock.
3392    fn holds_write_lock(&self) -> bool {
3393        self.write_lock_held.load(Ordering::Acquire)
3394    }
3395
3396    fn should_checkpoint_on_close(&self) -> bool {
3397        self.coordination.should_checkpoint_on_close()
3398    }
3399
3400    /// Find the latest frame containing a page.
3401    #[instrument(skip_all, level = Level::DEBUG)]
3402    #[aristo::intent(
3403        "find_frame never reads outside the live frame range [nbackfills, max_frame]\n",
3404        id = "aristos:wal_find_frame_range_invariant",
3405        verify = "full",
3406        parent = "wal_protocol_correctness"
3407    )]
3408    fn find_frame(&self, page_id: u64, frame_watermark: Option<u64>) -> Result<Option<u64>> {
3409        #[cfg(not(clt_turso_feature = "conn_raw_api"))]
3410        turso_assert!(
3411            frame_watermark.is_none(),
3412            "unexpected use of frame_watermark optional argument"
3413        );
3414
3415        turso_assert!(
3416            frame_watermark.unwrap_or(0) <= self.max_frame.load(Ordering::Acquire),
3417            "frame_watermark must be <= than current WAL max_frame value"
3418        );
3419
3420        // we can guarantee correctness of the method, only if frame_watermark is strictly after the current checkpointed prefix
3421        //
3422        // if it's not, than pages from WAL range [frame_watermark..nBackfill] are already in the DB file,
3423        // and in case if page first occurrence in WAL was after frame_watermark - we will be unable to read proper previous version of the page
3424        let nbackfills = self.load_coordination_snapshot().nbackfills;
3425        turso_assert!(
3426            frame_watermark.is_none() || frame_watermark.unwrap() >= nbackfills,
3427            "frame_watermark must be >= than current WAL backfill amount",
3428            { "frame_watermark": frame_watermark, "nbackfills": nbackfills }
3429        );
3430
3431        // if we are holding read_lock 0 and didn't write anything to the WAL, skip and read right from db file.
3432        //
3433        // note, that max_frame_read_lock_index is set to 0 only when shared_max_frame == nbackfill in which case
3434        // min_frame is set to nbackfill + 1 and max_frame is set to shared_max_frame
3435        //
3436        // by default, SQLite tries to restart log file in this case - but for now let's keep it simple in the turso-db
3437        if self.max_frame_read_lock_index.load(Ordering::Acquire) == 0
3438            && self.max_frame.load(Ordering::Acquire) < self.min_frame.load(Ordering::Acquire)
3439        {
3440            tracing::debug!(
3441                "find_frame(page_id={}, frame_watermark={:?}): max_frame is 0 - read from DB file",
3442                page_id,
3443                frame_watermark,
3444            );
3445            return Ok(None);
3446        }
3447        let min_frame = self.min_frame.load(Ordering::Acquire);
3448        let max_frame = self.max_frame.load(Ordering::Acquire);
3449        self.coordination.ensure_local_frame_cache_covers(
3450            &self.io,
3451            WalSnapshot {
3452                max_frame,
3453                nbackfills: self.min_frame.load(Ordering::Acquire).saturating_sub(1),
3454                last_checksum: *self.last_checksum.read(),
3455                checkpoint_seq: self.coordination.wal_header().checkpoint_seq,
3456                transaction_count: self.transaction_count.load(Ordering::Acquire),
3457            },
3458        )?;
3459        tracing::debug!(
3460            "find_frame(page_id={}, frame_watermark={:?}): min_frame={}, max_frame={}",
3461            page_id,
3462            frame_watermark,
3463            min_frame,
3464            max_frame
3465        );
3466        let frame = self
3467            .coordination
3468            .find_frame(page_id, min_frame, max_frame, frame_watermark);
3469        if let Some(frame) = frame {
3470            tracing::debug!(
3471                "find_frame(page_id={}, frame_watermark={:?}): found frame={}",
3472                page_id,
3473                frame_watermark,
3474                frame
3475            );
3476        }
3477        Ok(frame)
3478    }
3479
3480    /// Read a frame from the WAL.
3481    #[instrument(skip_all, level = Level::DEBUG)]
3482    fn read_frame(
3483        &self,
3484        frame_id: u64,
3485        page: PageRef,
3486        buffer_pool: Arc<BufferPool>,
3487    ) -> Result<Completion> {
3488        tracing::debug!(
3489            "read_frame(page_idx = {}, frame_id = {})",
3490            page.get().id,
3491            frame_id
3492        );
3493        let offset = self.frame_offset(frame_id);
3494        page.set_locked();
3495        let frame = page.clone();
3496        let page_idx = page.get().id;
3497        let epoch_at_issue = self.coordination.checkpoint_epoch();
3498        let complete = Box::new(move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
3499            let Ok((buf, bytes_read)) = res else {
3500                tracing::debug!(err = ?res.unwrap_err());
3501                page.clear_locked();
3502                page.clear_wal_tag();
3503                return None; // IO error already captured in completion
3504            };
3505            let buf_len = buf.len();
3506            if bytes_read != buf_len as i32 {
3507                tracing::debug!(
3508                    "WAL short read at offset {offset}, page {page_idx}, frame_id={frame_id}: expected {buf_len} bytes, got {bytes_read}"
3509                );
3510                page.clear_locked();
3511                page.clear_wal_tag();
3512                return Some(CompletionError::ShortReadWalFrame {
3513                    offset,
3514                    expected: buf_len,
3515                    actual: bytes_read as usize,
3516                });
3517            }
3518            let cloned = frame.clone();
3519            finish_read_page(page.get().id, buf, cloned);
3520            frame.set_wal_tag(frame_id, epoch_at_issue);
3521            None
3522        });
3523        // important not to hold shared state locks beyond this point to avoid deadlock with
3524        // completions that re-enter WAL state while a writer is waiting.
3525        let file = self.coordination.wal_file()?;
3526        begin_read_wal_frame(
3527            file.as_ref(),
3528            offset + WAL_FRAME_HEADER_SIZE as u64,
3529            buffer_pool,
3530            complete,
3531            page_idx,
3532            &self.io_ctx.read(),
3533        )
3534    }
3535
3536    #[instrument(skip_all, level = Level::DEBUG)]
3537    fn read_frames_batch(
3538        &self,
3539        start_frame: u64,
3540        pages: &[PageRef],
3541        buffer_pool: Arc<BufferPool>,
3542        scratch_buf: Option<Arc<Buffer>>,
3543    ) -> Result<Completion> {
3544        turso_assert!(
3545            !pages.is_empty(),
3546            "read_frames_batch requires at least one page"
3547        );
3548        let page_size = self.page_size() as usize;
3549        turso_assert!(page_size > 0, "WAL page size must be initialized");
3550        let frame_size = WAL_FRAME_HEADER_SIZE + page_size;
3551        let count = pages.len();
3552        let total = frame_size * count;
3553        let offset = self.frame_offset(start_frame);
3554        if let Some(buf) = &scratch_buf {
3555            turso_assert!(
3556                buf.len() == total,
3557                "read_frames_batch scratch_buf size must match expected pread length",
3558                { "buf_len": buf.len(), "expected": total }
3559            );
3560        }
3561
3562        // Lock each target page and pre-allocate its destination buffer so the
3563        // completion callback only parses headers, decrypts/verifies, and copies.
3564        let mut slots: Vec<(PageRef, Arc<Buffer>)> = Vec::with_capacity(count);
3565        for page in pages.iter() {
3566            #[cfg(debug_assertions)]
3567            {
3568                turso_assert!(
3569                    !page.is_locked(), "read_frames_batch target page must not already be locked",
3570                    { "page_id": page.get().id }
3571                );
3572                turso_assert!(
3573                    !page.is_loaded(), "read_frames_batch target page must be an unloaded scratch page",
3574                    { "page_id": page.get().id }
3575                );
3576                turso_assert!(
3577                    page.get().buffer.is_none(),
3578                    "read_frames_batch target page must not already retain a buffer",
3579                    { "page_id": page.get().id }
3580                );
3581            }
3582            page.set_locked();
3583            slots.push((page.clone(), Arc::new(buffer_pool.get_page())));
3584        }
3585
3586        let epoch = self.coordination.checkpoint_epoch();
3587        let enc_or_csum = self.io_ctx.read().encryption_or_checksum().clone();
3588        let raw_buf = scratch_buf.unwrap_or_else(|| Arc::new(Buffer::new_temporary(total)));
3589
3590        let complete = Box::new(move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
3591            let clear_slots_on_err = |slots: &[(PageRef, Arc<Buffer>)]| {
3592                for (page, _) in slots {
3593                    page.clear_locked();
3594                    page.clear_wal_tag();
3595                }
3596            };
3597
3598            let Ok((buf, bytes_read)) = res else {
3599                tracing::debug!(err = ?res.unwrap_err());
3600                clear_slots_on_err(&slots);
3601                return None;
3602            };
3603            if bytes_read != total as i32 {
3604                tracing::debug!(
3605                    "short read on WAL batch at offset {offset}: expected {total} bytes, got {bytes_read}"
3606                );
3607                clear_slots_on_err(&slots);
3608                return Some(CompletionError::ShortReadWalFrame {
3609                    offset,
3610                    expected: total,
3611                    actual: bytes_read as usize,
3612                });
3613            }
3614            let raw = buf.as_slice();
3615            for (i, (page, page_buf)) in slots.iter().enumerate() {
3616                let frame_start = i * frame_size;
3617                let frame = &raw[frame_start..frame_start + frame_size];
3618                let (header, page_body) = sqlite3_ondisk::parse_wal_frame_header(frame);
3619                let expected_page_id = page.get().id;
3620                if header.page_number as usize != expected_page_id {
3621                    mark_unlikely();
3622                    tracing::error!(
3623                        frame_id = start_frame + i as u64,
3624                        expected = expected_page_id,
3625                        got = header.page_number,
3626                        "WAL batch frame page_no mismatch"
3627                    );
3628                    clear_slots_on_err(&slots);
3629                    return Some(CompletionError::WalFramePageMismatch {
3630                        frame_id: start_frame + i as u64,
3631                        expected: expected_page_id,
3632                        actual: header.page_number,
3633                    });
3634                }
3635
3636                let body_slice = page_buf.as_mut_slice();
3637                turso_assert!(
3638                    body_slice.len() == page_size,
3639                    "read_frames_batch buffer size must match WAL page size",
3640                    { "buffer_len": body_slice.len(), "page_size": page_size }
3641                );
3642                body_slice.copy_from_slice(page_body);
3643
3644                match &enc_or_csum {
3645                    EncryptionOrChecksum::Encryption(ctx) => {
3646                        match ctx.decrypt_page(body_slice, expected_page_id) {
3647                            Ok(decrypted) => body_slice.copy_from_slice(&decrypted),
3648                            Err(e) => {
3649                                mark_unlikely();
3650                                tracing::error!(
3651                                    "Failed to decrypt WAL batch frame for page_idx={expected_page_id}: {e}"
3652                                );
3653                                clear_slots_on_err(&slots);
3654                                return Some(CompletionError::DecryptionError {
3655                                    page_idx: expected_page_id,
3656                                });
3657                            }
3658                        }
3659                    }
3660                    EncryptionOrChecksum::Checksum(ctx) => {
3661                        if let Err(e) = ctx.verify_checksum(body_slice, expected_page_id) {
3662                            mark_unlikely();
3663                            tracing::error!(
3664                                "Failed to verify checksum for page_id={expected_page_id}: {e}"
3665                            );
3666                            clear_slots_on_err(&slots);
3667                            return Some(e);
3668                        }
3669                    }
3670                    EncryptionOrChecksum::None => {}
3671                }
3672            }
3673
3674            for (i, (page, page_buf)) in slots.iter().enumerate() {
3675                let page_id = page.get().id;
3676                finish_read_page(page_id, page_buf.clone(), page.clone());
3677                page.set_wal_tag(start_frame + i as u64, epoch);
3678            }
3679            None
3680        });
3681
3682        let c = Completion::new_read(raw_buf, complete);
3683        let file = self.coordination.wal_file()?;
3684        file.pread(offset, c)
3685    }
3686
3687    #[instrument(skip_all, level = Level::DEBUG)]
3688    // todo(sivukhin): change API to accept Buffer or some other owned type
3689    // this method involves IO and cross "async" boundary - so juggling with references is bad and dangerous
3690    fn read_frame_raw(&self, frame_id: u64, frame: &mut [u8]) -> Result<Completion> {
3691        tracing::debug!("read_frame_raw({})", frame_id);
3692        let offset = self.frame_offset(frame_id);
3693
3694        // HACK: *mut u8 can't be Sent between threads safely, cast it to usize then
3695        // for the time of writing this comment - this is *safe* as all callers immediately call synchronous method wait_for_completion and hold necessary references
3696        let (frame_ptr, frame_len) = (frame.as_mut_ptr() as usize, frame.len());
3697
3698        let encryption_ctx = {
3699            let io_ctx = self.io_ctx.read();
3700            io_ctx.encryption_context().cloned()
3701        };
3702        let complete = Box::new(move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
3703            let Ok((buf, bytes_read)) = res else {
3704                return None; // IO error already captured in completion
3705            };
3706            let buf_len = buf.len();
3707            if bytes_read != buf_len as i32 {
3708                tracing::debug!(
3709                    "short read on WAL frame {frame_id} at offset {offset}: expected {buf_len} bytes, got {bytes_read}"
3710                );
3711                return Some(CompletionError::ShortReadWalFrame {
3712                    offset,
3713                    expected: buf_len,
3714                    actual: bytes_read as usize,
3715                });
3716            }
3717            let buf_ptr = buf.as_ptr();
3718            let frame_ptr = frame_ptr as *mut u8;
3719            let frame_ref: &mut [u8] =
3720                unsafe { std::slice::from_raw_parts_mut(frame_ptr, frame_len) };
3721
3722            // Copy the just-read WAL frame into the destination buffer
3723            unsafe {
3724                std::ptr::copy_nonoverlapping(buf_ptr, frame_ptr, frame_len);
3725            }
3726
3727            // Now parse the header from the freshly-copied data
3728            let (header, raw_page) = sqlite3_ondisk::parse_wal_frame_header(frame_ref);
3729
3730            if let Some(ctx) = encryption_ctx.clone() {
3731                match ctx.decrypt_page(raw_page, header.page_number as usize) {
3732                    Ok(decrypted_data) => {
3733                        turso_assert!(
3734                            (frame_len - WAL_FRAME_HEADER_SIZE) == decrypted_data.len(),
3735                            "frame_len minus header_size does not equal expected decrypted data length",
3736                            { "frame_len_minus_header": frame_len - WAL_FRAME_HEADER_SIZE, "decrypted_data_len": decrypted_data.len() }
3737                        );
3738                        frame_ref[WAL_FRAME_HEADER_SIZE..].copy_from_slice(&decrypted_data);
3739                    }
3740                    Err(_) => {
3741                        tracing::debug!("Failed to decrypt page data for frame_id={frame_id}");
3742                    }
3743                }
3744            }
3745            None
3746        });
3747        let file = self.coordination.wal_file()?;
3748        let c = begin_read_wal_frame_raw(&self.buffer_pool, file.as_ref(), offset, complete)?;
3749        Ok(c)
3750    }
3751
3752    #[instrument(skip_all, level = Level::DEBUG)]
3753    // todo(sivukhin): change API to accept Buffer or some other owned type
3754    // this method involves IO and cross "async" boundary - so juggling with references is bad and dangerous
3755    fn write_frame_raw(
3756        &self,
3757        buffer_pool: Arc<BufferPool>,
3758        frame_id: u64,
3759        page_id: u64,
3760        db_size: u64,
3761        page: &[u8],
3762        sync_type: FileSyncType,
3763    ) -> Result<()> {
3764        let Some(page_size) = PageSize::new(page.len() as u32) else {
3765            bail_corrupt_error!("invalid page size: {}", page.len());
3766        };
3767        self.ensure_header_if_needed(page_size, sync_type)?;
3768        tracing::debug!("write_raw_frame({})", frame_id);
3769        // if page_size wasn't initialized before - we will initialize it during that raw write
3770        if self.page_size() != 0 && page.len() != self.page_size() as usize {
3771            return Err(LimboError::InvalidArgument(format!(
3772                "unexpected page size in frame: got={}, expected={}",
3773                page.len(),
3774                self.page_size(),
3775            )));
3776        }
3777        if frame_id > self.max_frame.load(Ordering::Acquire) + 1 {
3778            // attempt to write frame out of sequential order - error out
3779            return Err(LimboError::InvalidArgument(format!(
3780                "frame_id is beyond next frame in the WAL: frame_id={}, max_frame={}",
3781                frame_id,
3782                self.max_frame.load(Ordering::Acquire)
3783            )));
3784        }
3785        if frame_id <= self.max_frame.load(Ordering::Acquire) {
3786            // just validate if page content from the frame matches frame in the WAL
3787            let offset = self.frame_offset(frame_id);
3788            let conflict = Arc::new(Mutex::new(false));
3789
3790            // HACK: *mut u8 can't be shared between threads safely, cast it to usize then
3791            // for the time of writing this comment - this is *safe* as the function immediately call synchronous method wait_for_completion and hold necessary references
3792            let (page_ptr, page_len) = (page.as_ptr() as usize, page.len());
3793
3794            let complete = Box::new({
3795                let conflict = conflict.clone();
3796                move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
3797                    let Ok((buf, bytes_read)) = res else {
3798                        return None; // IO error already captured in completion
3799                    };
3800                    let buf_len = buf.len();
3801                    if bytes_read != buf_len as i32 {
3802                        tracing::debug!(
3803                            "short read on WAL frame validation at offset {offset}, page_id={page_id}: expected {buf_len} bytes, got {bytes_read}"
3804                        );
3805                        return Some(CompletionError::ShortReadWalFrame {
3806                            offset,
3807                            expected: buf_len,
3808                            actual: bytes_read as usize,
3809                        });
3810                    }
3811                    let page = unsafe { std::slice::from_raw_parts(page_ptr as *mut u8, page_len) };
3812                    if buf.as_slice() != page {
3813                        *conflict.lock() = true;
3814                    }
3815                    None
3816                }
3817            });
3818            let file = self.coordination.wal_file()?;
3819            let c = begin_read_wal_frame(
3820                file.as_ref(),
3821                offset + WAL_FRAME_HEADER_SIZE as u64,
3822                buffer_pool,
3823                complete,
3824                page_id as usize,
3825                &self.io_ctx.read(),
3826            )?;
3827            self.io.wait_for_completion(c)?;
3828            return if *conflict.lock() {
3829                Err(LimboError::Conflict(format!(
3830                    "frame content differs from the WAL: frame_id={frame_id}"
3831                )))
3832            } else {
3833                Ok(())
3834            };
3835        }
3836
3837        // perform actual write
3838        let offset = self.frame_offset(frame_id);
3839        let header = self.coordination.wal_header();
3840        let file = self.coordination.wal_file()?;
3841        let checksums = *self.last_checksum.read();
3842        let (checksums, frame_bytes) = prepare_wal_frame(
3843            &self.buffer_pool,
3844            &header,
3845            checksums,
3846            header.page_size,
3847            page_id as u32,
3848            db_size as u32,
3849            page,
3850        );
3851        let c = Completion::new_write(|_| {});
3852        let c = file.pwrite(offset, frame_bytes, c)?;
3853        self.io.wait_for_completion(c)?;
3854        self.complete_append_frame(page_id, frame_id, checksums);
3855        if db_size > 0 {
3856            self.finish_append_frames_commit()?;
3857        }
3858        Ok(())
3859    }
3860
3861    #[instrument(skip_all, level = Level::DEBUG)]
3862    fn should_checkpoint(&self) -> bool {
3863        let snapshot = self.load_coordination_snapshot();
3864        snapshot.max_frame as usize > self.checkpoint_threshold + snapshot.nbackfills as usize
3865    }
3866
3867    #[instrument(skip_all, level = Level::DEBUG)]
3868    fn checkpoint(
3869        &self,
3870        pager: &Pager,
3871        mode: CheckpointMode,
3872    ) -> Result<IOResult<CheckpointResult>> {
3873        self.checkpoint_inner(pager, mode, CheckpointLockSource::Acquire)
3874            .inspect_err(|e| {
3875                tracing::debug!("WAL checkpoint failed: {e}");
3876                let _ = self.checkpoint_guard.write().take();
3877                self.ongoing_checkpoint.write().state = CheckpointState::Start;
3878            })
3879    }
3880
3881    fn vacuum_checkpoint_with_held_lock(
3882        &self,
3883        pager: &Pager,
3884    ) -> Result<IOResult<CheckpointResult>> {
3885        self.checkpoint_inner(
3886            pager,
3887            CheckpointMode::Truncate {
3888                upper_bound_inclusive: None,
3889            },
3890            CheckpointLockSource::HeldByCaller,
3891        )
3892        .inspect_err(|e| {
3893            tracing::debug!("WAL checkpoint failed: {e}");
3894            let _ = self.checkpoint_guard.write().take();
3895            self.ongoing_checkpoint.write().state = CheckpointState::Start;
3896        })
3897    }
3898
3899    fn install_durable_backfill_proof(
3900        &self,
3901        max_frame: u64,
3902        db_size_pages: u32,
3903        db_header_crc32c: u32,
3904        sync_type: FileSyncType,
3905    ) -> Result<Option<Completion>> {
3906        self.coordination.install_durable_backfill_proof(
3907            max_frame,
3908            db_size_pages,
3909            db_header_crc32c,
3910            sync_type,
3911        )
3912    }
3913
3914    fn publish_backfill(&self, max_frame: u64) {
3915        let snapshot = self.load_coordination_snapshot();
3916        turso_assert!(
3917            (snapshot.nbackfills..=snapshot.max_frame).contains(&max_frame),
3918            "published backfill must stay within the current WAL generation",
3919            {
3920                "publish_backfill": max_frame,
3921                "current_nbackfills": snapshot.nbackfills,
3922                "current_max_frame": snapshot.max_frame
3923            }
3924        );
3925        self.coordination.publish_backfill(max_frame);
3926    }
3927
3928    #[instrument(err, skip_all, level = Level::DEBUG)]
3929    fn sync(&self, sync_type: FileSyncType) -> Result<Completion> {
3930        tracing::debug!("wal_sync");
3931        let syncing = self.syncing.clone();
3932        let dirty = self.dirty.clone();
3933        let completion = Completion::new_sync(move |result| {
3934            tracing::debug!("wal_sync finish");
3935            if let Err(err) = result {
3936                tracing::debug!("wal_sync failed: {err}");
3937            } else {
3938                dirty.store(false, Ordering::Release);
3939            }
3940            syncing.store(false, Ordering::Release);
3941        });
3942        let file = self.coordination.wal_file()?;
3943        self.syncing.store(true, Ordering::Release);
3944        let c = file.sync(completion, sync_type)?;
3945        Ok(c)
3946    }
3947
3948    // Currently used for assertion purposes
3949    fn is_syncing(&self) -> bool {
3950        self.syncing.load(Ordering::Acquire)
3951    }
3952
3953    fn is_dirty(&self) -> bool {
3954        self.dirty.load(Ordering::Acquire)
3955    }
3956
3957    fn get_max_frame_in_wal(&self) -> u64 {
3958        self.load_coordination_snapshot().max_frame
3959    }
3960
3961    fn get_checkpoint_seq(&self) -> u32 {
3962        self.load_coordination_snapshot().checkpoint_seq
3963    }
3964
3965    fn get_max_frame(&self) -> u64 {
3966        self.max_frame.load(Ordering::Acquire)
3967    }
3968
3969    fn connection_wal_pos(&self) -> (u32, u64) {
3970        (
3971            self.checkpoint_seq.load(Ordering::Acquire),
3972            self.max_frame.load(Ordering::Acquire),
3973        )
3974    }
3975
3976    fn min_pinned_read_frame(&self) -> Option<u64> {
3977        self.coordination.min_pinned_read_frame()
3978    }
3979
3980    fn get_min_frame(&self) -> u64 {
3981        self.min_frame.load(Ordering::Acquire)
3982    }
3983
3984    fn backfill_frame(&self) -> u64 {
3985        self.load_coordination_snapshot().nbackfills
3986    }
3987
3988    fn get_last_checksum(&self) -> (u32, u32) {
3989        *self.last_checksum.read()
3990    }
3991    #[instrument(skip_all, level = Level::DEBUG)]
3992
3993    fn rollback(&self, rollback_to: Option<RollbackTo>) {
3994        let is_savepoint = rollback_to.is_some();
3995        let snapshot = self.load_coordination_snapshot();
3996        if let Some(r) = &rollback_to {
3997            // Savepoint WAL positions are captured under the write lock
3998            // (still held here), and no restart can happen while it is
3999            // held: the writer-upgrade restart runs before positions
4000            // materialize, and checkpoint RESTART/TRUNCATE takes the writer
4001            // lock. A cross-generation position is therefore impossible.
4002            // (SQLite must clamp instead — sqlite3WalSavepointUndo resets
4003            // aWalData on an nCkpt mismatch — because it captures at
4004            // write-tx begin but restarts later, at the first frame write.)
4005            turso_assert!(
4006                r.checkpoint_seq == snapshot.checkpoint_seq,
4007                "savepoint WAL position must be from the current WAL generation",
4008                {
4009                    "savepoint_checkpoint_seq": r.checkpoint_seq,
4010                    "authority_checkpoint_seq": snapshot.checkpoint_seq,
4011                    "savepoint_frame": r.frame,
4012                    "authority_max_frame": snapshot.max_frame
4013                }
4014            );
4015            // The committed mark cannot advance while the write lock is
4016            // held, so the position can never be behind it.
4017            turso_assert!(
4018                r.frame >= snapshot.max_frame,
4019                "savepoint WAL position must not be behind the committed high-water mark",
4020                { "savepoint_frame": r.frame, "authority_max_frame": snapshot.max_frame }
4021            );
4022        }
4023        // Restored verbatim, like SQLite's aWalData. A checksum captured at
4024        // frame 0 of a freshly restarted generation predates the new WAL
4025        // header; that is harmless because prepare_frames seeds frame 1
4026        // from the header itself.
4027        let max_frame = rollback_to
4028            .as_ref()
4029            .map(|r| r.frame)
4030            .unwrap_or(snapshot.max_frame);
4031        let last_checksum = rollback_to
4032            .as_ref()
4033            .map(|r| r.checksum)
4034            .unwrap_or(snapshot.last_checksum);
4035        self.coordination.rollback_cache(max_frame);
4036        *self.last_checksum.write() = last_checksum;
4037        self.max_frame.store(max_frame, Ordering::Release);
4038        if !is_savepoint {
4039            self.reset_internal_states();
4040        }
4041    }
4042
4043    fn abort_checkpoint(&self) {
4044        let _ = self.checkpoint_guard.write().take();
4045        self.reset_internal_states();
4046    }
4047
4048    fn try_begin_vacuum_checkpoint_lock(&self) -> Result<()> {
4049        self.with_shared(|shared| {
4050            if !shared.runtime.checkpoint_lock.write() {
4051                return Err(LimboError::Busy);
4052            }
4053            Ok(())
4054        })
4055    }
4056
4057    fn release_vacuum_checkpoint_lock(&self) {
4058        self.with_shared(|shared| {
4059            shared.runtime.checkpoint_lock.unlock();
4060        });
4061    }
4062
4063    fn begin_vacuum_blocking_tx(&self) -> Result<()> {
4064        turso_assert!(
4065            self.max_frame_read_lock_index.load(Ordering::Acquire) == NO_LOCK_HELD,
4066            "begin_vacuum_blocking_tx: must not already hold a read lock"
4067        );
4068        turso_assert!(
4069            !self.holds_write_lock(),
4070            "begin_vacuum_blocking_tx: must not already hold the write lock"
4071        );
4072        turso_assert!(
4073            self.vacuum_lock_guard.read().is_none(),
4074            "VACUUM lock guard already held"
4075        );
4076
4077        let Some(vacuum_lock_guard) =
4078            VacuumLockGuard::try_write(self.coordination.shared_wal_state())
4079        else {
4080            return Err(LimboError::Busy);
4081        };
4082
4083        // This block is purely an invariant check. The exclusive VACUUM lock can be held
4084        // only if we don't have any other active locks.
4085        self.with_shared(|shared| {
4086            for idx in 0..shared.runtime.read_locks.len() {
4087                // iff there are no read locks active, only then we should be able to
4088                // acquire the write lock
4089                turso_assert!(
4090                    shared.runtime.read_locks[idx].write(),
4091                    "begin_vacuum_blocking_tx: read lock held after VACUUM lock acquired",
4092                    { "read_lock_idx": idx }
4093                );
4094                shared.runtime.read_locks[idx].unlock();
4095            }
4096        });
4097
4098        // Install connection state with a fresh snapshot.
4099        let snapshot = self.load_coordination_snapshot();
4100        self.install_connection_state(WalConnectionState::new(snapshot, ReadGuardKind::None));
4101        turso_assert!(
4102            self.with_shared(|shared| shared.runtime.write_lock.write()),
4103            "begin_vacuum_blocking_tx: write lock held after VACUUM lock acquired"
4104        );
4105        if self
4106            .write_lock_held
4107            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
4108            .is_err()
4109        {
4110            turso_assert!(
4111                false,
4112                "begin_vacuum_blocking_tx: write_lock_held already set"
4113            );
4114        }
4115        self.install_vacuum_lock_guard(vacuum_lock_guard);
4116        Ok(())
4117    }
4118
4119    fn release_vacuum_lock(&self) {
4120        // This drops the stop-the-world gate after VACUUM is one.
4121        // Only after this new readers can proceed.
4122        turso_assert!(
4123            !self.holds_write_lock(),
4124            "release_vacuum_lock called while source write lock is still held"
4125        );
4126        self.release_vacuum_write_lock_guard();
4127    }
4128
4129    #[instrument(skip_all, level = Level::DEBUG)]
4130    fn finish_append_frames_commit(&self) -> Result<()> {
4131        let max_frame = self.max_frame.load(Ordering::Acquire);
4132        let last_checksum = *self.last_checksum.read();
4133        tracing::trace!(max_frame, ?last_checksum);
4134        let transaction_count = self.transaction_count.fetch_add(1, Ordering::AcqRel) + 1;
4135        self.coordination.publish_commit(WalCommitState {
4136            max_frame,
4137            last_checksum,
4138            transaction_count,
4139        });
4140        Ok(())
4141    }
4142
4143    fn changed_pages_after(&self, frame_watermark: u64) -> Result<Vec<u32>> {
4144        let frame_count = self.get_max_frame();
4145        let page_size = self.page_size();
4146        let mut frame = vec![0u8; page_size as usize + WAL_FRAME_HEADER_SIZE];
4147        let mut seen = FxHashSet::default();
4148        turso_assert!(
4149            frame_count >= frame_watermark,
4150            "frame_count must be not less than frame_watermark",
4151            { "frame_count": frame_count, "frame_watermark": frame_watermark }
4152        );
4153        let mut pages = Vec::with_capacity((frame_count - frame_watermark) as usize);
4154        for frame_no in frame_watermark + 1..=frame_count {
4155            let c = self.read_frame_raw(frame_no, &mut frame)?;
4156            self.io.wait_for_completion(c)?;
4157            let (header, _) = sqlite3_ondisk::parse_wal_frame_header(&frame);
4158            if seen.insert(header.page_number) {
4159                pages.push(header.page_number);
4160            }
4161        }
4162        Ok(pages)
4163    }
4164
4165    fn prepare_wal_start(&self, page_size: PageSize) -> Result<Option<Completion>> {
4166        if self.coordination.wal_is_initialized() {
4167            return Ok(None);
4168        }
4169        tracing::debug!("ensure_header_if_needed");
4170        let Some(header) = self
4171            .coordination
4172            .prepare_wal_header(self.io.as_ref(), page_size)
4173        else {
4174            return Ok(None);
4175        };
4176        *self.last_checksum.write() = (header.checksum_1, header.checksum_2);
4177
4178        self.max_frame.store(0, Ordering::Release);
4179        let file = self.coordination.wal_file()?;
4180        let header_c = sqlite3_ondisk::begin_write_wal_header(file.as_ref(), &header)?;
4181
4182        // After a RESTART or try_restart_log_before_write the WAL file may
4183        // still contain orphaned frames from the previous epoch. Truncate
4184        // them so that classify_authority_snapshot_against_wal does not see a
4185        // length mismatch and unnecessarily fall back to a full disk scan
4186        // (which can race with concurrent writers and corrupt the authority).
4187        let should_skip_truncate = match file.size() {
4188            Ok(size) => size <= WAL_HEADER_SIZE as u64,
4189            Err(_) => {
4190                tracing::warn!("Failed to get WAL file size");
4191                true
4192            }
4193        };
4194        if !should_skip_truncate {
4195            let trunc_c = file.truncate(
4196                WAL_HEADER_SIZE as u64,
4197                Completion::new_trunc(|res| {
4198                    if let Err(err) = res {
4199                        tracing::warn!("WAL truncate of orphaned frames failed: {err}");
4200                    }
4201                }),
4202            )?;
4203            let mut group = CompletionGroup::new(|_| {});
4204            group.add(&header_c);
4205            group.add(&trunc_c);
4206            Ok(Some(group.build()))
4207        } else {
4208            Ok(Some(header_c))
4209        }
4210    }
4211
4212    #[aristo::intent(
4213        "The WAL initialized flag is set true only after a successful sync of the wal-header\n",
4214        id = "aristos:wal_initialized_reflects_sync_outcome",
4215        verify = "full",
4216        parent = "wal_protocol_correctness"
4217    )]
4218    fn prepare_wal_finish(&self, sync_type: FileSyncType) -> Result<Completion> {
4219        let file = self.coordination.wal_file()?;
4220        let coordination = self.coordination.clone();
4221        let c = file.sync(
4222            Completion::new_sync(move |res| {
4223                // Only mark the WAL header durable once its sync has actually
4224                // succeeded. A failed sync must leave the WAL uninitialized so
4225                // the header is re-issued before the next append, keeping the
4226                // in-memory initialized state consistent with what is on disk.
4227                if res.is_ok() {
4228                    coordination.mark_initialized();
4229                }
4230            }),
4231            sync_type,
4232        )?;
4233        Ok(c)
4234    }
4235
4236    /// Prepares a batch of dirty pages as WAL frames without modifying WAL state.
4237    ///
4238    /// This is the first phase of a three-phase commit protocol:
4239    /// 1. prepare (`prepare_frames`) - serialize frames, compute checksums
4240    /// 2. write + fsync - caller submits I/O and waits for durability
4241    /// 3. commit/finalize (`commit_prepared_frames`) - update WAL index and page metadata
4242    ///
4243    /// WAL frames form a checksum chain for corruption detection. When writing
4244    /// multiple batches in a single transaction, pass the previous batch via `prev`
4245    /// to continue the chain. For the first batch, pass `None` to start from
4246    /// the committed WAL state.
4247    fn prepare_frames(
4248        &self,
4249        pages: &[PageRef],
4250        page_sz: PageSize,
4251        db_size_on_commit: Option<u32>,
4252        prev: Option<&PreparedFrames>,
4253    ) -> Result<PreparedFrames> {
4254        turso_assert!(
4255            !pages.is_empty(),
4256            "prepare_frames requires at least one page"
4257        );
4258        turso_assert!(
4259            pages.len() <= IOV_MAX,
4260            "supported up to IOV_MAX pages at once"
4261        );
4262        turso_assert!(
4263            self.coordination.wal_is_initialized(),
4264            "WAL must be initialized"
4265        );
4266
4267        let header = self.coordination.wal_header();
4268        let epoch = self.coordination.checkpoint_epoch();
4269
4270        turso_assert!(
4271            header.page_size == page_sz.get(),
4272            "page size mismatch between header and requested",
4273            { "header_page_size": header.page_size, "requested_page_size": page_sz.get() }
4274        );
4275
4276        // Either chain from previous batch of PreparedFrames or use committed WAL state.
4277        // For the first batch, also check the authority's max_frame to handle
4278        // cross-process WAL restarts where our local max_frame is stale.
4279        let (mut rolling_checksum, mut next_frame_id) = match prev {
4280            Some(p) => (p.final_checksum, p.final_max_frame + 1),
4281            None => {
4282                let snapshot = self.load_coordination_snapshot();
4283                let local_state = self.connection_state();
4284                if local_state.snapshot.max_frame > snapshot.max_frame {
4285                    // The local position is past the committed high-water
4286                    // mark exactly when this connection has spilled or
4287                    // raw-inserted frames that carry no commit marker yet.
4288                    // Chain from local state so we don't overwrite them.
4289                    (
4290                        local_state.snapshot.last_checksum,
4291                        local_state.snapshot.max_frame + 1,
4292                    )
4293                } else {
4294                    // Inside a write transaction the local position can
4295                    // never be behind the committed mark: the upgrade
4296                    // requires a fresh snapshot, and the mark cannot advance
4297                    // while the write lock is held.
4298                    turso_assert!(
4299                        local_state.snapshot.max_frame == snapshot.max_frame,
4300                        "connection WAL position must not be behind the committed high-water mark",
4301                        {
4302                            "local_max_frame": local_state.snapshot.max_frame,
4303                            "authority_max_frame": snapshot.max_frame
4304                        }
4305                    );
4306                    // At the mark the authority owns the seed; re-sync local
4307                    // state if it drifted (e.g. a savepoint rollback
4308                    // reinstalled a pre-header checksum at frame 0, or a
4309                    // concurrent checkpoint advanced nbackfills).
4310                    if snapshot != local_state.snapshot {
4311                        self.install_connection_state(local_state.with_snapshot(snapshot));
4312                    }
4313                    (snapshot.last_checksum, snapshot.max_frame + 1)
4314                }
4315            }
4316        };
4317
4318        // The first frame of a generation always chains from the WAL header
4319        // checksum, like SQLite's walFrames at mxFrame == 0. Connection and
4320        // authority state may still carry the pre-header checksum here: a
4321        // restart resets the position before the next append writes the new
4322        // header, and a savepoint rollback can reinstall a position captured
4323        // in that window. The wal_is_initialized assert above guarantees
4324        // `header` is the current generation's synced header.
4325        if next_frame_id == 1 {
4326            rolling_checksum = (header.checksum_1, header.checksum_2);
4327        }
4328
4329        let first_frame_id = next_frame_id;
4330
4331        let mut bufs: Vec<Arc<Buffer>> = Vec::with_capacity(pages.len());
4332        let mut metadata = Vec::with_capacity(pages.len());
4333
4334        for (idx, page) in pages.iter().enumerate() {
4335            let page_id = page.get().id;
4336            let plain = page.get_contents().as_ptr();
4337
4338            let data: Cow<[u8]> = {
4339                let io_ctx = self.io_ctx.read();
4340                match io_ctx.encryption_or_checksum() {
4341                    EncryptionOrChecksum::Encryption(ctx) => {
4342                        Cow::Owned(ctx.encrypt_page(plain, page_id)?)
4343                    }
4344                    EncryptionOrChecksum::Checksum(ctx) => {
4345                        ctx.add_checksum_to_page(plain, page_id)?;
4346                        Cow::Borrowed(plain)
4347                    }
4348                    EncryptionOrChecksum::None => Cow::Borrowed(plain),
4349                }
4350            };
4351
4352            // if DB size is included for commit frame, it will need to be included only in the last frame of the batch.
4353            // however it might not be present in this batch so we cannot assert its presence
4354            let frame_db_size = if idx + 1 == pages.len() {
4355                db_size_on_commit.unwrap_or(0)
4356            } else {
4357                0
4358            };
4359            let (checksum, frame_buf) = prepare_wal_frame(
4360                &self.buffer_pool,
4361                &header,
4362                rolling_checksum,
4363                header.page_size,
4364                page_id as u32,
4365                frame_db_size,
4366                &data,
4367            );
4368            bufs.push(frame_buf);
4369            metadata.push((page.clone(), next_frame_id, checksum));
4370            rolling_checksum = checksum;
4371            next_frame_id += 1;
4372        }
4373        let offset = self.frame_offset(first_frame_id);
4374        Ok(PreparedFrames {
4375            offset,
4376            bufs,
4377            metadata,
4378            final_checksum: rolling_checksum,
4379            final_max_frame: next_frame_id - 1,
4380            epoch,
4381        })
4382    }
4383
4384    /// For each prepared frame, update in-memory WAL index and rolling checksum.
4385    /// and advance max_frame to make frames visible to readers.
4386    fn commit_prepared_frames(&self, batches: &[PreparedFrames]) {
4387        for batch in batches {
4388            for (page, frame_id, checksum) in &batch.metadata {
4389                // Update WAL index mapping page -> frame
4390                self.complete_append_frame(page.get().id as u64, *frame_id, *checksum);
4391            }
4392            // Update rolling checksum
4393            *self.last_checksum.write() = batch.final_checksum;
4394            // Advance max_frame and make frames visible to readers
4395            self.max_frame
4396                .store(batch.final_max_frame, Ordering::Release);
4397        }
4398    }
4399
4400    /// Mark pages clean and set WAL tags after durable commit.
4401    fn finalize_committed_pages(&self, prepared: &[PreparedFrames]) {
4402        for batch in prepared {
4403            for (page, frame_id, _) in &batch.metadata {
4404                page.clear_dirty();
4405                page.set_wal_tag(*frame_id, batch.epoch);
4406            }
4407        }
4408    }
4409
4410    /// Get WAL file for durable writes.
4411    fn wal_file(&self) -> Result<Arc<dyn File>> {
4412        self.coordination.wal_file()
4413    }
4414
4415    /// Use pwritev to append many frames to the log at once.
4416    ///
4417    /// # Safety:
4418    /// this method should only be used for cacheflush/spilling,
4419    /// the commit path should use prepare_frames + commit_prepared_frames instead,
4420    /// as it prevents prematurely modifing WAL state before durability is ensured.
4421    fn append_frames_vectored(&self, pages: Vec<PageRef>, page_sz: PageSize) -> Result<Completion> {
4422        turso_assert!(
4423            pages.len() <= IOV_MAX,
4424            "we limit number of iovecs to IOV_MAX"
4425        );
4426        turso_assert!(
4427            self.coordination.wal_is_initialized(),
4428            "WAL must be prepared with prepare_wal_start/prepare_wal_finish method"
4429        );
4430
4431        let header = self.coordination.wal_header();
4432        let shared_page_size = header.page_size;
4433        let epoch = self.coordination.checkpoint_epoch();
4434        turso_assert!(
4435            shared_page_size == page_sz.get(),
4436            "page size mismatch, tried to change page size after WAL header was already initialized",
4437            { "shared_page_size": shared_page_size, "page_size": page_sz.get() }
4438        );
4439
4440        // Prepare write buffers and bookkeeping
4441        let mut iovecs: Vec<Arc<Buffer>> = Vec::with_capacity(pages.len());
4442        let mut page_frame_and_checksum: Vec<(PageRef, u64, (u32, u32))> =
4443            Vec::with_capacity(pages.len());
4444
4445        // Rolling checksum input to each frame build
4446        let mut rolling_checksum: (u32, u32) = *self.last_checksum.read();
4447
4448        let mut next_frame_id = self.max_frame.load(Ordering::Acquire) + 1;
4449        // Build every frame in order, updating the rolling checksum
4450        for page in pages.iter() {
4451            tracing::debug!("append_frames_vectored: page_id={}", page.get().id);
4452            let page_id = page.get().id;
4453            let plain = page.get_contents().as_ptr();
4454
4455            let data_to_write: std::borrow::Cow<[u8]> = {
4456                let io_ctx = self.io_ctx.read();
4457                match &io_ctx.encryption_or_checksum() {
4458                    EncryptionOrChecksum::Encryption(ctx) => {
4459                        Cow::Owned(ctx.encrypt_page(plain, page_id)?)
4460                    }
4461                    EncryptionOrChecksum::Checksum(ctx) => {
4462                        ctx.add_checksum_to_page(plain, page_id)?;
4463                        Cow::Borrowed(plain)
4464                    }
4465                    EncryptionOrChecksum::None => Cow::Borrowed(plain),
4466                }
4467            };
4468
4469            let frame_db_size = 0; // this method is not used for the commit path
4470            let (new_checksum, frame_bytes) = prepare_wal_frame(
4471                &self.buffer_pool,
4472                &header,
4473                rolling_checksum,
4474                shared_page_size,
4475                page_id as u32,
4476                frame_db_size,
4477                &data_to_write,
4478            );
4479            iovecs.push(frame_bytes);
4480
4481            // (page, assigned_frame_id, cumulative_checksum_at_this_frame)
4482            page_frame_and_checksum.push((page.clone(), next_frame_id, new_checksum));
4483
4484            // Advance for the next frame
4485            rolling_checksum = new_checksum;
4486            next_frame_id += 1;
4487        }
4488
4489        let first_frame_id = self.max_frame.load(Ordering::Acquire) + 1;
4490        let start_off = self.frame_offset(first_frame_id);
4491
4492        // single completion for the whole batch
4493        let total_len: i32 = iovecs.iter().map(|b| b.len() as i32).sum();
4494        let page_frame_for_cb = page_frame_and_checksum.clone();
4495        // Make the frames readable only once the write is durable. `find_frame`
4496        // (reads) and `iter_latest_frames` (checkpoint) resolve a page->frame
4497        // only through the frame cache, so populating it here — from the write
4498        // completion callback — is what publishes the frames. Doing it before
4499        // durability would let a reader or a checkpoint pick up a frame whose
4500        // bytes are not on disk yet. On write failure `res` is `Err`, so we
4501        // publish nothing.
4502        let coordination = self.coordination.clone();
4503        let on_complete = move |res: Result<i32, CompletionError>| {
4504            let Ok(bytes_written) = res else {
4505                return;
4506            };
4507            turso_assert!(
4508                bytes_written == total_len,
4509                "pwritev wrote unexpected number of bytes",
4510                { "bytes_written": bytes_written, "expected": total_len }
4511            );
4512
4513            for (page, fid, _csum) in &page_frame_for_cb {
4514                page.set_wal_tag(*fid, epoch);
4515                coordination.cache_frame(page.get().id as u64, *fid);
4516            }
4517        };
4518
4519        let c = Completion::new_write(on_complete);
4520
4521        let file = self.coordination.wal_file()?;
4522        let c = file.pwritev(start_off, iovecs, c)?;
4523
4524        // Advance the connection-private write cursor (max_frame / rolling
4525        // checksum / dirty) synchronously so a following batch in the same
4526        // flush chains onto the correct frame ids and checksum.
4527        //
4528        // These are optimistic in-memory bookkeeping fields, not durable state,
4529        // and they do not make the frame visible (visibility is the frame
4530        // cache, published from the completion callback above only after the
4531        // write succeeds). So advancing them before the write lands is safe:
4532        // if the write fails the transaction unwinds and `rollback()` restores
4533        // max_frame / last_checksum from the committed watermark and drops
4534        // cached frames above it; nothing is durable until a commit frame is
4535        // fsynced, and crash recovery rebuilds max_frame by scanning only
4536        // committed, checksum-valid frames. `dirty` is conservative — it only
4537        // forces an fsync before the next commit is reported durable.
4538        //
4539        // Must NOT block for durability here: the returned completion is awaited
4540        // by the caller's state machine (spill: `SpillState::WritingToWal`;
4541        // cacheflush: the collected completions). A synchronous drain would
4542        // deadlock a caller that drives I/O from a single-threaded event loop.
4543        if let Some((_, last_frame_id, last_checksum)) = page_frame_and_checksum.last() {
4544            self.dirty.store(true, Ordering::Release);
4545            *self.last_checksum.write() = *last_checksum;
4546            self.max_frame.store(*last_frame_id, Ordering::Release);
4547        }
4548
4549        Ok(c)
4550    }
4551
4552    #[cfg(any(clt_turso_tests, debug_assertions))]
4553    fn as_any(&self) -> &dyn std::any::Any {
4554        self
4555    }
4556
4557    fn set_io_context(&self, ctx: IOContext) {
4558        *self.io_ctx.write() = ctx;
4559    }
4560
4561    fn update_max_frame(&self) {
4562        let new_max_frame = self.load_coordination_snapshot().max_frame;
4563        self.max_frame.store(new_max_frame, Ordering::Release);
4564    }
4565
4566    fn truncate_wal(
4567        &self,
4568        result: &mut CheckpointResult,
4569        sync_type: FileSyncType,
4570    ) -> Result<IOResult<()>> {
4571        self.truncate_log(result, sync_type)
4572    }
4573}
4574
4575impl WalFile {
4576    #[cfg(host_shared_wal)]
4577    pub(crate) fn new_with_shared_coordination(
4578        io: Arc<dyn IO>,
4579        shared: Arc<RwLock<WalFileShared>>,
4580        authority: Arc<MappedSharedWalCoordination>,
4581        _last_checksum_and_max_frame: ((u32, u32), u64),
4582        buffer_pool: Arc<BufferPool>,
4583    ) -> Self {
4584        let coordination: Arc<dyn WalCoordination> =
4585            Arc::new(ShmWalCoordination::new(shared, authority));
4586        let snapshot = coordination.load_snapshot();
4587        Self::new_with_coordination(
4588            io,
4589            coordination,
4590            (snapshot.last_checksum, snapshot.max_frame),
4591            buffer_pool,
4592        )
4593    }
4594
4595    pub fn new(
4596        io: Arc<dyn IO>,
4597        shared: Arc<RwLock<WalFileShared>>,
4598        (last_checksum, max_frame): ((u32, u32), u64),
4599        buffer_pool: Arc<BufferPool>,
4600    ) -> Self {
4601        let coordination: Arc<dyn WalCoordination> =
4602            Arc::new(InProcessWalCoordination::new(shared));
4603        Self::new_with_coordination(io, coordination, (last_checksum, max_frame), buffer_pool)
4604    }
4605
4606    /// Construct a WAL using an explicit coordination backend.
4607    fn new_with_coordination(
4608        io: Arc<dyn IO>,
4609        coordination: Arc<dyn WalCoordination>,
4610        (last_checksum, max_frame): ((u32, u32), u64),
4611        buffer_pool: Arc<BufferPool>,
4612    ) -> Self {
4613        let now = io.current_time_monotonic();
4614        Self {
4615            io,
4616            coordination,
4617            // default to max frame in WAL, so that when we read schema we can read from WAL too if it's there.
4618            max_frame: AtomicU64::new(max_frame),
4619            ongoing_checkpoint: RwLock::new(OngoingCheckpoint {
4620                time: now,
4621                pending_writes: WriteBatch::new(),
4622                inflight_writes: Vec::new(),
4623                state: CheckpointState::Start,
4624                min_frame: 0,
4625                max_frame: 0,
4626                current_page: 0,
4627                pages_to_checkpoint: Vec::new(),
4628                inflight_reads: Vec::with_capacity(MAX_INFLIGHT_READS),
4629            }),
4630            checkpoint_threshold: 1000,
4631            buffer_pool,
4632            checkpoint_seq: AtomicU32::new(0),
4633            syncing: Arc::new(AtomicBool::new(false)),
4634            write_lock_held: AtomicBool::new(false),
4635            vacuum_lock_guard: RwLock::new(None),
4636            min_frame: AtomicU64::new(0),
4637            transaction_count: AtomicU64::new(0),
4638            max_frame_read_lock_index: AtomicUsize::new(NO_LOCK_HELD),
4639            last_checksum: RwLock::new(last_checksum),
4640            checkpoint_guard: RwLock::new(None),
4641            io_ctx: RwLock::new(IOContext::default()),
4642            dirty: Arc::new(AtomicBool::new(false)),
4643        }
4644    }
4645
4646    #[cfg(clt_turso_tests)]
4647    pub(crate) fn shared_ptr(&self) -> usize {
4648        self.coordination.shared_ptr()
4649    }
4650
4651    #[cfg(clt_turso_tests)]
4652    pub(crate) fn coordination_backend_name(&self) -> &'static str {
4653        self.coordination.backend_name()
4654    }
4655
4656    #[cfg(clt_turso_tests)]
4657    pub(crate) fn coordination_open_mode_name(&self) -> Option<&'static str> {
4658        self.coordination.open_mode_name()
4659    }
4660
4661    fn with_shared<F, R>(&self, func: F) -> R
4662    where
4663        F: FnOnce(&WalFileShared) -> R,
4664    {
4665        let shared = self.coordination.shared_wal_state();
4666        let guard = shared.read();
4667        func(&guard)
4668    }
4669
4670    fn page_size(&self) -> u32 {
4671        self.coordination.wal_header().page_size
4672    }
4673
4674    fn frame_offset(&self, frame_id: u64) -> u64 {
4675        turso_assert_greater_than!(frame_id, 0, "Frame ID must be 1-based");
4676        let page_offset = (frame_id - 1) * (self.page_size() + WAL_FRAME_HEADER_SIZE as u32) as u64;
4677        WAL_HEADER_SIZE as u64 + page_offset
4678    }
4679
4680    fn increment_checkpoint_epoch(&self) {
4681        let prev = self.coordination.bump_checkpoint_epoch();
4682        tracing::debug!("increment checkpoint epoch: prev={}", prev);
4683    }
4684
4685    fn complete_append_frame(&self, page_id: u64, frame_id: u64, checksums: (u32, u32)) {
4686        self.dirty.store(true, Ordering::Release);
4687        *self.last_checksum.write() = checksums;
4688        self.max_frame.store(frame_id, Ordering::Release);
4689        self.coordination.cache_frame(page_id, frame_id);
4690    }
4691
4692    /// Reset connection-private WAL state.
4693    fn reset_internal_states(&self) {
4694        self.ongoing_checkpoint.write().reset();
4695        self.syncing.store(false, Ordering::Release);
4696    }
4697
4698    /// the WAL file has been truncated and we are writing the first
4699    /// frame since then. We need to ensure that the header is initialized.
4700    fn ensure_header_if_needed(&self, page_size: PageSize, sync_type: FileSyncType) -> Result<()> {
4701        let Some(c) = self.prepare_wal_start(page_size)? else {
4702            return Ok(());
4703        };
4704        self.io.wait_for_completion(c)?;
4705        let c = self.prepare_wal_finish(sync_type)?;
4706        self.io.wait_for_completion(c)?;
4707        Ok(())
4708    }
4709
4710    fn checkpoint_inner(
4711        &self,
4712        pager: &Pager,
4713        mode: CheckpointMode,
4714        lock_source: CheckpointLockSource,
4715    ) -> Result<IOResult<CheckpointResult>> {
4716        loop {
4717            let state = self.ongoing_checkpoint.read().state.clone();
4718            tracing::debug!(?state);
4719            match state {
4720                // Acquire the relevant exclusive locks and checkpoint_lock
4721                // so no other checkpointer can run. fsync WAL if there are unapplied frames.
4722                // Decide the largest frame we are allowed to back‑fill.
4723                CheckpointState::Start => {
4724                    let snapshot = self.load_coordination_snapshot();
4725                    let max_frame = snapshot.max_frame;
4726                    let nbackfills = snapshot.nbackfills;
4727                    tracing::debug!("shared_wal: max_frame={max_frame}, nbackfills={nbackfills}");
4728                    let needs_backfill = max_frame > nbackfills;
4729                    if matches!(lock_source, CheckpointLockSource::HeldByCaller) {
4730                        turso_assert!(
4731                            needs_backfill,
4732                            "held checkpoint-lock path requires WAL frames to backfill",
4733                            { "max_frame": max_frame, "nbackfills": nbackfills }
4734                        );
4735                    }
4736                    if !needs_backfill && !mode.should_restart_log() {
4737                        // there are no frames to copy over and we don't need to reset
4738                        // the log so we can return early success.
4739                        return Ok(IOResult::Done(CheckpointResult::new(
4740                            max_frame, nbackfills, 0,
4741                        )));
4742                    }
4743                    // acquire the appropriate exclusive locks depending on the checkpoint mode
4744                    self.acquire_proper_checkpoint_guard(mode, lock_source)?;
4745                    let mut max_frame = self.determine_max_safe_checkpoint_frame();
4746
4747                    if let CheckpointMode::Truncate {
4748                        upper_bound_inclusive: Some(upper_bound),
4749                    } = mode
4750                    {
4751                        if max_frame > upper_bound {
4752                            tracing::debug!(
4753                                "abort checkpoint because latest frame in WAL is greater than upper_bound in TRUNCATE mode: {max_frame} != {upper_bound}"
4754                            );
4755                            return Err(LimboError::Busy);
4756                        }
4757                    }
4758                    if let CheckpointMode::Passive {
4759                        upper_bound_inclusive: Some(upper_bound),
4760                    } = mode
4761                    {
4762                        max_frame = max_frame.min(upper_bound);
4763                    }
4764
4765                    {
4766                        let mut oc = self.ongoing_checkpoint.write();
4767                        oc.max_frame = max_frame;
4768                        oc.min_frame = nbackfills + 1;
4769                    }
4770                    let (oc_min_frame, oc_max_frame) = {
4771                        let oc = self.ongoing_checkpoint.read();
4772                        (oc.min_frame, oc.max_frame)
4773                    };
4774                    self.coordination.ensure_local_frame_cache_covers(
4775                        &self.io,
4776                        WalSnapshot {
4777                            max_frame: oc_max_frame,
4778                            ..self.load_coordination_snapshot()
4779                        },
4780                    )?;
4781                    tracing::debug!(
4782                        "checkpoint_inner::Start: min_frame={oc_min_frame}, max_frame={oc_max_frame}"
4783                    );
4784                    let mut to_checkpoint = self
4785                        .coordination
4786                        .iter_latest_frames(oc_min_frame, oc_max_frame);
4787                    // sort by frame_id for read locality
4788                    to_checkpoint.sort_unstable_by(|a, b| (a.1, a.0).cmp(&(b.1, b.0)));
4789                    {
4790                        let mut oc = self.ongoing_checkpoint.write();
4791                        oc.pages_to_checkpoint = to_checkpoint;
4792                        oc.current_page = 0;
4793                        oc.inflight_writes.clear();
4794                        oc.inflight_reads.clear();
4795                        oc.state = CheckpointState::Processing;
4796                        oc.time = self.io.current_time_monotonic();
4797                    }
4798                    tracing::trace!(
4799                        "checkpoint_start(min_frame={}, max_frame={})",
4800                        oc_min_frame,
4801                        oc_max_frame,
4802                    );
4803                }
4804                // For locality, reading is ordered by frame ID, and writing ordered by page ID.
4805                // the more consecutive page ID's that we submit together, the fewer overall
4806                // write/writev syscalls made. All I/O during checkpointing is now in a single step
4807                // to prevent serialization, and we try to issue reads and flush batches concurrently
4808                // if at all possible, at the cost of some batching potential.
4809                CheckpointState::Processing => {
4810                    // Gather I/O completions using a completion group
4811                    let mut nr_completions = 0;
4812                    let mut group = CompletionGroup::new(|_| {});
4813                    let mut ongoing_chkpt = self.ongoing_checkpoint.write();
4814
4815                    // Check and clean any completed writes from pending flush
4816                    if ongoing_chkpt.process_inflight_writes() {
4817                        tracing::trace!("Completed a write batch");
4818                    }
4819                    // Process completed reads into current batch
4820                    if ongoing_chkpt.process_pending_reads()? {
4821                        tracing::trace!("Drained reads into batch");
4822                    }
4823                    if let Some(e) = ongoing_chkpt.first_write_error() {
4824                        mark_unlikely();
4825                        // cancel everything still in-flight to avoid leaks
4826                        let to_cancel: Vec<Completion> = ongoing_chkpt
4827                            .inflight_reads
4828                            .iter()
4829                            .map(|r| r.completion.clone())
4830                            .collect();
4831                        pager.io.cancel(&to_cancel)?;
4832                        pager.io.drain_completions(&to_cancel)?;
4833                        return Err(LimboError::CompletionError(e));
4834                    }
4835                    let epoch = self.coordination.checkpoint_epoch();
4836                    // Issue reads until we hit limits
4837                    'inner: while ongoing_chkpt.should_issue_reads() {
4838                        let (page_id, target_frame) = {
4839                            ongoing_chkpt.pages_to_checkpoint[ongoing_chkpt.current_page as usize]
4840                        };
4841                        if let Some(cached_page) =
4842                            pager.cache_get_for_checkpoint(page_id as usize, target_frame, epoch)?
4843                        {
4844                            let buffer = cached_page
4845                                .get_contents()
4846                                .buffer
4847                                .as_ref()
4848                                .expect("buffer missing")
4849                                .clone();
4850                            {
4851                                ongoing_chkpt
4852                                    .pending_writes
4853                                    .insert(page_id as usize, buffer);
4854                                // signify that a cached page was used, so it can be unpinned
4855                                let current = ongoing_chkpt.current_page as usize;
4856                                ongoing_chkpt.pages_to_checkpoint[current] =
4857                                    (page_id, target_frame);
4858                                ongoing_chkpt.current_page += 1;
4859                            }
4860                            continue 'inner;
4861                        }
4862                        // Issue read if page wasn't found in the page cache or doesnt meet
4863                        // the frame requirements
4864                        let inflight =
4865                            self.issue_wal_read_into_buffer(page_id as usize, target_frame)?;
4866                        group.add(&inflight.completion);
4867                        nr_completions += 1;
4868                        ongoing_chkpt.inflight_reads.push(inflight);
4869                        ongoing_chkpt.current_page += 1;
4870                    }
4871
4872                    // Start a write if batch is ready and we're not at write limit
4873                    let should_flush = ongoing_chkpt.inflight_writes.len() < MAX_INFLIGHT_WRITES
4874                        && ongoing_chkpt.should_flush_batch();
4875                    if should_flush {
4876                        let batch_map = ongoing_chkpt.pending_writes.take();
4877                        if !batch_map.is_empty() {
4878                            let new_write = InflightWriteBatch::new();
4879                            for c in write_pages_vectored(
4880                                pager,
4881                                batch_map,
4882                                new_write.done.clone(),
4883                                new_write.err.clone(),
4884                            )? {
4885                                group.add(&c);
4886                                nr_completions += 1;
4887                            }
4888                            ongoing_chkpt.inflight_writes.push(new_write);
4889                        }
4890                    }
4891                    if nr_completions > 0 {
4892                        io_yield_one!(group.build());
4893                    } else if ongoing_chkpt.complete() {
4894                        ongoing_chkpt.state = CheckpointState::DetermineResult;
4895                    } else {
4896                        // This should be impossible now so we treat it as logic error.
4897                        mark_unlikely();
4898                        return Err(LimboError::InternalError(
4899                            "checkpoint stuck: no inflight completions but not complete".into(),
4900                        ));
4901                    }
4902                }
4903                // All eligible frames copied to the db file.
4904                // Compute checkpoint result, update nBackfills, restart log if needed.
4905                CheckpointState::DetermineResult => {
4906                    let mut ongoing_chkpt = self.ongoing_checkpoint.write();
4907                    turso_assert!(
4908                        ongoing_chkpt.complete(),
4909                        "checkpoint pending flush must have finished"
4910                    );
4911                    let wal_max_frame = self.load_coordination_snapshot().max_frame;
4912                    let wal_total_backfilled = ongoing_chkpt.max_frame;
4913                    // Record two num pages fields to return as checkpoint result to caller.
4914                    // Ref: pnLog, pnCkpt on https://www.sqlite.org/c3ref/wal_checkpoint_v2.html
4915
4916                    // the total # of frames we actually backfilled
4917                    let wal_checkpoint_backfilled =
4918                        wal_total_backfilled.saturating_sub(ongoing_chkpt.min_frame - 1);
4919
4920                    let checkpoint_result = CheckpointResult::new(
4921                        wal_max_frame,
4922                        wal_total_backfilled,
4923                        wal_checkpoint_backfilled,
4924                    );
4925                    tracing::debug!("checkpoint_result={:?}, mode={:?}", checkpoint_result, mode);
4926                    if mode.require_all_backfilled() && !checkpoint_result.everything_backfilled() {
4927                        return Err(LimboError::Busy);
4928                    }
4929                    if mode.should_restart_log() {
4930                        turso_assert!(
4931                            matches!(
4932                                *self.checkpoint_guard.read(),
4933                                Some(CheckpointLocks::Writer { .. })
4934                            ),
4935                            "We must hold writer and checkpoint locks to restart the log",
4936                            { "checkpoint_guard": *self.checkpoint_guard.read() }
4937                        );
4938                        self.restart_log()?;
4939                    }
4940                    ongoing_chkpt.state = CheckpointState::Finalize {
4941                        checkpoint_result: Some(checkpoint_result),
4942                    };
4943                }
4944                CheckpointState::Finalize { .. } => {
4945                    // NOTE: For TRUNCATE mode, WAL truncation is NOT done here.
4946                    // It is deferred to pager.rs after the DB file has been synced,
4947                    // at which point it calls truncate_wal().
4948                    // This ensures data durability: if a crash occurs after WAL truncation
4949                    // but before DB sync, the data would be lost. By truncating the WAL
4950                    // only after the DB is safely synced, we guarantee recoverability.
4951                    if mode.should_restart_log() {
4952                        Self::unlock_after_restart(&self.coordination, None);
4953                    }
4954                    let mut checkpoint_result = {
4955                        let mut oc = self.ongoing_checkpoint.write();
4956                        let CheckpointState::Finalize {
4957                            checkpoint_result, ..
4958                        } = &mut oc.state
4959                        else {
4960                            panic!("unexpected state");
4961                        };
4962                        checkpoint_result.take().unwrap()
4963                    };
4964                    // increment wal epoch to ensure no stale pages are used for backfilling
4965                    self.increment_checkpoint_epoch();
4966
4967                    tracing::debug!("checkpoint_result={:?}", checkpoint_result);
4968                    // we cannot truncate the db file here because we are currently inside a
4969                    // mut borrow of pager.wal, and accessing the header will attempt a borrow
4970                    // during 'read_page', so the caller will use the result to determine if:
4971                    // a. the max frame == num wal frames (everything backfilled)
4972                    // b. the max frame > 0 (we have something to truncate)
4973                    if checkpoint_result.should_truncate()
4974                        || checkpoint_result.wal_checkpoint_backfilled > 0
4975                    {
4976                        // Backfilled frames are not globally durable until
4977                        // the pager syncs the DB file and publishes
4978                        // nbackfills. Keep the checkpoint guard through that
4979                        // tail so another writer cannot restart the WAL
4980                        // generation underneath a pending publish.
4981                        checkpoint_result.maybe_guard = self.checkpoint_guard.write().take();
4982                    } else {
4983                        let _ = self.checkpoint_guard.write().take();
4984                    }
4985                    {
4986                        let mut oc = self.ongoing_checkpoint.write();
4987                        oc.inflight_writes.clear();
4988                        oc.pending_writes.clear();
4989                        oc.pages_to_checkpoint.clear();
4990                        oc.current_page = 0;
4991                    }
4992                    let oc_time = self.ongoing_checkpoint.read().time;
4993                    tracing::debug!(
4994                        "total time spent checkpointing: {:?}",
4995                        self.io
4996                            .current_time_monotonic()
4997                            .duration_since(oc_time)
4998                            .as_millis()
4999                    );
5000                    self.ongoing_checkpoint.write().state = CheckpointState::Start;
5001                    return Ok(IOResult::Done(checkpoint_result));
5002                }
5003            }
5004        }
5005    }
5006
5007    /// Coordinate what the maximum safe frame is for us to backfill when checkpointing.
5008    /// We can never backfill a frame with a higher number than any reader's read mark,
5009    /// because we might overwrite content the reader is reading from the database file.
5010    ///
5011    /// A checkpoint must never overwrite a page in the main DB file if some
5012    /// active reader might still need to read that page from the WAL.
5013    /// Concretely: the checkpoint may only copy frames `<= aReadMark[k]` for
5014    /// every in-use reader slot `k > 0`.
5015    ///
5016    /// `read_locks[0]` is special: readers holding slot 0 ignore the WAL entirely
5017    /// (they read only the DB file). Its value is a placeholder and does not
5018    /// constrain `mxSafeFrame`.
5019    ///
5020    /// For each slot 1..N:
5021    /// - If we can acquire the write lock (slot is free):
5022    ///   - Slot 1: Set to mxSafeFrame (allowing new readers to see up to this point)
5023    ///   - Slots 2+: Set to READMARK_NOT_USED (freeing the slot)
5024    /// - If we cannot acquire the lock (SQLITE_BUSY):
5025    ///   - Lower mxSafeFrame to that reader's mark
5026    ///   - In PASSIVE mode: Already have no busy handler, continue scanning
5027    ///   - In FULL/RESTART/TRUNCATE: Disable busy handler for remaining slots
5028    ///
5029    /// Locking behavior:
5030    /// - PASSIVE: Never waits, no busy handler (xBusy==NULL)
5031    /// - FULL/RESTART/TRUNCATE: May wait via busy handler, but after first BUSY,
5032    ///   switches to non-blocking for remaining slots
5033    ///
5034    /// We never modify slot values while a reader holds that slot's lock.
5035    /// TOOD: implement proper BUSY handling behavior
5036    fn determine_max_safe_checkpoint_frame(&self) -> u64 {
5037        self.coordination
5038            .determine_max_safe_checkpoint_frame(self.load_coordination_snapshot().max_frame)
5039    }
5040
5041    /// attempt to restart WAL header before write in order to keep WAL file size under the control
5042    /// The conditions for WAL restart are following:
5043    /// 1. we can do that only under write transaction
5044    /// 2. max_frame_read_lock_index == 0 - this means that transaction was initiated to read data from DB file
5045    /// 3. nbackfills > 0 - otherwise nothing was backfilled and there is no reason to truncate header
5046    /// 4. max_frame == nbackfills - otherwise there are some non-checkpointed frames in the WAL and we can't truncate the log
5047    pub fn try_restart_log_before_write(&self) -> Result<()> {
5048        let max_frame_read_lock_index = self.max_frame_read_lock_index.load(Ordering::Acquire);
5049        if max_frame_read_lock_index != 0 {
5050            tracing::debug!(
5051                "try_restart_log_before_write: max_frame_read_lock_index={max_frame_read_lock_index}, writer use WAL - can't restart the log"
5052            );
5053            return Ok(());
5054        }
5055        let snapshot = self.load_coordination_snapshot();
5056        let max_frame = snapshot.max_frame;
5057        let nbackfills = snapshot.nbackfills;
5058        if nbackfills == 0 {
5059            tracing::debug!(
5060                "try_restart_log_before_write: nbackfills={nbackfills}, nothing were backfilled - can't restart the log"
5061            );
5062            return Ok(());
5063        }
5064        turso_assert!(
5065            max_frame >= nbackfills,
5066            "backfills can't be more than max_frame"
5067        );
5068        if max_frame != nbackfills {
5069            tracing::debug!(
5070                "try_restart_log_before_write: max_frame={max_frame}, nbackfills={nbackfills}, not everything is backfilled to the DB file - can't restart the log"
5071            );
5072            return Ok(());
5073        }
5074        let Some(snapshot) = self
5075            .coordination
5076            .try_restart_log_for_write(self.io.as_ref())?
5077        else {
5078            return Ok(());
5079        };
5080        self.apply_restart_snapshot(snapshot);
5081        self.increment_checkpoint_epoch();
5082        let result = Ok(());
5083        tracing::debug!("try_restart_log_before_write: result={:?}", result);
5084        result
5085    }
5086
5087    fn restart_log(&self) -> Result<()> {
5088        tracing::debug!("restart_log");
5089        let snapshot = self.coordination.begin_restart(self.io.as_ref())?;
5090        self.apply_restart_snapshot(snapshot);
5091        Ok(())
5092    }
5093
5094    /// Truncate WAL file to zero and sync it. Called by pager AFTER DB file is synced.
5095    #[aristo::intent("WAL truncate is atomic: no committed frame can be observed lost across the truncate operation\n", id = "aristos:wal_truncate_atomic_under_concurrent_writers", verify = "full", parent = "wal_protocol_correctness")]
5096    fn truncate_log(
5097        &self,
5098        result: &mut CheckpointResult,
5099        sync_type: FileSyncType,
5100    ) -> Result<IOResult<()>> {
5101        let file = self.coordination.prepare_truncate()?;
5102
5103        if !result.wal_truncate_sent {
5104            let c = Completion::new_trunc({
5105                move |res| {
5106                    if let Err(err) = res {
5107                        tracing::debug!("WAL truncate failed: {err}")
5108                    } else {
5109                        tracing::trace!("WAL file truncated to 0 B");
5110                    }
5111                }
5112            });
5113            let c = file.truncate(0, c)?;
5114            result.wal_truncate_sent = true;
5115            // after truncation - there will be nothing in the WAL
5116            result.wal_max_frame = 0;
5117            result.wal_total_backfilled = 0;
5118            io_yield_one!(c);
5119        } else if !result.wal_sync_sent {
5120            let c = file.sync(
5121                Completion::new_sync(move |res| {
5122                    if let Err(err) = res {
5123                        tracing::debug!("WAL sync failed: {err}")
5124                    } else {
5125                        tracing::trace!("WAL file synced after truncation");
5126                    }
5127                }),
5128                sync_type,
5129            )?;
5130            result.wal_sync_sent = true;
5131            io_yield_one!(c);
5132        }
5133        Ok(IOResult::Done(()))
5134    }
5135
5136    fn apply_restart_snapshot(&self, snapshot: WalSnapshot) {
5137        *self.last_checksum.write() = snapshot.last_checksum;
5138        self.max_frame.store(snapshot.max_frame, Ordering::Release);
5139        self.min_frame.store(0, Ordering::Release);
5140        self.checkpoint_seq
5141            .store(snapshot.checkpoint_seq, Ordering::Release);
5142    }
5143
5144    // unlock shared read locks taken by RESTART/TRUNCATE checkpoint modes
5145    fn unlock_after_restart(coordination: &Arc<dyn WalCoordination>, e: Option<&LimboError>) {
5146        coordination.end_restart();
5147        if let Some(e) = e {
5148            mark_unlikely();
5149            tracing::debug!(
5150                "Failed to restart WAL header: {:?}, releasing read locks",
5151                e
5152            );
5153        }
5154    }
5155
5156    fn acquire_proper_checkpoint_guard(
5157        &self,
5158        mode: CheckpointMode,
5159        lock_source: CheckpointLockSource,
5160    ) -> Result<()> {
5161        let needs_new_guard = {
5162            let guard = self.checkpoint_guard.read();
5163            !matches!(
5164                (&*guard, mode),
5165                (
5166                    Some(CheckpointLocks::Read0 { .. }),
5167                    CheckpointMode::Passive { .. },
5168                ) | (
5169                    Some(CheckpointLocks::Writer { .. }),
5170                    CheckpointMode::Restart | CheckpointMode::Truncate { .. },
5171                ),
5172            )
5173        };
5174        if needs_new_guard {
5175            // Drop any existing guard
5176            if self.checkpoint_guard.read().is_some() {
5177                let _ = self.checkpoint_guard.write().take();
5178            }
5179            let guard = match lock_source {
5180                CheckpointLockSource::Acquire => {
5181                    CheckpointLocks::new(self.coordination.clone(), mode)?
5182                }
5183                CheckpointLockSource::HeldByCaller => {
5184                    CheckpointLocks::from_held_vacuum_checkpoint_lock(self.coordination.clone())?
5185                }
5186            };
5187            *self.checkpoint_guard.write() = Some(guard);
5188        }
5189        Ok(())
5190    }
5191
5192    fn issue_wal_read_into_buffer(&self, page_id: usize, frame_id: u64) -> Result<InflightRead> {
5193        let offset = self.frame_offset(frame_id);
5194        let buf_slot = Arc::new(SpinLock::new(None));
5195        tracing::debug!(
5196            "Issuing WAL read: page_id={}, frame_id={}, offset={}",
5197            page_id,
5198            frame_id,
5199            offset
5200        );
5201
5202        let complete = {
5203            let buf_slot = buf_slot.clone();
5204            Box::new(move |res: Result<(Arc<Buffer>, i32), CompletionError>| {
5205                let Ok((buf, read)) = res else {
5206                    return None;
5207                };
5208                let buf_len = buf.len();
5209                turso_assert!(
5210                    read == buf_len as i32,
5211                    "read bytes does not match expected buffer length",
5212                    { "read": read, "expected": buf_len, "frame_id": frame_id }
5213                );
5214                *buf_slot.lock() = Some(buf);
5215                None
5216            })
5217        };
5218        // schedule read of the page payload
5219        let file = self.coordination.wal_file()?;
5220        let c = begin_read_wal_frame(
5221            file.as_ref(),
5222            offset + WAL_FRAME_HEADER_SIZE as u64,
5223            self.buffer_pool.clone(),
5224            complete,
5225            page_id,
5226            &self.io_ctx.read(),
5227        )?;
5228
5229        Ok(InflightRead {
5230            completion: c,
5231            page_id,
5232            buf: buf_slot,
5233        })
5234    }
5235
5236    /// MVCC helper: check if WAL state changed and refresh local snapshot without starting a read tx.
5237    /// FIXME: this isn't TOCTOU safe because we're not taking WAL read locks.
5238    ///
5239    /// This is only used to invalidate page cache, so false positives are sort of acceptable since
5240    /// MVCC reads currently don't read from WAL frames ever.
5241    /// FIXME: MVCC should start using pager read transactions anyway so that we can get rid of
5242    /// the stop-the-world MVCC checkpoint that blocks all reads.
5243    pub fn mvcc_refresh_if_db_changed(&self) -> bool {
5244        let snapshot = self.load_coordination_snapshot();
5245        let local_state = self.connection_state();
5246        let changed = self.db_changed_against(snapshot, local_state);
5247        if changed {
5248            self.install_connection_state(local_state.with_snapshot(snapshot));
5249        }
5250        changed
5251    }
5252}
5253
5254#[cfg(host_shared_wal)]
5255fn read_exact_bytes_from_file(
5256    io: &Arc<dyn IO>,
5257    file: &Arc<dyn File>,
5258    offset: u64,
5259    len: usize,
5260) -> Result<Option<Vec<u8>>> {
5261    let read_buf = Arc::new(Buffer::new_temporary(len));
5262    let bytes_read = Arc::new(AtomicUsize::new(usize::MAX));
5263    let c = file.pread(
5264        offset,
5265        Completion::new_read(read_buf.clone(), {
5266            let bytes_read = bytes_read.clone();
5267            Box::new(move |res| {
5268                if let Ok((_buf, count)) = res {
5269                    bytes_read.store(count as usize, Ordering::Release);
5270                }
5271                None
5272            })
5273        }),
5274    )?;
5275    io.wait_for_completion(c)?;
5276    if bytes_read.load(Ordering::Acquire) != len {
5277        return Ok(None);
5278    }
5279    Ok(Some(read_buf.as_slice()[..len].to_vec()))
5280}
5281
5282#[cfg(host_shared_wal)]
5283fn read_validated_wal_header_from_file(
5284    io: &Arc<dyn IO>,
5285    file: &Arc<dyn File>,
5286) -> Result<Option<WalHeader>> {
5287    let Some(bytes) = read_exact_bytes_from_file(io, file, 0, WAL_HEADER_SIZE)? else {
5288        return Ok(None);
5289    };
5290    let header = WalHeader {
5291        magic: u32::from_be_bytes(bytes[0..4].try_into().unwrap()),
5292        file_format: u32::from_be_bytes(bytes[4..8].try_into().unwrap()),
5293        page_size: u32::from_be_bytes(bytes[8..12].try_into().unwrap()),
5294        checkpoint_seq: u32::from_be_bytes(bytes[12..16].try_into().unwrap()),
5295        salt_1: u32::from_be_bytes(bytes[16..20].try_into().unwrap()),
5296        salt_2: u32::from_be_bytes(bytes[20..24].try_into().unwrap()),
5297        checksum_1: u32::from_be_bytes(bytes[24..28].try_into().unwrap()),
5298        checksum_2: u32::from_be_bytes(bytes[28..32].try_into().unwrap()),
5299    };
5300    if !matches!(header.magic, WAL_MAGIC_LE | WAL_MAGIC_BE) {
5301        return Ok(None);
5302    }
5303    if PageSize::new(header.page_size).is_none() {
5304        return Ok(None);
5305    }
5306    let use_native_endian = cfg!(target_endian = "big") == ((header.magic & 1) != 0);
5307    let calc = checksum_wal(
5308        &bytes[..WAL_HEADER_SIZE - 8],
5309        &header,
5310        (0, 0),
5311        use_native_endian,
5312    );
5313    if calc != (header.checksum_1, header.checksum_2) {
5314        return Ok(None);
5315    }
5316    Ok(Some(header))
5317}
5318
5319#[cfg(host_shared_wal)]
5320fn wal_header_matches_authority_snapshot(
5321    wal_header: WalHeader,
5322    snapshot: SharedWalCoordinationHeader,
5323) -> bool {
5324    wal_header.page_size == snapshot.page_size
5325        && wal_header.checkpoint_seq == snapshot.checkpoint_seq
5326        && wal_header.salt_1 == snapshot.salt_1
5327        && wal_header.salt_2 == snapshot.salt_2
5328}
5329
5330pub(crate) fn database_identity_from_header_bytes(header_bytes: &[u8]) -> Result<(u32, u32)> {
5331    if header_bytes.len() < DatabaseHeader::SIZE {
5332        return Err(LimboError::Corrupt(format!(
5333            "database header must be at least {} bytes, got {}",
5334            DatabaseHeader::SIZE,
5335            header_bytes.len()
5336        )));
5337    }
5338    if header_bytes[0..16] != *b"SQLite format 3\0" {
5339        return Err(LimboError::Corrupt("database header magic mismatch".into()));
5340    }
5341    let db_size_pages = u32::from_be_bytes(header_bytes[28..32].try_into().unwrap());
5342    let header_crc32c = crc32c::crc32c(&header_bytes[..DatabaseHeader::SIZE]);
5343    Ok((db_size_pages, header_crc32c))
5344}
5345
5346fn read_database_identity_from_storage(
5347    io: &Arc<dyn IO>,
5348    db_file: &Arc<dyn DatabaseStorage>,
5349) -> Result<Option<(u32, u32)>> {
5350    let read_buf = Arc::new(Buffer::new_temporary(PageSize::MIN as usize));
5351    let bytes_read = Arc::new(AtomicUsize::new(usize::MAX));
5352    let c = db_file.read_header(Completion::new_read(read_buf.clone(), {
5353        let bytes_read = bytes_read.clone();
5354        Box::new(move |res| {
5355            if let Ok((_buf, count)) = res {
5356                bytes_read.store(count as usize, Ordering::Release);
5357            }
5358            None
5359        })
5360    }))?;
5361    io.wait_for_completion(c)?;
5362    if bytes_read.load(Ordering::Acquire) < DatabaseHeader::SIZE {
5363        return Ok(None);
5364    }
5365    Ok(Some(database_identity_from_header_bytes(
5366        &read_buf.as_slice()[..DatabaseHeader::SIZE],
5367    )?))
5368}
5369
5370#[cfg(all(clt_turso_tests, host_shared_wal))]
5371fn read_database_identity_from_file_path(
5372    io: &Arc<dyn IO>,
5373    wal_path: &str,
5374) -> Result<Option<(u32, u32)>> {
5375    let db_path = wal_path
5376        .strip_suffix("-wal")
5377        .unwrap_or(wal_path)
5378        .to_string();
5379    let file = match io.open_file(&db_path, crate::OpenFlags::None, false) {
5380        Ok(file) => file,
5381        Err(LimboError::CompletionError(CompletionError::IOError(
5382            std::io::ErrorKind::NotFound,
5383            _,
5384        ))) => return Ok(None),
5385        Err(err) => return Err(err),
5386    };
5387    let Some(bytes) = read_exact_bytes_from_file(io, &file, 0, DatabaseHeader::SIZE)? else {
5388        return Ok(None);
5389    };
5390    Ok(Some(database_identity_from_header_bytes(&bytes)?))
5391}
5392
5393#[cfg(host_shared_wal)]
5394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5395enum AuthoritySnapshotValidation {
5396    Trusted,
5397    RebuildFromDisk(AuthoritySnapshotRebuildReason),
5398}
5399
5400#[cfg(host_shared_wal)]
5401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5402enum AuthoritySnapshotRebuildReason {
5403    WalHeaderUnreadable,
5404    WalHeaderMismatch,
5405    WalTooShortForSnapshot,
5406    WalLengthMismatch,
5407    LastFrameMissing,
5408    LastFrameNotCommit,
5409    LastFrameSaltMismatch,
5410    LastFrameChecksumMismatch,
5411}
5412
5413#[cfg(host_shared_wal)]
5414fn classify_authority_snapshot_against_wal(
5415    io: &Arc<dyn IO>,
5416    file: &Arc<dyn File>,
5417    snapshot: SharedWalCoordinationHeader,
5418) -> Result<AuthoritySnapshotValidation> {
5419    let wal_size = file.size()?;
5420    if snapshot.max_frame == 0 {
5421        if wal_size == 0 {
5422            return Ok(AuthoritySnapshotValidation::Trusted);
5423        }
5424        if wal_size == WAL_HEADER_SIZE as u64 {
5425            let Some(wal_header) = read_validated_wal_header_from_file(io, file)? else {
5426                return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5427                    AuthoritySnapshotRebuildReason::WalHeaderUnreadable,
5428                ));
5429            };
5430            return Ok(
5431                if wal_header_matches_authority_snapshot(wal_header, snapshot) {
5432                    AuthoritySnapshotValidation::Trusted
5433                } else {
5434                    AuthoritySnapshotValidation::RebuildFromDisk(
5435                        AuthoritySnapshotRebuildReason::WalHeaderMismatch,
5436                    )
5437                },
5438            );
5439        }
5440        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5441            AuthoritySnapshotRebuildReason::WalLengthMismatch,
5442        ));
5443    }
5444
5445    if wal_size < WAL_HEADER_SIZE as u64 {
5446        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5447            AuthoritySnapshotRebuildReason::WalTooShortForSnapshot,
5448        ));
5449    }
5450
5451    let Some(wal_header) = read_validated_wal_header_from_file(io, file)? else {
5452        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5453            AuthoritySnapshotRebuildReason::WalHeaderUnreadable,
5454        ));
5455    };
5456    if !wal_header_matches_authority_snapshot(wal_header, snapshot) {
5457        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5458            AuthoritySnapshotRebuildReason::WalHeaderMismatch,
5459        ));
5460    }
5461
5462    let frame_size = WAL_FRAME_HEADER_SIZE as u64 + wal_header.page_size as u64;
5463    let expected_wal_len = WAL_HEADER_SIZE as u64 + snapshot.max_frame * frame_size;
5464    if wal_size != expected_wal_len {
5465        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5466            AuthoritySnapshotRebuildReason::WalLengthMismatch,
5467        ));
5468    }
5469
5470    let last_frame_offset = WAL_HEADER_SIZE as u64 + (snapshot.max_frame - 1) * frame_size;
5471    let Some(frame_bytes) =
5472        read_exact_bytes_from_file(io, file, last_frame_offset, frame_size as usize)?
5473    else {
5474        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5475            AuthoritySnapshotRebuildReason::LastFrameMissing,
5476        ));
5477    };
5478    let (frame_header, _) = sqlite3_ondisk::parse_wal_frame_header(&frame_bytes);
5479    if !frame_header.is_commit_frame() {
5480        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5481            AuthoritySnapshotRebuildReason::LastFrameNotCommit,
5482        ));
5483    }
5484    if frame_header.salt_1 != snapshot.salt_1 || frame_header.salt_2 != snapshot.salt_2 {
5485        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5486            AuthoritySnapshotRebuildReason::LastFrameSaltMismatch,
5487        ));
5488    }
5489    if frame_header.checksum_1 != snapshot.checksum_1
5490        || frame_header.checksum_2 != snapshot.checksum_2
5491    {
5492        return Ok(AuthoritySnapshotValidation::RebuildFromDisk(
5493            AuthoritySnapshotRebuildReason::LastFrameChecksumMismatch,
5494        ));
5495    }
5496    Ok(AuthoritySnapshotValidation::Trusted)
5497}
5498
5499impl WalFileShared {
5500    pub fn last_checksum_and_max_frame(&self) -> ((u32, u32), u64) {
5501        (
5502            self.metadata.last_checksum,
5503            self.metadata.max_frame.load(Ordering::Acquire),
5504        )
5505    }
5506
5507    #[cfg(host_shared_wal)]
5508    pub(crate) fn open_shared_from_authority_if_exists(
5509        io: &Arc<dyn IO>,
5510        path: &str,
5511        flags: crate::OpenFlags,
5512        authority: &Arc<MappedSharedWalCoordination>,
5513        db_file: &Arc<dyn DatabaseStorage>,
5514    ) -> Result<Arc<RwLock<WalFileShared>>> {
5515        let snapshot = authority.snapshot();
5516        let file = match io.open_file(path, flags, false) {
5517            Ok(file) => file,
5518            Err(LimboError::CompletionError(CompletionError::IOError(
5519                std::io::ErrorKind::NotFound,
5520                _,
5521            ))) if flags.contains(crate::OpenFlags::ReadOnly) => {
5522                return Ok(WalFileShared::new_noop());
5523            }
5524            Err(e) => return Err(e),
5525        };
5526        let wal_size = file.size()?;
5527
5528        match classify_authority_snapshot_against_wal(io, &file, snapshot)? {
5529            AuthoritySnapshotValidation::Trusted => {}
5530            AuthoritySnapshotValidation::RebuildFromDisk(reason) => {
5531                tracing::debug!(
5532                    ?reason,
5533                    "rebuilding WAL state from disk because persisted authority is not provably reusable"
5534                );
5535                return sqlite3_ondisk::build_shared_wal(&file, io);
5536            }
5537        }
5538        if authority.frame_index_overflowed() {
5539            tracing::debug!(
5540                "rebuilding WAL state from disk because the persisted tshm frame index is marked overflowed"
5541            );
5542            return sqlite3_ondisk::build_shared_wal(&file, io);
5543        }
5544        if snapshot.nbackfills != 0
5545            && authority.open_mode() == SharedWalCoordinationOpenMode::Exclusive
5546        {
5547            tracing::debug!(
5548                nbackfills = snapshot.nbackfills,
5549                max_frame = snapshot.max_frame,
5550                "rebuilding WAL state from disk because an exclusive reopen must conservatively clear published backfill progress"
5551            );
5552            return sqlite3_ondisk::build_shared_wal(&file, io);
5553        }
5554        if snapshot.max_frame > snapshot.nbackfills
5555            && authority
5556                .iter_latest_frames(0, snapshot.max_frame)
5557                .is_empty()
5558        {
5559            tracing::debug!(
5560                max_frame = snapshot.max_frame,
5561                nbackfills = snapshot.nbackfills,
5562                "rebuilding WAL state from disk because the persisted tshm frame index has no entries for a visible WAL tail"
5563            );
5564            return sqlite3_ondisk::build_shared_wal(&file, io);
5565        }
5566        if snapshot.nbackfills != 0 {
5567            let Some((db_size_pages, db_header_crc32c)) =
5568                read_database_identity_from_storage(io, db_file)?
5569            else {
5570                tracing::debug!(
5571                    nbackfills = snapshot.nbackfills,
5572                    "rebuilding WAL state from disk because the main database header is unavailable for backfill-proof validation"
5573                );
5574                return sqlite3_ondisk::build_shared_wal(&file, io);
5575            };
5576            if !authority.validate_backfill_proof(snapshot, db_size_pages, db_header_crc32c) {
5577                tracing::debug!(
5578                    nbackfills = snapshot.nbackfills,
5579                    "rebuilding WAL state from disk because persisted tshm backfill proof is not valid for the current database header"
5580                );
5581                return sqlite3_ondisk::build_shared_wal(&file, io);
5582            }
5583        }
5584        let wal_is_initialized = wal_size >= WAL_HEADER_SIZE as u64;
5585
5586        let wal_header = WalHeader {
5587            page_size: snapshot.page_size,
5588            checkpoint_seq: snapshot.checkpoint_seq,
5589            salt_1: snapshot.salt_1,
5590            salt_2: snapshot.salt_2,
5591            checksum_1: snapshot.checksum_1,
5592            checksum_2: snapshot.checksum_2,
5593            ..WalHeader::new()
5594        };
5595        let read_locks = array::from_fn(|_| TursoRwLock::new());
5596        for (i, lock) in read_locks.iter().enumerate() {
5597            lock.write();
5598            lock.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED });
5599            lock.unlock();
5600        }
5601
5602        let shared = WalFileShared {
5603            metadata: WalSharedMetadata {
5604                enabled: AtomicBool::new(true),
5605                wal_header: Arc::new(SpinLock::new(wal_header)),
5606                min_frame: AtomicU64::new(0),
5607                max_frame: AtomicU64::new(snapshot.max_frame),
5608                nbackfills: AtomicU64::new(snapshot.nbackfills),
5609                transaction_count: AtomicU64::new(snapshot.transaction_count),
5610                last_checksum: (snapshot.checksum_1, snapshot.checksum_2),
5611                loaded: AtomicBool::new(true),
5612                loaded_from_disk_scan: AtomicBool::new(false),
5613                initialized: AtomicBool::new(wal_is_initialized),
5614            },
5615            runtime: WalSharedRuntime {
5616                authority_reconciliation: Default::default(),
5617                frame_cache: Arc::new(SpinLock::new(FxHashMap::default())),
5618                frame_cache_high_water: AtomicU64::new(0),
5619                file: Some(file),
5620                read_locks,
5621                vacuum_lock: TursoRwLock::new(),
5622                write_lock: TursoRwLock::new(),
5623                checkpoint_lock: TursoRwLock::new(),
5624                epoch: AtomicU32::new(snapshot.checkpoint_epoch),
5625                overflow_fallback_coverage: Arc::new(SpinLock::new(
5626                    OverflowFallbackCoverage::default(),
5627                )),
5628            },
5629        };
5630        Ok(Arc::new(RwLock::new(shared)))
5631    }
5632
5633    pub fn open_shared_if_exists(
5634        io: &Arc<dyn IO>,
5635        path: &str,
5636        flags: crate::OpenFlags,
5637    ) -> Result<Arc<RwLock<WalFileShared>>> {
5638        let mut driver = Self::open_shared_if_exists_begin(io, path, flags)?;
5639        io.block(|| driver.poll())
5640    }
5641
5642    /// Non-blocking entry point for [`WalFileShared::open_shared_if_exists`].
5643    /// Performs only the synchronous file open (and readonly/NotFound noop
5644    /// handling); the WAL recovery scan is driven via [`OpenSharedWal::poll`].
5645    pub fn open_shared_if_exists_begin(
5646        io: &Arc<dyn IO>,
5647        path: &str,
5648        flags: crate::OpenFlags,
5649    ) -> Result<OpenSharedWal> {
5650        let file = match io.open_file(path, flags, false) {
5651            Ok(file) => file,
5652            Err(LimboError::CompletionError(CompletionError::IOError(
5653                std::io::ErrorKind::NotFound,
5654                _,
5655            ))) if flags.contains(crate::OpenFlags::ReadOnly) => {
5656                // In readonly mode, if the WAL file doesn't exist, we just return a noop WAL
5657                // since there's nothing to read from.
5658                return Ok(OpenSharedWal::Noop(WalFileShared::new_noop()));
5659            }
5660            Err(e) => return Err(e),
5661        };
5662        Ok(OpenSharedWal::Build(sqlite3_ondisk::BuildSharedWal::begin(
5663            &file,
5664        )?))
5665    }
5666
5667    pub fn is_initialized(&self) -> Result<bool> {
5668        Ok(self.metadata.initialized.load(Ordering::Acquire))
5669    }
5670
5671    pub fn new_noop() -> Arc<RwLock<WalFileShared>> {
5672        let wal_header = WalHeader::new();
5673        let read_locks = array::from_fn(|_| TursoRwLock::new());
5674        for (i, lock) in read_locks.iter().enumerate() {
5675            lock.write();
5676            lock.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED });
5677            lock.unlock();
5678        }
5679        let shared = WalFileShared {
5680            metadata: WalSharedMetadata {
5681                enabled: AtomicBool::new(false),
5682                wal_header: Arc::new(SpinLock::new(wal_header)),
5683                min_frame: AtomicU64::new(0),
5684                max_frame: AtomicU64::new(0),
5685                nbackfills: AtomicU64::new(0),
5686                transaction_count: AtomicU64::new(0),
5687                last_checksum: (0, 0),
5688                loaded: AtomicBool::new(true),
5689                loaded_from_disk_scan: AtomicBool::new(false),
5690                initialized: AtomicBool::new(false),
5691            },
5692            runtime: WalSharedRuntime {
5693                authority_reconciliation: Default::default(),
5694                frame_cache: Arc::new(SpinLock::new(FxHashMap::default())),
5695                frame_cache_high_water: AtomicU64::new(0),
5696                file: None,
5697                read_locks,
5698                vacuum_lock: TursoRwLock::new(),
5699                write_lock: TursoRwLock::new(),
5700                checkpoint_lock: TursoRwLock::new(),
5701                epoch: AtomicU32::new(0),
5702                overflow_fallback_coverage: Arc::new(SpinLock::new(
5703                    OverflowFallbackCoverage::default(),
5704                )),
5705            },
5706        };
5707        Arc::new(RwLock::new(shared))
5708    }
5709
5710    #[cfg(clt_turso_tests)]
5711    pub(super) fn new_shared(file: Arc<dyn File>) -> Result<Arc<RwLock<WalFileShared>>> {
5712        let wal_header = WalHeader::new();
5713        let read_locks = array::from_fn(|_| TursoRwLock::new());
5714        // slot zero is always zero as it signifies that reads can be done from the db file
5715        // directly, and slot 1 is the default read mark containing the max frame. in this case
5716        // our max frame is zero so both slots 0 and 1 begin at 0
5717        for (i, lock) in read_locks.iter().enumerate() {
5718            lock.write();
5719            lock.set_value_exclusive(if i < 2 { 0 } else { READMARK_NOT_USED });
5720            lock.unlock();
5721        }
5722        let shared = WalFileShared {
5723            metadata: WalSharedMetadata {
5724                enabled: AtomicBool::new(true),
5725                wal_header: Arc::new(SpinLock::new(wal_header)),
5726                min_frame: AtomicU64::new(0),
5727                max_frame: AtomicU64::new(0),
5728                nbackfills: AtomicU64::new(0),
5729                transaction_count: AtomicU64::new(0),
5730                last_checksum: (0, 0),
5731                loaded: AtomicBool::new(true),
5732                loaded_from_disk_scan: AtomicBool::new(false),
5733                initialized: AtomicBool::new(false),
5734            },
5735            runtime: WalSharedRuntime {
5736                authority_reconciliation: Default::default(),
5737                frame_cache: Arc::new(SpinLock::new(FxHashMap::default())),
5738                frame_cache_high_water: AtomicU64::new(0),
5739                file: Some(file),
5740                read_locks,
5741                vacuum_lock: TursoRwLock::new(),
5742                write_lock: TursoRwLock::new(),
5743                checkpoint_lock: TursoRwLock::new(),
5744                epoch: AtomicU32::new(0),
5745                overflow_fallback_coverage: Arc::new(SpinLock::new(
5746                    OverflowFallbackCoverage::default(),
5747                )),
5748            },
5749        };
5750        Ok(Arc::new(RwLock::new(shared)))
5751    }
5752
5753    pub fn page_size(&self) -> u32 {
5754        self.metadata.wal_header.lock().page_size
5755    }
5756
5757    /// Called after a successful RESTART/TRUNCATE mode checkpoint
5758    /// when all frames are back‑filled.
5759    ///
5760    /// sqlite3/src/wal.c
5761    /// The following is guaranteed when this function is called:
5762    ///
5763    ///   a) the WRITER lock is held,
5764    ///   b) the entire log file has been checkpointed, and
5765    ///   c) any existing readers are reading exclusively from the database
5766    ///      file - there are no readers that may attempt to read a frame from
5767    ///      the log file.
5768    ///
5769    /// This function updates the shared-memory structures so that the next
5770    /// client to write to the database (which may be this one) does so by
5771    /// writing frames into the start of the log file.
5772    fn restart_wal_header(&mut self, io: &dyn IO) {
5773        {
5774            let mut hdr = self.metadata.wal_header.lock();
5775            hdr.checkpoint_seq = hdr.checkpoint_seq.wrapping_add(1);
5776            // keep hdr.magic, hdr.file_format, hdr.page_size as-is
5777            hdr.salt_1 = hdr.salt_1.wrapping_add(1);
5778            hdr.salt_2 = io.generate_random_number() as u32;
5779
5780            self.metadata.max_frame.store(0, Ordering::Release);
5781            self.metadata.nbackfills.store(0, Ordering::Release);
5782            self.metadata.last_checksum = (hdr.checksum_1, hdr.checksum_2);
5783            // `prepare_wal_start` (used in the `commit_wal_inner`) do the work only if WAL is not initialized yet (so, self.initialized is false)
5784            // we change WAL state here, so on next write attempt `prepare_wal_start` will update WAL header
5785            self.metadata.initialized.store(false, Ordering::Release);
5786        }
5787
5788        self.runtime.frame_cache.lock().clear();
5789        self.runtime
5790            .frame_cache_high_water
5791            .store(0, Ordering::Release);
5792        // read-marks
5793        self.runtime.read_locks[0].set_value_exclusive(0);
5794        self.runtime.read_locks[1].set_value_exclusive(0);
5795        for lock in &self.runtime.read_locks[2..] {
5796            lock.set_value_exclusive(READMARK_NOT_USED);
5797        }
5798    }
5799
5800    /// Replace restored WAL state while preserving process-local locks owned by
5801    /// existing connections.
5802    ///
5803    /// External restore paths rebuild metadata/cache/file state from disk while
5804    /// other connections may still hold read guards. Those guards are tied to
5805    /// the process-local lock objects, not to the restored on-disk WAL view, so
5806    /// replacing the lock objects would make normal `end_read_tx` unlock a
5807    /// fresh empty lock. Keep lock identity stable and refresh only state
5808    /// derived from storage.
5809    #[cfg(clt_turso_feature = "conn_raw_api")]
5810    pub fn replace_after_external_restore(&mut self, restored: WalFileShared) {
5811        self.metadata = restored.metadata;
5812        self.runtime.frame_cache = restored.runtime.frame_cache;
5813        self.runtime.file = restored.runtime.file;
5814        self.runtime.epoch.store(
5815            restored.runtime.epoch.load(Ordering::Acquire),
5816            Ordering::Release,
5817        );
5818        self.runtime.overflow_fallback_coverage = restored.runtime.overflow_fallback_coverage;
5819    }
5820}
5821
5822#[cfg(clt_turso_tests)]
5823pub mod test {
5824    #[cfg(host_shared_wal)]
5825    use super::{
5826        classify_authority_snapshot_against_wal, AuthoritySnapshotRebuildReason,
5827        AuthoritySnapshotValidation, ShmWalCoordination,
5828    };
5829    use super::{
5830        CheckpointLocks, InProcessWalCoordination, ReadGuardKind, RollbackTo, TryBeginReadResult,
5831        Wal, WalAutoActions, WalCommitState, WalConnectionState, WalCoordination, WalFile,
5832        WalSnapshot, NO_LOCK_HELD,
5833    };
5834    #[cfg(host_shared_wal)]
5835    use crate::storage::shared_wal_coordination::{
5836        MappedSharedWalCoordination, SharedWalCoordinationHeader, SharedWalCoordinationOpenMode,
5837    };
5838    use crate::sync::{atomic::Ordering, Arc};
5839    use crate::sync::{Mutex, RwLock};
5840    use crate::{
5841        io::FileSyncType,
5842        storage::{
5843            buffer_pool::BufferPool,
5844            database::{DatabaseFile, DatabaseStorage},
5845            pager::{allocate_new_page, PageRef},
5846            sqlite3_ondisk::{self, PageSize, WAL_HEADER_SIZE},
5847            wal::READMARK_NOT_USED,
5848        },
5849        types::IOResult,
5850        util::IOExt,
5851        Buffer, CheckpointMode, CheckpointResult, Completion, CompletionError, Connection,
5852        Database, File, LimboError, MemoryIO, OpenFlags, PlatformIO, SyncMode, WalFileShared, IO,
5853    };
5854    use std::num::NonZeroUsize;
5855    #[cfg(unix)]
5856    use std::os::unix::fs::MetadataExt;
5857    /// Returns an IO backend that supports shared WAL coordination on the host.
5858    /// On Windows the default `PlatformIO` (`WindowsIO`) lacks the byte-locking
5859    /// and mapping primitives, so the experimental IOCP backend is used when
5860    /// the `experimental_win_iocp` feature is enabled.
5861    fn shared_wal_test_io() -> Arc<dyn IO> {
5862        #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp"))]
5863        {
5864            Arc::new(crate::WindowsIOCP::new().unwrap())
5865        }
5866        #[cfg(not(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp")))]
5867        {
5868            Arc::new(PlatformIO::new().unwrap())
5869        }
5870    }
5871
5872    #[allow(clippy::arc_with_non_send_sync)]
5873    pub(crate) fn get_database() -> (Arc<Database>, std::path::PathBuf) {
5874        let mut path = tempfile::tempdir().unwrap().keep();
5875        let dbpath = path.clone();
5876        path.push("test.db");
5877        {
5878            let connection = rusqlite::Connection::open(&path).unwrap();
5879            connection
5880                .pragma_update(None, "journal_mode", "wal")
5881                .unwrap();
5882        }
5883        let io = shared_wal_test_io();
5884        let db = Database::open_file_with_flags(
5885            io.clone(),
5886            path.to_str().unwrap(),
5887            crate::OpenFlags::default(),
5888            crate::DatabaseOpts::new().with_multiprocess_wal(true),
5889            None,
5890        )
5891        .unwrap();
5892        // db + tmp directory
5893        (db, dbpath)
5894    }
5895
5896    struct DeferredReadFile {
5897        inner: Arc<dyn File>,
5898        pending_reads: Mutex<Vec<(u64, Completion)>>,
5899    }
5900
5901    impl DeferredReadFile {
5902        fn new(inner: Arc<dyn File>) -> Self {
5903            Self {
5904                inner,
5905                pending_reads: Mutex::new(Vec::new()),
5906            }
5907        }
5908
5909        fn complete_pending_reads(&self) {
5910            let pending_reads = std::mem::take(&mut *self.pending_reads.lock());
5911            for (pos, completion) in pending_reads {
5912                std::mem::drop(self.inner.pread(pos, completion).unwrap());
5913            }
5914        }
5915    }
5916
5917    impl File for DeferredReadFile {
5918        fn lock_file(&self, exclusive: bool) -> crate::Result<()> {
5919            self.inner.lock_file(exclusive)
5920        }
5921
5922        fn unlock_file(&self) -> crate::Result<()> {
5923            self.inner.unlock_file()
5924        }
5925
5926        fn pread(&self, pos: u64, c: Completion) -> crate::Result<Completion> {
5927            self.pending_reads.lock().push((pos, c.clone()));
5928            Ok(c)
5929        }
5930
5931        fn pwrite(
5932            &self,
5933            pos: u64,
5934            buffer: Arc<Buffer>,
5935            c: Completion,
5936        ) -> crate::Result<Completion> {
5937            self.inner.pwrite(pos, buffer, c)
5938        }
5939
5940        fn sync(
5941            &self,
5942            c: Completion,
5943            sync_type: crate::io::FileSyncType,
5944        ) -> crate::Result<Completion> {
5945            self.inner.sync(c, sync_type)
5946        }
5947
5948        fn size(&self) -> crate::Result<u64> {
5949            self.inner.size()
5950        }
5951
5952        fn truncate(&self, len: u64, c: Completion) -> crate::Result<Completion> {
5953            self.inner.truncate(len, c)
5954        }
5955    }
5956
5957    #[cfg(clt_turso_feature = "conn_raw_api")]
5958    #[test]
5959    fn replace_after_external_restore_preserves_lock_identity() {
5960        let shared = WalFileShared::new_noop();
5961        let restored = WalFileShared::new_noop();
5962
5963        let read_lock_ptrs = {
5964            let shared = shared.read();
5965            shared
5966                .runtime
5967                .read_locks
5968                .iter()
5969                .map(std::ptr::from_ref)
5970                .collect::<Vec<_>>()
5971        };
5972        {
5973            let shared = shared.read();
5974            assert!(shared.runtime.read_locks[1].write());
5975            shared.runtime.read_locks[1].set_value_exclusive(7);
5976            shared.runtime.read_locks[1].unlock();
5977        }
5978        {
5979            let restored = restored.read();
5980            restored.metadata.max_frame.store(42, Ordering::Release);
5981            assert!(restored.runtime.read_locks[1].write());
5982            restored.runtime.read_locks[1].set_value_exclusive(99);
5983            restored.runtime.read_locks[1].unlock();
5984        }
5985
5986        let restored = match Arc::try_unwrap(restored) {
5987            Ok(restored) => restored.into_inner(),
5988            Err(_) => panic!("restored WAL test state should not be shared"),
5989        };
5990        shared.write().replace_after_external_restore(restored);
5991
5992        let shared = shared.read();
5993        assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 42);
5994        assert_eq!(shared.runtime.read_locks[1].get_value(), 7);
5995        for (idx, lock) in shared.runtime.read_locks.iter().enumerate() {
5996            assert_eq!(std::ptr::from_ref(lock), read_lock_ptrs[idx]);
5997        }
5998    }
5999
6000    #[test]
6001    fn test_truncate_file() {
6002        let (db, _path) = get_database();
6003        let conn = db.connect().unwrap();
6004        conn.execute("create table test (id integer primary key, value text)")
6005            .unwrap();
6006        let _ = conn.execute("insert into test (value) values ('test1'), ('test2'), ('test3')");
6007        let wal = db.shared_wal.write();
6008        let wal_file = wal.runtime.file.as_ref().unwrap().clone();
6009        let done = Arc::new(Mutex::new(false));
6010        let _done = done.clone();
6011        let _ = wal_file.truncate(
6012            WAL_HEADER_SIZE as u64,
6013            Completion::new_trunc(move |_| {
6014                *_done.lock() = true;
6015            }),
6016        );
6017        assert!(wal_file.size().unwrap() == WAL_HEADER_SIZE as u64);
6018        assert!(*done.lock());
6019    }
6020
6021    #[test]
6022    fn test_wal_truncate_checkpoint() {
6023        let (db, path) = get_database();
6024        let mut walpath = path.clone().into_os_string().into_string().unwrap();
6025        walpath.push_str("/test.db-wal");
6026        let walpath = std::path::PathBuf::from(walpath);
6027
6028        let conn = db.connect().unwrap();
6029        conn.execute("create table test (id integer primary key, value text)")
6030            .unwrap();
6031        for _i in 0..25 {
6032            let _ = conn.execute("insert into test (value) values (randomblob(1024)), (randomblob(1024)), (randomblob(1024))");
6033        }
6034        let pager = conn.pager.load();
6035        let _ = pager.cacheflush();
6036
6037        let stat = std::fs::metadata(&walpath).unwrap();
6038        let meta_before = std::fs::metadata(&walpath).unwrap();
6039        let bytes_before = meta_before.len();
6040        run_checkpoint_until_done(
6041            &pager,
6042            CheckpointMode::Truncate {
6043                upper_bound_inclusive: None,
6044            },
6045        );
6046
6047        assert_eq!(pager.wal_state().unwrap().max_frame, 0);
6048
6049        tracing::debug!("wal filepath: {walpath:?}, size: {}", stat.len());
6050        let meta_after = std::fs::metadata(&walpath).unwrap();
6051        let bytes_after = meta_after.len();
6052        assert_ne!(
6053            bytes_before, bytes_after,
6054            "WAL file should not have been empty before checkpoint"
6055        );
6056        assert_eq!(
6057            bytes_after, 0,
6058            "WAL file should be truncated to 0 bytes, but is {bytes_after} bytes",
6059        );
6060        std::fs::remove_dir_all(path).unwrap();
6061    }
6062
6063    #[test]
6064    #[cfg_attr(
6065        windows,
6066        ignore = "shutdown checkpoint does not truncate the WAL file to zero on Windows"
6067    )]
6068    fn test_shutdown_checkpoint_truncates_after_restart() {
6069        let (db, path) = get_database();
6070        let mut walpath = path.clone().into_os_string().into_string().unwrap();
6071        walpath.push_str("/test.db-wal");
6072        let walpath = std::path::PathBuf::from(walpath);
6073
6074        let conn = db.connect().unwrap();
6075        conn.execute("create table test (id integer primary key, value text)")
6076            .unwrap();
6077        conn.execute("insert into test (value) values ('v1'), ('v2')")
6078            .unwrap();
6079
6080        let pager = conn.pager.load();
6081        run_checkpoint_until_done(&pager, CheckpointMode::Restart);
6082
6083        let bytes_before = std::fs::metadata(&walpath).unwrap().len();
6084        assert!(
6085            bytes_before > 0,
6086            "WAL should still have data after RESTART checkpoint"
6087        );
6088
6089        conn.close().unwrap();
6090
6091        let bytes_after = std::fs::metadata(&walpath).unwrap().len();
6092        assert_eq!(
6093            bytes_after, 0,
6094            "Shutdown checkpoint should truncate WAL after RESTART, but WAL is {bytes_after} bytes",
6095        );
6096        std::fs::remove_dir_all(path).unwrap();
6097    }
6098
6099    fn bulk_inserts(conn: &Arc<Connection>, n_txns: usize, rows_per_txn: usize) {
6100        for _ in 0..n_txns {
6101            conn.execute("begin transaction").unwrap();
6102            for i in 0..rows_per_txn {
6103                conn.execute(format!("insert into test(value) values ('v{i}')"))
6104                    .unwrap();
6105            }
6106            conn.execute("commit").unwrap();
6107        }
6108    }
6109
6110    fn count_test_table(conn: &Arc<Connection>) -> i64 {
6111        let mut stmt = conn.prepare("select count(*) from test").unwrap();
6112        let mut count: i64 = 0;
6113        stmt.run_with_row_callback(|row| {
6114            count = row.get(0).unwrap();
6115            Ok(())
6116        })
6117        .unwrap();
6118        count
6119    }
6120
6121    fn run_checkpoint_until_done(pager: &crate::Pager, mode: CheckpointMode) -> CheckpointResult {
6122        // Use pager.checkpoint() instead of wal.checkpoint() directly because
6123        // WAL truncation (for TRUNCATE mode) now happens in pager's TruncateWalFile phase.
6124        pager
6125            .io
6126            .block(|| pager.checkpoint(mode, crate::SyncMode::Full, true))
6127            .unwrap()
6128    }
6129
6130    fn run_wal_checkpoint_until_done(
6131        db: &Database,
6132        pager: &crate::Pager,
6133        mode: CheckpointMode,
6134    ) -> CheckpointResult {
6135        let wal = pager.wal.as_ref().expect("wal should be present");
6136        loop {
6137            match wal.checkpoint(pager, mode) {
6138                Ok(IOResult::IO(io)) => io.wait(db.io.as_ref()).unwrap(),
6139                Ok(IOResult::Done(result)) => return result,
6140                Err(err) => panic!("checkpoint should succeed: {err:?}"),
6141            }
6142        }
6143    }
6144
6145    #[test]
6146    fn test_wal_checkpoint_defers_backfill_publication_until_db_sync() {
6147        let (db, _path) = get_database();
6148        let wal_shared = db.shared_wal.clone();
6149        let conn = db.connect().unwrap();
6150        conn.execute("create table test(id integer primary key, value text)")
6151            .unwrap();
6152        bulk_inserts(&conn, 8, 2);
6153
6154        let pager = conn.pager.load();
6155        let result = run_wal_checkpoint_until_done(&db, &pager, CheckpointMode::Full);
6156        assert!(
6157            result.wal_total_backfilled > 0,
6158            "checkpoint setup should backfill frames before DB sync"
6159        );
6160        assert_eq!(
6161            wal_shared.read().metadata.nbackfills.load(Ordering::SeqCst),
6162            0,
6163            "wal.checkpoint() must not publish positive nbackfills before DB sync completes"
6164        );
6165    }
6166
6167    #[test]
6168    fn test_checkpoint_sync_mode_off_leaves_backfill_unpublished() {
6169        let (db, _path) = get_database();
6170        let wal_shared = db.shared_wal.clone();
6171        let conn = db.connect().unwrap();
6172        conn.execute("create table test(id integer primary key, value text)")
6173            .unwrap();
6174        bulk_inserts(&conn, 8, 2);
6175
6176        let pager = conn.pager.load();
6177        let result = pager
6178            .io
6179            .block(|| pager.checkpoint(CheckpointMode::Full, SyncMode::Off, true))
6180            .unwrap();
6181        assert!(
6182            result.wal_total_backfilled > 0,
6183            "sync-mode-off checkpoint setup should still backfill frames into the DB file"
6184        );
6185        assert_eq!(
6186            wal_shared.read().metadata.nbackfills.load(Ordering::SeqCst),
6187            0,
6188            "SyncMode::Off must not publish positive nbackfills as durable shared state"
6189        );
6190    }
6191
6192    fn make_test_wal() -> (Arc<RwLock<WalFileShared>>, WalFile) {
6193        let io = shared_wal_test_io();
6194        let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6195        let shared = WalFileShared::new_noop();
6196        let coordination: Arc<dyn WalCoordination> =
6197            Arc::new(InProcessWalCoordination::new(shared.clone()));
6198        let wal = WalFile::new_with_coordination(io, coordination, ((0, 0), 0), buffer_pool);
6199        (shared, wal)
6200    }
6201
6202    fn make_test_wal_from_shared(shared: Arc<RwLock<WalFileShared>>) -> WalFile {
6203        let io = shared_wal_test_io();
6204        let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6205        let snapshot = shared.read().last_checksum_and_max_frame();
6206        WalFile::new(io, shared, snapshot, buffer_pool)
6207    }
6208
6209    fn make_initialized_memory_wal(page_size: u32) -> (Arc<dyn IO>, Arc<BufferPool>, WalFile) {
6210        let io: Arc<dyn IO> = Arc::new(MemoryIO::new());
6211        let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6212        buffer_pool
6213            .finalize_with_page_size(page_size as usize)
6214            .unwrap();
6215        let file = io
6216            .open_file("direct-batch-read.db-wal", OpenFlags::Create, false)
6217            .unwrap();
6218        let shared = WalFileShared::new_shared(file).unwrap();
6219        let wal = WalFile::new(io.clone(), shared, ((0, 0), 0), buffer_pool.clone());
6220        let page_size = PageSize::new(page_size).unwrap();
6221
6222        if let Some(c) = wal.prepare_wal_start(page_size).unwrap() {
6223            io.wait_for_completion(c).unwrap();
6224        }
6225        let c = wal.prepare_wal_finish(FileSyncType::Fsync).unwrap();
6226        io.wait_for_completion(c).unwrap();
6227
6228        (io, buffer_pool, wal)
6229    }
6230
6231    /// Like `make_initialized_memory_wal`, but backed by `MemoryYieldIO`, which
6232    /// writes bytes synchronously yet defers every I/O *completion* until the
6233    /// next `io.step()`. That makes the "write submitted but not yet durable"
6234    /// window observable in a single-threaded test.
6235    #[cfg(clt_turso_feature = "io_memory_yield")]
6236    fn make_initialized_memory_yield_wal(
6237        page_size: u32,
6238    ) -> (Arc<dyn IO>, Arc<BufferPool>, WalFile) {
6239        let io: Arc<dyn IO> = Arc::new(crate::io::MemoryYieldIO::new());
6240        let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6241        buffer_pool
6242            .finalize_with_page_size(page_size as usize)
6243            .unwrap();
6244        let file = io
6245            .open_file("spill-visibility.db-wal", OpenFlags::Create, false)
6246            .unwrap();
6247        let shared = WalFileShared::new_shared(file).unwrap();
6248        let wal = WalFile::new(io.clone(), shared, ((0, 0), 0), buffer_pool.clone());
6249        let page_size = PageSize::new(page_size).unwrap();
6250
6251        if let Some(c) = wal.prepare_wal_start(page_size).unwrap() {
6252            io.wait_for_completion(c).unwrap();
6253        }
6254        let c = wal.prepare_wal_finish(FileSyncType::Fsync).unwrap();
6255        io.wait_for_completion(c).unwrap();
6256
6257        (io, buffer_pool, wal)
6258    }
6259
6260    /// Regression test for the cache-spill WAL append path: a frame appended via
6261    /// `append_frames_vectored` must not become resolvable by `find_frame`
6262    /// (reads) or `iter_latest_frames` (checkpoint) until its write is durable.
6263    ///
6264    /// An earlier version of the async spill fix published the page->frame
6265    /// mapping (`cache_frame`) synchronously at submission, before the write
6266    /// landed on disk — so a reader or a concurrent checkpoint could resolve a
6267    /// frame whose bytes were not yet written. `MemoryYieldIO` defers the write
6268    /// completion until `io.step()`, so this test can observe the frame while
6269    /// the write is still in flight: the mapping must not be visible yet.
6270    #[cfg(clt_turso_feature = "io_memory_yield")]
6271    #[test]
6272    fn append_frames_vectored_frame_hidden_until_write_is_durable() {
6273        let page_size = 512;
6274        let (io, buffer_pool, wal) = make_initialized_memory_yield_wal(page_size);
6275        let page = page_with_pattern(7, 0x70, &buffer_pool);
6276
6277        let completion = wal
6278            .append_frames_vectored(vec![page], PageSize::new(page_size).unwrap())
6279            .unwrap();
6280
6281        // The write cursor advances synchronously (so a following batch chains
6282        // correctly), but the completion has not fired: the write is not durable.
6283        assert!(
6284            !completion.succeeded(),
6285            "MemoryYieldIO must defer the write completion until io.step()"
6286        );
6287        assert_eq!(
6288            wal.get_max_frame(),
6289            1,
6290            "write cursor advances synchronously"
6291        );
6292
6293        // The frame must NOT be resolvable before the write is durable. The
6294        // buggy version cached the mapping at submission and returned Some(1)
6295        // here, exposing bytes that were not on disk yet.
6296        assert_eq!(
6297            wal.find_frame(7, None).unwrap(),
6298            None,
6299            "frame must not be visible to readers before its write is durable"
6300        );
6301
6302        // Drive the deferred completion: the write is now durable and the
6303        // completion callback publishes the page->frame mapping.
6304        io.step().unwrap();
6305        assert!(completion.succeeded());
6306
6307        assert_eq!(
6308            wal.find_frame(7, None).unwrap(),
6309            Some(1),
6310            "frame must be visible once its write is durable"
6311        );
6312    }
6313
6314    fn page_with_pattern(page_id: i64, seed: u8, buffer_pool: &Arc<BufferPool>) -> PageRef {
6315        let page = allocate_new_page(page_id, buffer_pool);
6316        for (idx, byte) in page.get_contents().as_ptr().iter_mut().enumerate() {
6317            *byte = seed.wrapping_add(idx as u8).wrapping_add(page_id as u8);
6318        }
6319        page
6320    }
6321
6322    fn append_test_pages(
6323        io: &Arc<dyn IO>,
6324        wal: &WalFile,
6325        page_size: u32,
6326        pages: &[PageRef],
6327    ) -> Vec<Vec<u8>> {
6328        let prepared = wal
6329            .prepare_frames(pages, PageSize::new(page_size).unwrap(), Some(99), None)
6330            .unwrap();
6331        let expected = pages
6332            .iter()
6333            .map(|page| page.get_contents().as_ptr().to_vec())
6334            .collect::<Vec<_>>();
6335
6336        let file = wal.wal_file().unwrap();
6337        let c = file
6338            .pwritev(
6339                prepared.offset,
6340                prepared.bufs.clone(),
6341                Completion::new_write(|_| {}),
6342            )
6343            .unwrap();
6344        io.wait_for_completion(c).unwrap();
6345        wal.commit_prepared_frames(&[prepared]);
6346        wal.finish_append_frames_commit().unwrap();
6347        expected
6348    }
6349
6350    #[test]
6351    fn append_frames_vectored_spill_frames_are_not_reused_by_next_prepare() {
6352        let page_size = 512;
6353        let (_io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6354        let spill_page = page_with_pattern(7, 0x70, &buffer_pool);
6355
6356        let completion = wal
6357            .append_frames_vectored(vec![spill_page], PageSize::new(page_size).unwrap())
6358            .unwrap();
6359        assert!(completion.succeeded());
6360        assert_eq!(wal.get_max_frame(), 1);
6361        assert_eq!(wal.get_max_frame_in_wal(), 0);
6362
6363        let commit_page = page_with_pattern(9, 0x90, &buffer_pool);
6364        let prepared = wal
6365            .prepare_frames(
6366                &[commit_page],
6367                PageSize::new(page_size).unwrap(),
6368                Some(99),
6369                None,
6370            )
6371            .unwrap();
6372
6373        assert_eq!(
6374            prepared.metadata[0].1, 2,
6375            "prepare_frames must chain after unpublished spill frames"
6376        );
6377        assert_eq!(prepared.final_max_frame, 2);
6378    }
6379
6380    fn wait_for_completion_error(io: &Arc<dyn IO>, completion: Completion) -> CompletionError {
6381        match io.wait_for_completion(completion) {
6382            Err(LimboError::CompletionError(err)) => err,
6383            other => panic!("expected completion error, got {other:?}"),
6384        }
6385    }
6386
6387    #[test]
6388    fn read_frames_batch_reads_contiguous_wal_frames_directly() {
6389        let page_size = 512;
6390        let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6391        let source_pages = vec![
6392            page_with_pattern(2, 0x10, &buffer_pool),
6393            page_with_pattern(3, 0x20, &buffer_pool),
6394            page_with_pattern(4, 0x30, &buffer_pool),
6395            page_with_pattern(5, 0x40, &buffer_pool),
6396        ];
6397        let expected = append_test_pages(&io, &wal, page_size, &source_pages);
6398
6399        let target_pages = vec![
6400            Arc::new(crate::Page::new(2)),
6401            Arc::new(crate::Page::new(3)),
6402            Arc::new(crate::Page::new(4)),
6403            Arc::new(crate::Page::new(5)),
6404        ];
6405        let c = wal
6406            .read_frames_batch(1, &target_pages, buffer_pool, None)
6407            .unwrap();
6408        io.wait_for_completion(c).unwrap();
6409
6410        for (idx, page) in target_pages.iter().enumerate() {
6411            assert!(page.is_loaded(), "page {} should be loaded", page.get().id);
6412            assert!(!page.is_locked(), "page {} lock leaked", page.get().id);
6413            assert_eq!(page.wal_tag_pair(), ((idx + 1) as u64, 0));
6414            assert_eq!(page.get_contents().as_ptr(), expected[idx].as_slice());
6415        }
6416    }
6417
6418    #[test]
6419    fn read_frames_batch_can_start_from_middle_frame() {
6420        let page_size = 512;
6421        let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6422        let source_pages = vec![
6423            page_with_pattern(10, 0x01, &buffer_pool),
6424            page_with_pattern(11, 0x02, &buffer_pool),
6425            page_with_pattern(12, 0x03, &buffer_pool),
6426            page_with_pattern(13, 0x04, &buffer_pool),
6427        ];
6428        let expected = append_test_pages(&io, &wal, page_size, &source_pages);
6429
6430        let target_pages = vec![
6431            Arc::new(crate::Page::new(11)),
6432            Arc::new(crate::Page::new(12)),
6433            Arc::new(crate::Page::new(13)),
6434        ];
6435        let c = wal
6436            .read_frames_batch(2, &target_pages, buffer_pool, None)
6437            .unwrap();
6438        io.wait_for_completion(c).unwrap();
6439
6440        for (idx, page) in target_pages.iter().enumerate() {
6441            assert!(page.is_loaded(), "page {} should be loaded", page.get().id);
6442            assert!(!page.is_locked(), "page {} lock leaked", page.get().id);
6443            assert_eq!(page.wal_tag_pair(), ((idx + 2) as u64, 0));
6444            assert_eq!(page.get_contents().as_ptr(), expected[idx + 1].as_slice());
6445        }
6446    }
6447
6448    #[test]
6449    fn read_frames_batch_follows_physical_frame_order_not_page_id_order() {
6450        let page_size = 512;
6451        let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6452        let source_pages = vec![
6453            page_with_pattern(7, 0x71, &buffer_pool),
6454            page_with_pattern(2, 0x22, &buffer_pool),
6455            page_with_pattern(5, 0x55, &buffer_pool),
6456            page_with_pattern(9, 0x99, &buffer_pool),
6457        ];
6458        let expected = append_test_pages(&io, &wal, page_size, &source_pages);
6459
6460        let target_pages = vec![
6461            Arc::new(crate::Page::new(7)),
6462            Arc::new(crate::Page::new(2)),
6463            Arc::new(crate::Page::new(5)),
6464            Arc::new(crate::Page::new(9)),
6465        ];
6466        let c = wal
6467            .read_frames_batch(1, &target_pages, buffer_pool, None)
6468            .unwrap();
6469        io.wait_for_completion(c).unwrap();
6470
6471        for (idx, page) in target_pages.iter().enumerate() {
6472            assert_eq!(
6473                page.get_contents().as_ptr(),
6474                expected[idx].as_slice(),
6475                "frame-order read should preserve page {} contents",
6476                page.get().id
6477            );
6478            assert_eq!(page.wal_tag_pair(), ((idx + 1) as u64, 0));
6479        }
6480    }
6481
6482    #[test]
6483    fn read_frames_batch_short_read_errors_and_clears_page_locks() {
6484        let page_size = 512;
6485        let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6486        let source_pages = vec![
6487            page_with_pattern(20, 0x20, &buffer_pool),
6488            page_with_pattern(21, 0x21, &buffer_pool),
6489        ];
6490        append_test_pages(&io, &wal, page_size, &source_pages);
6491
6492        let target_pages = vec![
6493            Arc::new(crate::Page::new(20)),
6494            Arc::new(crate::Page::new(21)),
6495            Arc::new(crate::Page::new(22)),
6496        ];
6497        let c = wal
6498            .read_frames_batch(1, &target_pages, buffer_pool, None)
6499            .unwrap();
6500        let err = wait_for_completion_error(&io, c);
6501
6502        assert!(
6503            matches!(err, CompletionError::ShortReadWalFrame { .. }),
6504            "unexpected error: {err:?}"
6505        );
6506        for page in &target_pages {
6507            assert!(!page.is_locked(), "page {} lock leaked", page.get().id);
6508            assert!(
6509                !page.is_loaded(),
6510                "page {} should not be loaded",
6511                page.get().id
6512            );
6513            assert!(
6514                !page.has_wal_tag(),
6515                "page {} should not be tagged",
6516                page.get().id
6517            );
6518        }
6519    }
6520
6521    #[test]
6522    fn read_frames_batch_page_number_mismatch_returns_error_not_panic() {
6523        let page_size = 512;
6524        let (io, buffer_pool, wal) = make_initialized_memory_wal(page_size);
6525        let source_pages = vec![
6526            page_with_pattern(30, 0x30, &buffer_pool),
6527            page_with_pattern(31, 0x31, &buffer_pool),
6528        ];
6529        append_test_pages(&io, &wal, page_size, &source_pages);
6530
6531        let target_pages = vec![
6532            Arc::new(crate::Page::new(30)),
6533            Arc::new(crate::Page::new(99)),
6534        ];
6535        let c = wal
6536            .read_frames_batch(1, &target_pages, buffer_pool, None)
6537            .unwrap();
6538        let err = wait_for_completion_error(&io, c);
6539
6540        assert!(
6541            matches!(
6542                err,
6543                CompletionError::WalFramePageMismatch {
6544                    frame_id: 2,
6545                    expected: 99,
6546                    actual: 31
6547                }
6548            ),
6549            "unexpected error: {err:?}"
6550        );
6551        for page in &target_pages {
6552            assert!(!page.is_locked(), "page {} lock leaked", page.get().id);
6553            assert!(
6554                !page.is_loaded(),
6555                "page {} should not be loaded",
6556                page.get().id
6557            );
6558            assert!(
6559                !page.has_wal_tag(),
6560                "page {} should not be tagged",
6561                page.get().id
6562            );
6563            assert!(
6564                page.get().buffer.is_none(),
6565                "page {} should not retain a buffer",
6566                page.get().id
6567            );
6568        }
6569    }
6570
6571    fn set_shared_snapshot(shared: &Arc<RwLock<WalFileShared>>, snapshot: WalSnapshot) {
6572        let mut guard = shared.write();
6573        guard
6574            .metadata
6575            .max_frame
6576            .store(snapshot.max_frame, Ordering::Release);
6577        guard
6578            .metadata
6579            .nbackfills
6580            .store(snapshot.nbackfills, Ordering::Release);
6581        guard.metadata.last_checksum = snapshot.last_checksum;
6582        guard.metadata.wal_header.lock().checkpoint_seq = snapshot.checkpoint_seq;
6583        guard
6584            .metadata
6585            .transaction_count
6586            .store(snapshot.transaction_count, Ordering::Release);
6587    }
6588
6589    fn make_test_coordination(shared: &Arc<RwLock<WalFileShared>>) -> InProcessWalCoordination {
6590        InProcessWalCoordination::new(shared.clone())
6591    }
6592
6593    #[cfg(host_shared_wal)]
6594    fn make_test_shm_coordination(
6595        shared: &Arc<RwLock<WalFileShared>>,
6596        path: &std::path::Path,
6597    ) -> (Arc<MappedSharedWalCoordination>, ShmWalCoordination) {
6598        let io = shared_wal_test_io();
6599        let authority =
6600            Arc::new(MappedSharedWalCoordination::create_or_open(&io, path, 64).unwrap());
6601        let coordination = ShmWalCoordination::new(shared.clone(), authority.clone());
6602        (authority, coordination)
6603    }
6604
6605    #[cfg(host_shared_wal)]
6606    fn active_shared_reader_slot_count(authority: &MappedSharedWalCoordination) -> usize {
6607        let reader_slot_count = authority.snapshot().reader_slot_count;
6608        (0..reader_slot_count)
6609            .filter(|&slot_index| authority.reader_owner(slot_index).is_some())
6610            .count()
6611    }
6612
6613    #[cfg(host_shared_wal)]
6614    fn write_test_wal_with_single_commit_frame(
6615        io: &Arc<dyn IO>,
6616        wal_path: &std::path::Path,
6617    ) -> SharedWalCoordinationHeader {
6618        let file = io
6619            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
6620            .unwrap();
6621        let mut wal_header = sqlite3_ondisk::WalHeader {
6622            page_size: 4096,
6623            checkpoint_seq: 5,
6624            salt_1: 17,
6625            salt_2: 23,
6626            ..sqlite3_ondisk::WalHeader::new()
6627        };
6628        let use_native_endian = cfg!(target_endian = "big") == ((wal_header.magic & 1) != 0);
6629        let mut header_prefix = [0u8; WAL_HEADER_SIZE - 8];
6630        header_prefix[0..4].copy_from_slice(&wal_header.magic.to_be_bytes());
6631        header_prefix[4..8].copy_from_slice(&wal_header.file_format.to_be_bytes());
6632        header_prefix[8..12].copy_from_slice(&wal_header.page_size.to_be_bytes());
6633        header_prefix[12..16].copy_from_slice(&wal_header.checkpoint_seq.to_be_bytes());
6634        header_prefix[16..20].copy_from_slice(&wal_header.salt_1.to_be_bytes());
6635        header_prefix[20..24].copy_from_slice(&wal_header.salt_2.to_be_bytes());
6636        let header_checksum =
6637            sqlite3_ondisk::checksum_wal(&header_prefix, &wal_header, (0, 0), use_native_endian);
6638        wal_header.checksum_1 = header_checksum.0;
6639        wal_header.checksum_2 = header_checksum.1;
6640
6641        io.wait_for_completion(
6642            sqlite3_ondisk::begin_write_wal_header(file.as_ref(), &wal_header).unwrap(),
6643        )
6644        .unwrap();
6645
6646        let buffer_pool = BufferPool::begin_init(io, BufferPool::TEST_ARENA_SIZE);
6647        buffer_pool
6648            .finalize_with_page_size(wal_header.page_size as usize)
6649            .unwrap();
6650        #[allow(unused_mut)]
6651        let mut page = vec![0x5a; wal_header.page_size as usize];
6652        #[cfg(clt_turso_feature = "checksum")]
6653        crate::storage::checksum::ChecksumContext::new()
6654            .add_checksum_to_page(&mut page, 7)
6655            .unwrap();
6656        let (frame_checksum, frame_buf) = sqlite3_ondisk::prepare_wal_frame(
6657            &buffer_pool,
6658            &wal_header,
6659            header_checksum,
6660            wal_header.page_size,
6661            7,
6662            1,
6663            &page,
6664        );
6665        let c = file
6666            .pwrite(
6667                WAL_HEADER_SIZE as u64,
6668                frame_buf,
6669                Completion::new_write(|_| {}),
6670            )
6671            .unwrap();
6672        io.wait_for_completion(c).unwrap();
6673        let c = file
6674            .sync(Completion::new_sync(|_| {}), crate::io::FileSyncType::Fsync)
6675            .unwrap();
6676        io.wait_for_completion(c).unwrap();
6677
6678        SharedWalCoordinationHeader {
6679            max_frame: 1,
6680            nbackfills: 0,
6681            transaction_count: 9,
6682            visibility_generation: 3,
6683            checkpoint_seq: wal_header.checkpoint_seq,
6684            checkpoint_epoch: 7,
6685            page_size: wal_header.page_size,
6686            salt_1: wal_header.salt_1,
6687            salt_2: wal_header.salt_2,
6688            checksum_1: frame_checksum.0,
6689            checksum_2: frame_checksum.1,
6690            reader_slot_count: 64,
6691        }
6692    }
6693
6694    #[cfg(host_shared_wal)]
6695    fn open_test_db_file_for_wal(
6696        io: &Arc<dyn IO>,
6697        wal_path: &std::path::Path,
6698    ) -> Arc<dyn DatabaseStorage> {
6699        let db_path = wal_path.with_extension("db");
6700        Arc::new(DatabaseFile::new(
6701            io.open_file(db_path.to_str().unwrap(), crate::OpenFlags::Create, false)
6702                .unwrap(),
6703        ))
6704    }
6705
6706    #[test]
6707    #[cfg(host_shared_wal)]
6708    fn test_read_frame_keeps_epoch_from_issue_time() {
6709        let dir = tempfile::tempdir().unwrap();
6710        let wal_path = dir.path().join("epoch-race.db-wal");
6711        let io = shared_wal_test_io();
6712        let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
6713
6714        let file = io
6715            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
6716            .unwrap();
6717        let shared = WalFileShared::new_shared(file.clone()).unwrap();
6718        let deferred_file = Arc::new(DeferredReadFile::new(file));
6719        {
6720            let mut shared = shared.write();
6721            shared.runtime.file = Some(deferred_file.clone());
6722            shared
6723                .runtime
6724                .epoch
6725                .store(snapshot.checkpoint_epoch, Ordering::Release);
6726        }
6727
6728        let coordination: Arc<dyn WalCoordination> = Arc::new(make_test_coordination(&shared));
6729        let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6730        buffer_pool
6731            .finalize_with_page_size(snapshot.page_size as usize)
6732            .unwrap();
6733        let wal = WalFile::new_with_coordination(
6734            io.clone(),
6735            coordination,
6736            (
6737                (snapshot.checksum_1, snapshot.checksum_2),
6738                snapshot.max_frame,
6739            ),
6740            buffer_pool.clone(),
6741        );
6742
6743        let page = Arc::new(crate::storage::pager::Page::new(7));
6744        let issued_epoch = wal.coordination.checkpoint_epoch();
6745        let completion = wal.read_frame(1, page.clone(), buffer_pool).unwrap();
6746
6747        wal.increment_checkpoint_epoch();
6748        deferred_file.complete_pending_reads();
6749        io.wait_for_completion(completion).unwrap();
6750
6751        assert_eq!(
6752            page.wal_tag_pair(),
6753            (1, issued_epoch),
6754            "WAL reads must retain the epoch from when the read was issued"
6755        );
6756    }
6757
6758    #[cfg(clt_turso_tests)]
6759    fn read_slots_with_readers(shared: &WalFileShared) -> Vec<usize> {
6760        shared
6761            .runtime
6762            .read_locks
6763            .iter()
6764            .enumerate()
6765            .filter_map(|(slot, lock)| {
6766                let state = lock.0.load(Ordering::Acquire);
6767                let has_readers = (state & super::TursoRwLock::READER_COUNT_MASK) != 0;
6768                has_readers.then_some(slot)
6769            })
6770            .collect()
6771    }
6772
6773    fn wal_header_snapshot(shared: &Arc<RwLock<WalFileShared>>) -> (u32, u32, u32, u32) {
6774        // (checkpoint_seq, salt1, salt2, page_size)
6775        let shared_guard = shared.read();
6776        let hdr = shared_guard.metadata.wal_header.lock();
6777        (hdr.checkpoint_seq, hdr.salt_1, hdr.salt_2, hdr.page_size)
6778    }
6779
6780    #[test]
6781    fn test_wal_connection_state_round_trip() {
6782        let (_shared, wal) = make_test_wal();
6783        let state = WalConnectionState::new(
6784            WalSnapshot {
6785                max_frame: 11,
6786                nbackfills: 7,
6787                last_checksum: (31, 47),
6788                checkpoint_seq: 5,
6789                transaction_count: 13,
6790            },
6791            ReadGuardKind::ReadMark(NonZeroUsize::new(3).unwrap()),
6792        );
6793
6794        wal.install_connection_state(state);
6795
6796        assert_eq!(wal.connection_state(), state);
6797        assert_eq!(wal.connection_state().snapshot.min_frame(), 8);
6798    }
6799
6800    #[test]
6801    fn test_wal_explicit_backend_constructor_does_not_keep_shared_handle() {
6802        let io = shared_wal_test_io();
6803        let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
6804        let shared = WalFileShared::new_noop();
6805        let coordination: Arc<dyn WalCoordination> =
6806            Arc::new(InProcessWalCoordination::new(shared.clone()));
6807
6808        assert_eq!(Arc::strong_count(&shared), 2);
6809
6810        let _wal = WalFile::new_with_coordination(io, coordination, ((0, 0), 0), buffer_pool);
6811
6812        assert_eq!(Arc::strong_count(&shared), 2);
6813    }
6814
6815    #[test]
6816    fn test_mvcc_refresh_updates_snapshot_without_changing_read_guard() {
6817        let (shared, wal) = make_test_wal();
6818        let initial = WalSnapshot {
6819            max_frame: 4,
6820            nbackfills: 2,
6821            last_checksum: (9, 10),
6822            checkpoint_seq: 1,
6823            transaction_count: 3,
6824        };
6825        set_shared_snapshot(&shared, initial);
6826        wal.install_connection_state(WalConnectionState::new(
6827            initial,
6828            ReadGuardKind::ReadMark(NonZeroUsize::new(2).unwrap()),
6829        ));
6830
6831        assert!(!wal.mvcc_refresh_if_db_changed());
6832
6833        let updated = WalSnapshot {
6834            max_frame: 8,
6835            nbackfills: 5,
6836            last_checksum: (21, 34),
6837            checkpoint_seq: 7,
6838            transaction_count: 4,
6839        };
6840        set_shared_snapshot(&shared, updated);
6841
6842        assert!(wal.mvcc_refresh_if_db_changed());
6843        assert_eq!(
6844            wal.connection_state(),
6845            WalConnectionState::new(
6846                updated,
6847                ReadGuardKind::ReadMark(NonZeroUsize::new(2).unwrap())
6848            )
6849        );
6850    }
6851
6852    #[test]
6853    fn test_in_process_coordination_uses_shared_authority() {
6854        let (shared, _wal) = make_test_wal();
6855        let coordination = make_test_coordination(&shared);
6856        let snapshot = WalSnapshot {
6857            max_frame: 9,
6858            nbackfills: 3,
6859            last_checksum: (55, 89),
6860            checkpoint_seq: 7,
6861            transaction_count: 11,
6862        };
6863        set_shared_snapshot(&shared, snapshot);
6864        {
6865            let guard = shared.write();
6866            guard.runtime.epoch.store(5, Ordering::Release);
6867            guard.runtime.frame_cache.lock().extend([
6868                (1, vec![1, 4, 8]),
6869                (2, vec![2, 6]),
6870                (3, vec![3]),
6871            ]);
6872        }
6873
6874        assert_eq!(coordination.load_snapshot(), snapshot);
6875        assert_eq!(coordination.checkpoint_epoch(), 5);
6876        assert_eq!(coordination.find_frame(1, 4, 9, None), Some(8));
6877        assert_eq!(coordination.find_frame(2, 4, 9, Some(5)), Some(2));
6878        assert_eq!(coordination.iter_latest_frames(4, 9), vec![(1, 8), (2, 6)]);
6879
6880        coordination.publish_commit(WalCommitState {
6881            max_frame: 12,
6882            last_checksum: (144, 233),
6883            transaction_count: 12,
6884        });
6885        let published = coordination.load_snapshot();
6886        assert_eq!(published.max_frame, 12);
6887        assert_eq!(published.last_checksum, (144, 233));
6888        assert_eq!(published.transaction_count, 12);
6889        assert_eq!(published.nbackfills, snapshot.nbackfills);
6890        assert_eq!(published.checkpoint_seq, snapshot.checkpoint_seq);
6891    }
6892
6893    #[test]
6894    fn test_in_process_coordination_publishes_checkpoint_and_restart_state() {
6895        let (shared, _wal) = make_test_wal();
6896        let coordination = make_test_coordination(&shared);
6897        let io = PlatformIO::new().unwrap();
6898        let snapshot = WalSnapshot {
6899            max_frame: 9,
6900            nbackfills: 3,
6901            last_checksum: (55, 89),
6902            checkpoint_seq: 7,
6903            transaction_count: 11,
6904        };
6905        set_shared_snapshot(&shared, snapshot);
6906        {
6907            let guard = shared.write();
6908            let mut header = guard.metadata.wal_header.lock();
6909            header.page_size = 4096;
6910            header.checksum_1 = 144;
6911            header.checksum_2 = 233;
6912            guard.metadata.initialized.store(true, Ordering::Release);
6913            guard.runtime.epoch.store(5, Ordering::Release);
6914            guard
6915                .runtime
6916                .frame_cache
6917                .lock()
6918                .extend([(1, vec![1, 4, 8]), (2, vec![2, 6])]);
6919        }
6920
6921        coordination.publish_backfill(8);
6922        assert_eq!(coordination.load_snapshot().nbackfills, 8);
6923        assert_eq!(coordination.bump_checkpoint_epoch(), 5);
6924        assert_eq!(coordination.checkpoint_epoch(), 6);
6925
6926        assert!(coordination.try_read_mark_exclusive(0));
6927        let restarted = coordination.begin_restart(&io).unwrap();
6928        coordination.end_restart();
6929        coordination.unlock_read_mark(0);
6930
6931        assert_eq!(restarted.max_frame, 0);
6932        assert_eq!(restarted.nbackfills, 0);
6933        assert_eq!(restarted.last_checksum, (144, 233));
6934        assert_eq!(restarted.checkpoint_seq, 8);
6935        assert_eq!(restarted.transaction_count, 11);
6936
6937        let guard = shared.read();
6938        assert_eq!(guard.runtime.read_locks[0].get_value(), 0);
6939        assert_eq!(guard.runtime.read_locks[1].get_value(), 0);
6940        for lock in &guard.runtime.read_locks[2..] {
6941            assert_eq!(lock.get_value(), READMARK_NOT_USED);
6942        }
6943        assert!(guard.runtime.frame_cache.lock().is_empty());
6944        assert!(!guard.metadata.initialized.load(Ordering::Acquire));
6945    }
6946
6947    #[test]
6948    fn test_in_process_coordination_manages_frame_cache() {
6949        let (shared, _wal) = make_test_wal();
6950        let coordination = make_test_coordination(&shared);
6951
6952        // Frames are cached in WAL append order (globally ascending): page 7 at
6953        // frame 2, page 9 at frame 4, page 7 again at frame 5.
6954        coordination.cache_frame(7, 2);
6955        coordination.cache_frame(9, 4);
6956        coordination.cache_frame(7, 5);
6957
6958        assert_eq!(coordination.find_frame(7, 0, 5, None), Some(5));
6959        assert_eq!(coordination.iter_latest_frames(0, 5), vec![(7, 5), (9, 4)]);
6960
6961        coordination.rollback_cache(4);
6962
6963        assert_eq!(coordination.find_frame(7, 0, 5, None), Some(2));
6964        assert_eq!(coordination.iter_latest_frames(0, 5), vec![(7, 2), (9, 4)]);
6965        assert_eq!(
6966            shared.read().runtime.frame_cache.lock().get(&7),
6967            Some(&vec![2])
6968        );
6969    }
6970
6971    /// Regression test for WAL frame-index aliasing corruption: when a WAL
6972    /// frame slot is reused for a different page (the append position rewinds
6973    /// to an already-cached frame — e.g. an aborted/uncommitted append's slots
6974    /// being overwritten, or a different connection reusing the slots), the
6975    /// stale `page -> frame` mapping for that slot must be purged. Otherwise
6976    /// `find_frame` can hand a page a frame number whose slot now physically
6977    /// holds a different page, and the reader gets the wrong page's bytes
6978    /// (surfacing as "non-index page" / "Invalid page type" / corruption).
6979    #[test]
6980    fn cache_frame_purges_stale_mapping_on_frame_slot_reuse() {
6981        let (shared, _wal) = make_test_wal();
6982        let coordination = make_test_coordination(&shared);
6983
6984        // Ascending append: page 7 @3, page 9 @4, page 7 @5.
6985        coordination.cache_frame(7, 3);
6986        coordination.cache_frame(9, 4);
6987        coordination.cache_frame(7, 5);
6988        assert_eq!(coordination.find_frame(9, 0, 10, None), Some(4));
6989
6990        // The append position rewinds and frame slots 4 and 5 are overwritten,
6991        // now belonging to page 11 (@4) and page 13 (@5). The earlier owners of
6992        // those slots (page 9 @4, page 7 @5) must no longer be reachable.
6993        coordination.cache_frame(11, 4);
6994        coordination.cache_frame(13, 5);
6995
6996        assert_eq!(
6997            coordination.find_frame(9, 0, 10, None),
6998            None,
6999            "stale page 9 -> frame 4 mapping must be purged once slot 4 is reused"
7000        );
7001        assert_eq!(
7002            coordination.find_frame(11, 0, 10, None),
7003            Some(4),
7004            "page 11 now owns frame slot 4"
7005        );
7006        assert_eq!(
7007            coordination.find_frame(13, 0, 10, None),
7008            Some(5),
7009            "page 13 now owns frame slot 5"
7010        );
7011        // Page 7's still-valid lower frame (3) survives; its stale 5 is gone.
7012        assert_eq!(coordination.find_frame(7, 0, 10, None), Some(3));
7013    }
7014
7015    #[test]
7016    fn test_savepoint_rollback_discards_frame_cache_past_rollback_point() {
7017        let (shared, wal) = make_test_wal();
7018        let coordination = make_test_coordination(&shared);
7019        set_shared_snapshot(
7020            &shared,
7021            WalSnapshot {
7022                max_frame: 25,
7023                nbackfills: 0,
7024                last_checksum: (55, 89),
7025                checkpoint_seq: 1,
7026                transaction_count: 3,
7027            },
7028        );
7029
7030        // The connection spilled uncommitted frames past the committed
7031        // high-water mark (25); a savepoint opened mid-transaction recorded
7032        // frame 27.
7033        coordination.cache_frame(7, 10);
7034        coordination.cache_frame(9, 26);
7035        coordination.cache_frame(11, 28);
7036        wal.max_frame.store(30, Ordering::Release);
7037
7038        wal.rollback(Some(RollbackTo {
7039            frame: 27,
7040            checksum: (13, 21),
7041            checkpoint_seq: 1,
7042        }));
7043
7044        // Mappings at or below the rollback point survive, later ones are
7045        // discarded; frames 26..=27 remain as unpublished spills.
7046        assert_eq!(coordination.find_frame(7, 0, 30, None), Some(10));
7047        assert_eq!(coordination.find_frame(9, 0, 30, None), Some(26));
7048        assert_eq!(coordination.find_frame(11, 0, 30, None), None);
7049        assert_eq!(wal.get_max_frame(), 27);
7050        assert_eq!(*wal.last_checksum.read(), (13, 21));
7051
7052        // Rolling back to the committed high-water mark discards every
7053        // spill.
7054        wal.rollback(Some(RollbackTo {
7055            frame: 25,
7056            checksum: (55, 89),
7057            checkpoint_seq: 1,
7058        }));
7059        assert_eq!(coordination.find_frame(9, 0, 30, None), None);
7060        assert_eq!(wal.get_max_frame(), 25);
7061    }
7062
7063    #[test]
7064    fn test_in_process_coordination_transaction_guards() {
7065        let (shared, _wal) = make_test_wal();
7066        let coordination = make_test_coordination(&shared);
7067
7068        let db_file_snapshot = WalSnapshot {
7069            max_frame: 0,
7070            nbackfills: 0,
7071            last_checksum: (0, 0),
7072            checkpoint_seq: 0,
7073            transaction_count: 0,
7074        };
7075        set_shared_snapshot(&shared, db_file_snapshot);
7076        let read_guard = coordination.try_begin_read_tx(db_file_snapshot).unwrap();
7077        assert_eq!(read_guard, ReadGuardKind::DbFile);
7078        coordination.end_read_tx(read_guard);
7079
7080        let wal_snapshot = WalSnapshot {
7081            max_frame: 5,
7082            nbackfills: 2,
7083            last_checksum: (11, 13),
7084            checkpoint_seq: 1,
7085            transaction_count: 2,
7086        };
7087        set_shared_snapshot(&shared, wal_snapshot);
7088        let read_guard = coordination.try_begin_read_tx(wal_snapshot).unwrap();
7089        assert!(matches!(read_guard, ReadGuardKind::ReadMark(_)));
7090        coordination.end_read_tx(read_guard);
7091
7092        assert!(coordination.try_begin_write_tx());
7093        assert!(!coordination.try_begin_write_tx());
7094        coordination.end_write_tx();
7095    }
7096
7097    #[cfg(host_shared_wal)]
7098    #[test]
7099    #[cfg_attr(
7100        windows,
7101        ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
7102    )]
7103    fn test_shm_coordination_uses_shared_authority() {
7104        let dir = tempfile::tempdir().unwrap();
7105        let wal_path = dir.path().join("test.db-wal");
7106        let shm_path = dir.path().join("test.db-tshm");
7107        let io = shared_wal_test_io();
7108        let file_a = io
7109            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7110            .unwrap();
7111        let file_b = io
7112            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7113            .unwrap();
7114        let shared_a = WalFileShared::new_shared(file_a).unwrap();
7115        let shared_b = WalFileShared::new_shared(file_b).unwrap();
7116        let snapshot = WalSnapshot {
7117            max_frame: 14,
7118            nbackfills: 8,
7119            last_checksum: (31, 37),
7120            checkpoint_seq: 5,
7121            transaction_count: 9,
7122        };
7123        set_shared_snapshot(&shared_a, snapshot);
7124        {
7125            let shared = shared_a.write();
7126            let mut header = shared.metadata.wal_header.lock();
7127            header.page_size = 4096;
7128            header.salt_1 = 17;
7129            header.salt_2 = 23;
7130            header.checksum_1 = snapshot.last_checksum.0;
7131            header.checksum_2 = snapshot.last_checksum.1;
7132        }
7133
7134        let (_authority_a, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
7135        let (authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
7136        coordination_a.cache_frame(7, 2);
7137        coordination_a.cache_frame(9, 4);
7138        coordination_a.cache_frame(7, 5);
7139
7140        assert_eq!(coordination_b.load_snapshot(), snapshot);
7141        assert_eq!(coordination_b.wal_header().page_size, 4096);
7142        assert_eq!(coordination_b.wal_header().salt_1, 17);
7143        assert_eq!(coordination_b.wal_header().salt_2, 23);
7144        assert_eq!(coordination_b.find_frame(7, 0, 5, None), Some(5));
7145        assert_eq!(
7146            coordination_b.iter_latest_frames(0, 5),
7147            vec![(7, 5), (9, 4)]
7148        );
7149        assert_eq!(coordination_a.checkpoint_epoch(), 0);
7150        assert_eq!(coordination_b.bump_checkpoint_epoch(), 0);
7151        assert_eq!(coordination_a.checkpoint_epoch(), 1);
7152
7153        assert!(coordination_a.try_begin_write_tx());
7154        assert!(!coordination_b.try_begin_write_tx());
7155        coordination_a.end_write_tx();
7156
7157        let read_guard = coordination_a.try_begin_read_tx(snapshot).unwrap();
7158        assert_eq!(
7159            authority_b.min_active_reader_frame(),
7160            Some(snapshot.max_frame)
7161        );
7162        coordination_a.end_read_tx(read_guard);
7163        assert_eq!(authority_b.min_active_reader_frame(), None);
7164
7165        coordination_b.publish_commit(WalCommitState {
7166            max_frame: 21,
7167            last_checksum: (55, 89),
7168            transaction_count: 10,
7169        });
7170        assert_eq!(
7171            coordination_a.load_snapshot(),
7172            WalSnapshot {
7173                max_frame: 21,
7174                nbackfills: 8,
7175                last_checksum: (55, 89),
7176                checkpoint_seq: 5,
7177                transaction_count: 10,
7178            }
7179        );
7180
7181        coordination_b.rollback_cache(4);
7182        assert_eq!(coordination_a.find_frame(7, 0, 5, None), Some(2));
7183        assert_eq!(
7184            coordination_a.iter_latest_frames(0, 5),
7185            vec![(7, 2), (9, 4)]
7186        );
7187        assert!(shm_path.exists());
7188    }
7189
7190    #[cfg(host_shared_wal)]
7191    #[test]
7192    fn test_shm_coordination_many_same_snapshot_readers_share_one_published_slot() {
7193        let dir = tempfile::tempdir().unwrap();
7194        let wal_path = dir.path().join("test-many-same-snapshot-readers.db-wal");
7195        let shm_path = dir.path().join("test-many-same-snapshot-readers.db-tshm");
7196        let io = shared_wal_test_io();
7197        let file = io
7198            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7199            .unwrap();
7200        let shared = WalFileShared::new_shared(file).unwrap();
7201        let snapshot = WalSnapshot {
7202            max_frame: 9,
7203            nbackfills: 2,
7204            last_checksum: (31, 37),
7205            checkpoint_seq: 5,
7206            transaction_count: 9,
7207        };
7208        set_shared_snapshot(&shared, snapshot);
7209        {
7210            let shared = shared.write();
7211            let mut header = shared.metadata.wal_header.lock();
7212            header.page_size = 4096;
7213            header.salt_1 = 17;
7214            header.salt_2 = 23;
7215            header.checksum_1 = snapshot.last_checksum.0;
7216            header.checksum_2 = snapshot.last_checksum.1;
7217        }
7218
7219        let authority =
7220            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7221        let mut readers = Vec::new();
7222        for _ in 0..128 {
7223            let coordination = ShmWalCoordination::new(shared.clone(), authority.clone());
7224            let read_guard = coordination
7225                .try_begin_read_tx(snapshot)
7226                .expect("same-snapshot readers should share a published reader barrier");
7227            readers.push((coordination, read_guard));
7228        }
7229
7230        assert_eq!(
7231            authority.min_active_reader_frame(),
7232            Some(snapshot.max_frame)
7233        );
7234        assert_eq!(
7235            active_shared_reader_slot_count(&authority),
7236            1,
7237            "same-snapshot readers should collapse onto one shared reader slot"
7238        );
7239
7240        for (coordination, read_guard) in readers {
7241            coordination.end_read_tx(read_guard);
7242        }
7243        assert_eq!(authority.min_active_reader_frame(), None);
7244        assert_eq!(active_shared_reader_slot_count(&authority), 0);
7245    }
7246
7247    #[cfg(host_shared_wal)]
7248    #[test]
7249    fn test_shm_coordination_uses_one_published_slot_per_active_snapshot_generation() {
7250        let dir = tempfile::tempdir().unwrap();
7251        let wal_path = dir.path().join("test-mixed-snapshot-readers.db-wal");
7252        let shm_path = dir.path().join("test-mixed-snapshot-readers.db-tshm");
7253        let io = shared_wal_test_io();
7254        let file = io
7255            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7256            .unwrap();
7257        let shared = WalFileShared::new_shared(file).unwrap();
7258        let snapshot_a = WalSnapshot {
7259            max_frame: 5,
7260            nbackfills: 2,
7261            last_checksum: (31, 37),
7262            checkpoint_seq: 5,
7263            transaction_count: 9,
7264        };
7265        set_shared_snapshot(&shared, snapshot_a);
7266        {
7267            let shared = shared.write();
7268            let mut header = shared.metadata.wal_header.lock();
7269            header.page_size = 4096;
7270            header.salt_1 = 17;
7271            header.salt_2 = 23;
7272            header.checksum_1 = snapshot_a.last_checksum.0;
7273            header.checksum_2 = snapshot_a.last_checksum.1;
7274        }
7275
7276        let authority =
7277            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7278        let reader_a1 = ShmWalCoordination::new(shared.clone(), authority.clone());
7279        let guard_a1 = reader_a1.try_begin_read_tx(snapshot_a).unwrap();
7280        let reader_a2 = ShmWalCoordination::new(shared.clone(), authority.clone());
7281        let guard_a2 = reader_a2.try_begin_read_tx(snapshot_a).unwrap();
7282        assert_eq!(
7283            authority.min_active_reader_frame(),
7284            Some(snapshot_a.max_frame)
7285        );
7286        assert_eq!(active_shared_reader_slot_count(&authority), 1);
7287
7288        let snapshot_b = WalSnapshot {
7289            max_frame: 9,
7290            nbackfills: 2,
7291            last_checksum: (41, 43),
7292            checkpoint_seq: 5,
7293            transaction_count: 10,
7294        };
7295        reader_a1.publish_commit(WalCommitState {
7296            max_frame: snapshot_b.max_frame,
7297            last_checksum: snapshot_b.last_checksum,
7298            transaction_count: snapshot_b.transaction_count,
7299        });
7300
7301        let reader_b1 = ShmWalCoordination::new(shared.clone(), authority.clone());
7302        let guard_b1 = reader_b1.try_begin_read_tx(snapshot_b).unwrap();
7303        let reader_b2 = ShmWalCoordination::new(shared, authority.clone());
7304        let guard_b2 = reader_b2.try_begin_read_tx(snapshot_b).unwrap();
7305
7306        assert_eq!(
7307            active_shared_reader_slot_count(&authority),
7308            2,
7309            "distinct live snapshots should each publish one shared reader slot"
7310        );
7311        assert_eq!(
7312            authority.min_active_reader_frame(),
7313            Some(snapshot_a.max_frame),
7314            "checkpoint barrier should stay pinned to the oldest active snapshot"
7315        );
7316
7317        reader_a1.end_read_tx(guard_a1);
7318        reader_a2.end_read_tx(guard_a2);
7319        assert_eq!(
7320            authority.min_active_reader_frame(),
7321            Some(snapshot_b.max_frame)
7322        );
7323        assert_eq!(active_shared_reader_slot_count(&authority), 1);
7324
7325        reader_b1.end_read_tx(guard_b1);
7326        reader_b2.end_read_tx(guard_b2);
7327        assert_eq!(authority.min_active_reader_frame(), None);
7328        assert_eq!(active_shared_reader_slot_count(&authority), 0);
7329    }
7330
7331    #[cfg(host_shared_wal)]
7332    #[test]
7333    #[cfg_attr(
7334        windows,
7335        ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
7336    )]
7337    fn test_shm_coordination_shared_index_grows_past_old_fixed_limit() {
7338        const OLD_FIXED_LIMIT: u64 = 65_536;
7339
7340        let dir = tempfile::tempdir().unwrap();
7341        let wal_path = dir.path().join("test.db-wal");
7342        let shm_path = dir.path().join("test.db-tshm");
7343        let io = shared_wal_test_io();
7344        let file_a = io
7345            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7346            .unwrap();
7347        let file_b = io
7348            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7349            .unwrap();
7350        let shared_a = WalFileShared::new_shared(file_a).unwrap();
7351        let shared_b = WalFileShared::new_shared(file_b).unwrap();
7352        let snapshot = WalSnapshot {
7353            max_frame: OLD_FIXED_LIMIT + 2,
7354            nbackfills: 0,
7355            last_checksum: (31, 37),
7356            checkpoint_seq: 5,
7357            transaction_count: OLD_FIXED_LIMIT + 2,
7358        };
7359        set_shared_snapshot(&shared_a, snapshot);
7360        {
7361            let shared = shared_a.write();
7362            let mut header = shared.metadata.wal_header.lock();
7363            header.page_size = 4096;
7364            header.salt_1 = 17;
7365            header.salt_2 = 23;
7366            header.checksum_1 = snapshot.last_checksum.0;
7367            header.checksum_2 = snapshot.last_checksum.1;
7368        }
7369
7370        let (_authority_a, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
7371        let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
7372
7373        coordination_a.cache_frame(7, 2);
7374        for frame_id in 3..=OLD_FIXED_LIMIT + 1 {
7375            coordination_a.cache_frame(100 + (frame_id % 31), frame_id);
7376        }
7377        coordination_a.cache_frame(7, OLD_FIXED_LIMIT + 2);
7378
7379        assert_eq!(
7380            coordination_b.find_frame(7, 0, OLD_FIXED_LIMIT + 2, None),
7381            Some(OLD_FIXED_LIMIT + 2)
7382        );
7383        assert_eq!(
7384            coordination_b.find_frame(7, 0, OLD_FIXED_LIMIT + 2, Some(OLD_FIXED_LIMIT + 1)),
7385            Some(2)
7386        );
7387        assert!(coordination_b
7388            .iter_latest_frames(0, OLD_FIXED_LIMIT + 2)
7389            .contains(&(7, OLD_FIXED_LIMIT + 2)));
7390    }
7391
7392    #[cfg(host_shared_wal)]
7393    #[test]
7394    fn test_shm_coordination_restart_uses_authority_snapshot() {
7395        let dir = tempfile::tempdir().unwrap();
7396        let wal_path = dir.path().join("test.db-wal");
7397        let shm_path = dir.path().join("test.db-tshm");
7398        let io = PlatformIO::new().unwrap();
7399        let file_a = io
7400            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7401            .unwrap();
7402        let file_b = io
7403            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7404            .unwrap();
7405        let shared_a = WalFileShared::new_shared(file_a).unwrap();
7406        let shared_b = WalFileShared::new_shared(file_b).unwrap();
7407        let snapshot = WalSnapshot {
7408            max_frame: 12,
7409            nbackfills: 12,
7410            last_checksum: (31, 37),
7411            checkpoint_seq: 5,
7412            transaction_count: 9,
7413        };
7414        set_shared_snapshot(&shared_a, snapshot);
7415        {
7416            let shared = shared_a.write();
7417            let mut header = shared.metadata.wal_header.lock();
7418            header.page_size = 4096;
7419            header.salt_1 = 17;
7420            header.salt_2 = 23;
7421            header.checksum_1 = snapshot.last_checksum.0;
7422            header.checksum_2 = snapshot.last_checksum.1;
7423            shared.metadata.initialized.store(true, Ordering::Release);
7424            shared.runtime.epoch.store(5, Ordering::Release);
7425        }
7426
7427        let (_authority_a, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
7428        let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
7429        coordination_a.cache_frame(7, 2);
7430        coordination_a.cache_frame(9, 4);
7431
7432        {
7433            let mut shared = shared_b.write();
7434            shared.metadata.max_frame.store(99, Ordering::Release);
7435            shared.metadata.nbackfills.store(77, Ordering::Release);
7436            shared.metadata.last_checksum = (1, 2);
7437            shared
7438                .metadata
7439                .transaction_count
7440                .store(42, Ordering::Release);
7441            shared.runtime.epoch.store(99, Ordering::Release);
7442            shared.metadata.initialized.store(true, Ordering::Release);
7443            let mut header = shared.metadata.wal_header.lock();
7444            header.checkpoint_seq = 88;
7445            header.page_size = 2048;
7446            header.salt_1 = 91;
7447            header.salt_2 = 92;
7448            header.checksum_1 = 93;
7449            header.checksum_2 = 94;
7450        }
7451
7452        assert!(coordination_b.fallback.try_read_mark_exclusive(0));
7453        let restarted = coordination_b.begin_restart(&io).unwrap();
7454        coordination_b.end_restart();
7455        coordination_b.fallback.unlock_read_mark(0);
7456
7457        assert_eq!(
7458            restarted,
7459            WalSnapshot {
7460                max_frame: 0,
7461                nbackfills: 0,
7462                last_checksum: snapshot.last_checksum,
7463                checkpoint_seq: snapshot.checkpoint_seq.wrapping_add(1),
7464                transaction_count: snapshot.transaction_count,
7465            }
7466        );
7467        assert_eq!(
7468            coordination_a.load_snapshot(),
7469            WalSnapshot {
7470                max_frame: 0,
7471                nbackfills: 0,
7472                last_checksum: snapshot.last_checksum,
7473                checkpoint_seq: snapshot.checkpoint_seq.wrapping_add(1),
7474                transaction_count: snapshot.transaction_count,
7475            }
7476        );
7477        let header = coordination_a.wal_header();
7478        assert_eq!(header.page_size, 4096);
7479        assert_eq!(
7480            header.checkpoint_seq,
7481            snapshot.checkpoint_seq.wrapping_add(1)
7482        );
7483        assert_eq!(header.salt_1, 18);
7484        assert_ne!(header.salt_2, 23);
7485        assert_eq!(header.checksum_1, snapshot.last_checksum.0);
7486        assert_eq!(header.checksum_2, snapshot.last_checksum.1);
7487        assert_eq!(coordination_a.iter_latest_frames(0, u64::MAX), Vec::new());
7488        assert_eq!(coordination_a.checkpoint_epoch(), 5);
7489
7490        let shared = shared_b.read();
7491        assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 0);
7492        assert_eq!(shared.metadata.nbackfills.load(Ordering::Acquire), 0);
7493        assert_eq!(shared.metadata.last_checksum, snapshot.last_checksum);
7494        assert_eq!(
7495            shared.metadata.transaction_count.load(Ordering::Acquire),
7496            snapshot.transaction_count
7497        );
7498        assert_eq!(shared.runtime.epoch.load(Ordering::Acquire), 5);
7499        assert!(!shared.metadata.initialized.load(Ordering::Acquire));
7500        assert!(shared.runtime.frame_cache.lock().is_empty());
7501    }
7502
7503    #[cfg(host_shared_wal)]
7504    #[test]
7505    fn test_shm_coordination_exclusive_reopen_reuses_persisted_authority() {
7506        let dir = tempfile::tempdir().unwrap();
7507        let wal_path = dir.path().join("test.db-wal");
7508        let shm_path = dir.path().join("test.db-tshm");
7509        let io = shared_wal_test_io();
7510
7511        {
7512            let file = io
7513                .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7514                .unwrap();
7515            let shared = WalFileShared::new_shared(file).unwrap();
7516            let snapshot = WalSnapshot {
7517                max_frame: 12,
7518                nbackfills: 8,
7519                last_checksum: (31, 37),
7520                checkpoint_seq: 5,
7521                transaction_count: 9,
7522            };
7523            set_shared_snapshot(&shared, snapshot);
7524            {
7525                let shared = shared.write();
7526                let mut header = shared.metadata.wal_header.lock();
7527                header.page_size = 4096;
7528                header.salt_1 = 17;
7529                header.salt_2 = 23;
7530                header.checksum_1 = snapshot.last_checksum.0;
7531                header.checksum_2 = snapshot.last_checksum.1;
7532            }
7533
7534            let (authority, coordination) = make_test_shm_coordination(&shared, &shm_path);
7535            coordination.cache_frame(7, 2);
7536            coordination.cache_frame(7, 5);
7537            assert_eq!(
7538                authority.open_mode(),
7539                SharedWalCoordinationOpenMode::Exclusive
7540            );
7541            assert_eq!(coordination.load_snapshot(), snapshot);
7542            assert_eq!(coordination.find_frame(7, 0, 5, None), Some(5));
7543        }
7544
7545        let reopened_file = io
7546            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7547            .unwrap();
7548        let reopened_shared = WalFileShared::new_shared(reopened_file).unwrap();
7549        let (reopened_authority, reopened_coordination) =
7550            make_test_shm_coordination(&reopened_shared, &shm_path);
7551
7552        assert_eq!(
7553            reopened_authority.open_mode(),
7554            SharedWalCoordinationOpenMode::Exclusive
7555        );
7556        assert_eq!(
7557            reopened_coordination.load_snapshot(),
7558            WalSnapshot {
7559                max_frame: 12,
7560                nbackfills: 8,
7561                last_checksum: (31, 37),
7562                checkpoint_seq: 5,
7563                transaction_count: 9,
7564            }
7565        );
7566        assert_eq!(
7567            reopened_coordination.iter_latest_frames(0, u64::MAX),
7568            vec![(7, 5)]
7569        );
7570        assert_eq!(reopened_authority.min_active_reader_frame(), None);
7571    }
7572
7573    #[cfg(host_shared_wal)]
7574    #[test]
7575    fn test_open_shared_from_authority_reuses_trusted_snapshot_after_exclusive_reopen() {
7576        let dir = tempfile::tempdir().unwrap();
7577        let wal_path = dir.path().join("test.db-wal");
7578        let shm_path = dir.path().join("test.db-tshm");
7579        let io = shared_wal_test_io();
7580        let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7581        {
7582            let authority =
7583                Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7584            authority.install_snapshot(snapshot);
7585            authority.record_frame(7, 1);
7586        }
7587        let reopened_authority =
7588            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7589        assert_eq!(
7590            reopened_authority.open_mode(),
7591            SharedWalCoordinationOpenMode::Exclusive
7592        );
7593
7594        let shared = WalFileShared::open_shared_from_authority_if_exists(
7595            &io,
7596            wal_path.to_str().unwrap(),
7597            crate::OpenFlags::Create,
7598            &reopened_authority,
7599            &open_test_db_file_for_wal(&io, &wal_path),
7600        )
7601        .unwrap();
7602
7603        let shared = shared.read();
7604        assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 1);
7605        assert_eq!(shared.metadata.nbackfills.load(Ordering::Acquire), 0);
7606        assert_eq!(
7607            shared.metadata.transaction_count.load(Ordering::Acquire),
7608            snapshot.transaction_count
7609        );
7610        assert_eq!(
7611            shared.metadata.last_checksum,
7612            (snapshot.checksum_1, snapshot.checksum_2)
7613        );
7614        assert_eq!(
7615            shared.runtime.epoch.load(Ordering::Acquire),
7616            snapshot.checkpoint_epoch
7617        );
7618        assert!(shared.metadata.initialized.load(Ordering::Acquire));
7619        assert!(!shared
7620            .metadata
7621            .loaded_from_disk_scan
7622            .load(Ordering::Acquire));
7623        assert!(shared.runtime.frame_cache.lock().is_empty());
7624    }
7625
7626    #[cfg(host_shared_wal)]
7627    #[test]
7628    fn test_shm_coordination_live_overflow_returns_busy_without_runtime_disk_scan() {
7629        let dir = tempfile::tempdir().unwrap();
7630        let wal_path = dir.path().join("test-live-overflow.db-wal");
7631        let shm_path = dir.path().join("test-live-overflow.db-tshm");
7632        let io = shared_wal_test_io();
7633        let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7634        let authority =
7635            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7636        authority.install_snapshot(snapshot);
7637        authority.record_frame(7, 1);
7638
7639        let reopened_authority =
7640            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7641        let shared = WalFileShared::open_shared_from_authority_if_exists(
7642            &io,
7643            wal_path.to_str().unwrap(),
7644            crate::OpenFlags::Create,
7645            &reopened_authority,
7646            &open_test_db_file_for_wal(&io, &wal_path),
7647        )
7648        .unwrap();
7649        assert!(shared.read().runtime.frame_cache.lock().is_empty());
7650
7651        let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
7652        buffer_pool.finalize_with_page_size(4096).unwrap();
7653        let wal = WalFile::new_with_shared_coordination(
7654            io.clone(),
7655            shared.clone(),
7656            reopened_authority.clone(),
7657            ((0, 0), 0),
7658            buffer_pool,
7659        );
7660
7661        wal.begin_read_tx().unwrap();
7662        reopened_authority.mark_frame_index_overflowed_for_tests();
7663
7664        assert!(
7665            matches!(wal.find_frame(7, None), Err(LimboError::Busy)),
7666            "page lookup must refuse the overflowed path instead of rescanning the WAL synchronously"
7667        );
7668        assert!(
7669            shared.read().runtime.frame_cache.lock().is_empty(),
7670            "refusing the overflow refresh must leave the local fallback cache untouched"
7671        );
7672
7673        wal.end_read_tx();
7674        assert!(
7675            matches!(wal.begin_read_tx(), Err(LimboError::Busy)),
7676            "new readers must also refuse an uncovered overflowed frame index without blocking"
7677        );
7678    }
7679
7680    #[cfg(host_shared_wal)]
7681    #[test]
7682    fn test_open_shared_from_authority_exclusive_rebuilds_positive_snapshot_from_disk() {
7683        let dir = tempfile::tempdir().unwrap();
7684        let wal_path = dir.path().join("test-exclusive-positive.db-wal");
7685        let shm_path = dir.path().join("test-exclusive-positive.db-tshm");
7686        let io = shared_wal_test_io();
7687        let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7688        {
7689            let authority =
7690                Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7691            authority.install_snapshot(SharedWalCoordinationHeader {
7692                nbackfills: snapshot.max_frame,
7693                ..snapshot
7694            });
7695            authority.record_frame(7, 1);
7696            assert_eq!(
7697                authority.open_mode(),
7698                SharedWalCoordinationOpenMode::Exclusive
7699            );
7700        }
7701
7702        let reopened_authority =
7703            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7704        assert_eq!(
7705            reopened_authority.open_mode(),
7706            SharedWalCoordinationOpenMode::Exclusive
7707        );
7708
7709        let shared = WalFileShared::open_shared_from_authority_if_exists(
7710            &io,
7711            wal_path.to_str().unwrap(),
7712            crate::OpenFlags::Create,
7713            &reopened_authority,
7714            &open_test_db_file_for_wal(&io, &wal_path),
7715        )
7716        .unwrap();
7717
7718        let shared = shared.read();
7719        assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 1);
7720        assert_eq!(shared.metadata.nbackfills.load(Ordering::Acquire), 0);
7721        assert!(shared
7722            .metadata
7723            .loaded_from_disk_scan
7724            .load(Ordering::Acquire));
7725        assert_eq!(
7726            shared.runtime.frame_cache.lock().get(&7).cloned(),
7727            Some(vec![1])
7728        );
7729    }
7730
7731    #[cfg(host_shared_wal)]
7732    #[test]
7733    fn test_shared_coordination_open_uses_reconciled_snapshot_for_local_wal_state() {
7734        let dir = tempfile::tempdir().unwrap();
7735        let wal_path = dir.path().join("test.db-wal");
7736        let shm_path = dir.path().join("test.db-tshm");
7737        let io = shared_wal_test_io();
7738
7739        let file = io
7740            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
7741            .unwrap();
7742        let shared = WalFileShared::new_shared(file).unwrap();
7743
7744        let authority =
7745            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7746        let snapshot = SharedWalCoordinationHeader {
7747            max_frame: 1,
7748            nbackfills: 0,
7749            transaction_count: 9,
7750            visibility_generation: 3,
7751            checkpoint_seq: 5,
7752            checkpoint_epoch: 7,
7753            page_size: 4096,
7754            salt_1: 17,
7755            salt_2: 23,
7756            checksum_1: 31,
7757            checksum_2: 37,
7758            reader_slot_count: 64,
7759        };
7760        authority.install_snapshot(snapshot);
7761        authority.record_frame(7, 1);
7762
7763        let buffer_pool = BufferPool::begin_init(&io, BufferPool::TEST_ARENA_SIZE);
7764        buffer_pool.finalize_with_page_size(4096).unwrap();
7765        let wal =
7766            WalFile::new_with_shared_coordination(io, shared, authority, ((0, 0), 0), buffer_pool);
7767
7768        assert_eq!(wal.get_max_frame(), 1);
7769        assert_eq!(wal.get_last_checksum(), (31, 37));
7770    }
7771
7772    #[cfg(host_shared_wal)]
7773    #[test]
7774    fn test_open_shared_from_authority_rebuilds_from_disk_when_snapshot_is_stale() {
7775        let dir = tempfile::tempdir().unwrap();
7776        let wal_path = dir.path().join("test-stale.db-wal");
7777        let shm_path = dir.path().join("test-stale.db-tshm");
7778        let io = shared_wal_test_io();
7779        let valid_snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7780        let authority =
7781            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7782        authority.install_snapshot(SharedWalCoordinationHeader {
7783            max_frame: 0,
7784            nbackfills: 0,
7785            transaction_count: 0,
7786            visibility_generation: 0,
7787            checkpoint_seq: valid_snapshot.checkpoint_seq,
7788            checkpoint_epoch: 0,
7789            page_size: valid_snapshot.page_size,
7790            salt_1: valid_snapshot.salt_1,
7791            salt_2: valid_snapshot.salt_2,
7792            checksum_1: 0,
7793            checksum_2: 0,
7794            reader_slot_count: 64,
7795        });
7796
7797        let shared = WalFileShared::open_shared_from_authority_if_exists(
7798            &io,
7799            wal_path.to_str().unwrap(),
7800            crate::OpenFlags::Create,
7801            &authority,
7802            &open_test_db_file_for_wal(&io, &wal_path),
7803        )
7804        .unwrap();
7805
7806        let shared = shared.read();
7807        assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 1);
7808        assert_eq!(
7809            shared.metadata.last_checksum,
7810            (valid_snapshot.checksum_1, valid_snapshot.checksum_2)
7811        );
7812        assert!(shared
7813            .metadata
7814            .loaded_from_disk_scan
7815            .load(Ordering::Acquire));
7816        assert_eq!(
7817            shared.runtime.frame_cache.lock().get(&7).cloned(),
7818            Some(vec![1])
7819        );
7820    }
7821
7822    #[cfg(host_shared_wal)]
7823    #[test]
7824    fn test_open_shared_from_authority_rebuilt_authority_persists_across_exclusive_reopen() {
7825        let dir = tempfile::tempdir().unwrap();
7826        let wal_path = dir.path().join("test-republish.db-wal");
7827        let shm_path = dir.path().join("test-republish.db-tshm");
7828        let io = shared_wal_test_io();
7829        let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7830        let authority =
7831            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7832        authority.install_snapshot(snapshot);
7833
7834        let exclusive = WalFileShared::open_shared_from_authority_if_exists(
7835            &io,
7836            wal_path.to_str().unwrap(),
7837            crate::OpenFlags::Create,
7838            &authority,
7839            &open_test_db_file_for_wal(&io, &wal_path),
7840        )
7841        .unwrap();
7842        assert!(exclusive
7843            .read()
7844            .metadata
7845            .loaded_from_disk_scan
7846            .load(Ordering::Acquire));
7847        assert!(
7848            authority.iter_latest_frames(0, u64::MAX).is_empty(),
7849            "open_shared_from_authority_if_exists should not republish authority before coordination reconciliation"
7850        );
7851
7852        let exclusive_coordination = ShmWalCoordination::new(exclusive, authority.clone());
7853        assert_eq!(authority.iter_latest_frames(0, u64::MAX), vec![(7, 1)]);
7854        assert_eq!(exclusive_coordination.find_frame(7, 0, 1, None), Some(1));
7855
7856        drop(exclusive_coordination);
7857        drop(authority);
7858
7859        let reopened_authority =
7860            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7861        assert_eq!(
7862            reopened_authority.open_mode(),
7863            SharedWalCoordinationOpenMode::Exclusive
7864        );
7865
7866        let reopened_shared = WalFileShared::open_shared_from_authority_if_exists(
7867            &io,
7868            wal_path.to_str().unwrap(),
7869            crate::OpenFlags::Create,
7870            &reopened_authority,
7871            &open_test_db_file_for_wal(&io, &wal_path),
7872        )
7873        .unwrap();
7874        assert!(!reopened_shared
7875            .read()
7876            .metadata
7877            .loaded_from_disk_scan
7878            .load(Ordering::Acquire));
7879        let reopened_coordination = ShmWalCoordination::new(reopened_shared, reopened_authority);
7880        assert_eq!(reopened_coordination.find_frame(7, 0, 1, None), Some(1));
7881    }
7882
7883    #[cfg(host_shared_wal)]
7884    #[test]
7885    fn test_open_shared_from_authority_exclusive_disk_scan_does_not_downgrade_newer_zero_frame_generation(
7886    ) {
7887        let dir = tempfile::tempdir().unwrap();
7888        let wal_path = dir.path().join("test-zero-frame-reopen.db-wal");
7889        let shm_path = dir.path().join("test-zero-frame-reopen.db-tshm");
7890        let io = shared_wal_test_io();
7891        let prior_generation = write_test_wal_with_single_commit_frame(&io, &wal_path);
7892        let authority =
7893            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7894        let restarted_generation = SharedWalCoordinationHeader {
7895            max_frame: 0,
7896            nbackfills: 0,
7897            transaction_count: prior_generation.transaction_count,
7898            visibility_generation: prior_generation.visibility_generation,
7899            checkpoint_seq: prior_generation.checkpoint_seq.wrapping_add(1),
7900            checkpoint_epoch: prior_generation.checkpoint_epoch,
7901            page_size: prior_generation.page_size,
7902            salt_1: prior_generation.salt_1.wrapping_add(1),
7903            salt_2: prior_generation.salt_2.wrapping_add(1),
7904            checksum_1: prior_generation.checksum_1,
7905            checksum_2: prior_generation.checksum_2,
7906            reader_slot_count: prior_generation.reader_slot_count,
7907        };
7908        authority.install_snapshot(restarted_generation);
7909
7910        let shared = WalFileShared::open_shared_from_authority_if_exists(
7911            &io,
7912            wal_path.to_str().unwrap(),
7913            crate::OpenFlags::Create,
7914            &authority,
7915            &open_test_db_file_for_wal(&io, &wal_path),
7916        )
7917        .unwrap();
7918        assert!(shared
7919            .read()
7920            .metadata
7921            .loaded_from_disk_scan
7922            .load(Ordering::Acquire));
7923
7924        let coordination = ShmWalCoordination::new(shared.clone(), authority.clone());
7925        let reopened = coordination.load_snapshot();
7926        assert_eq!(reopened.max_frame, 0);
7927        assert_eq!(reopened.nbackfills, 0);
7928        assert_eq!(reopened.checkpoint_seq, restarted_generation.checkpoint_seq);
7929        assert_eq!(
7930            authority.snapshot().checkpoint_seq,
7931            restarted_generation.checkpoint_seq
7932        );
7933        assert!(
7934            !coordination.wal_is_initialized(),
7935            "preserving a newer zero-frame generation must require the first append to rewrite the WAL header"
7936        );
7937        assert!(
7938            shared.read().runtime.frame_cache.lock().is_empty(),
7939            "older WAL frames from a prior generation must not survive zero-frame authority recovery"
7940        );
7941    }
7942
7943    #[cfg(host_shared_wal)]
7944    #[test]
7945    fn test_open_shared_from_authority_ignores_unpublished_backfill_proof_after_exclusive_reopen() {
7946        let dir = tempfile::tempdir().unwrap();
7947        let wal_path = dir.path().join("test-unpublished-proof.db-wal");
7948        let shm_path = dir.path().join("test-unpublished-proof.db-tshm");
7949        let io = shared_wal_test_io();
7950        let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
7951        {
7952            let authority =
7953                Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7954            authority.install_snapshot(snapshot);
7955            authority.install_backfill_proof(
7956                SharedWalCoordinationHeader {
7957                    nbackfills: snapshot.max_frame,
7958                    ..snapshot
7959                },
7960                11,
7961                0xAABB_CCDD,
7962            );
7963        }
7964        let reopened_authority =
7965            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
7966        assert_eq!(
7967            reopened_authority.open_mode(),
7968            SharedWalCoordinationOpenMode::Exclusive
7969        );
7970
7971        let shared = WalFileShared::open_shared_from_authority_if_exists(
7972            &io,
7973            wal_path.to_str().unwrap(),
7974            crate::OpenFlags::Create,
7975            &reopened_authority,
7976            &open_test_db_file_for_wal(&io, &wal_path),
7977        )
7978        .unwrap();
7979
7980        let shared = shared.read();
7981        assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 1);
7982        assert_eq!(shared.metadata.nbackfills.load(Ordering::Acquire), 0);
7983        assert!(shared
7984            .metadata
7985            .loaded_from_disk_scan
7986            .load(Ordering::Acquire));
7987    }
7988
7989    #[cfg(host_shared_wal)]
7990    #[test]
7991    fn test_restart_checkpoint_clears_backfill_proof_and_later_replaces_it() {
7992        let (db, path) = get_database();
7993        let wal_path = path.join("test.db-wal");
7994        let wal_path_str = wal_path.to_str().unwrap();
7995        let conn = db.connect().unwrap();
7996        conn.wal_auto_actions_disable();
7997        conn.execute("create table test(id integer primary key, value text)")
7998            .unwrap();
7999        bulk_inserts(&conn, 8, 2);
8000
8001        let pager = conn.pager.load();
8002        let partial = run_checkpoint_until_done(
8003            &pager,
8004            CheckpointMode::Passive {
8005                upper_bound_inclusive: Some(1),
8006            },
8007        );
8008        assert!(
8009            partial.wal_total_backfilled > 0 && !partial.everything_backfilled(),
8010            "setup must create a partial checkpoint with a positive durable backfill proof"
8011        );
8012
8013        let authority = db.shared_wal_coordination().unwrap().unwrap();
8014        let snapshot_before_restart = authority.snapshot();
8015        let (db_size_before, db_crc_before) =
8016            super::read_database_identity_from_file_path(&db.io, wal_path_str)
8017                .unwrap()
8018                .unwrap();
8019        assert!(
8020            authority.validate_backfill_proof(
8021                snapshot_before_restart,
8022                db_size_before,
8023                db_crc_before
8024            ),
8025            "setup must install a valid proof before RESTART"
8026        );
8027
8028        let restart = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
8029        assert!(
8030            restart.everything_backfilled(),
8031            "RESTART should fully backfill before resetting the WAL generation"
8032        );
8033
8034        let snapshot_after_restart = authority.snapshot();
8035        assert_eq!(snapshot_after_restart.max_frame, 0);
8036        assert_eq!(snapshot_after_restart.nbackfills, 0);
8037        assert!(
8038            !authority.validate_backfill_proof(
8039                snapshot_before_restart,
8040                db_size_before,
8041                db_crc_before
8042            ),
8043            "RESTART must clear the proof for the old WAL generation"
8044        );
8045
8046        bulk_inserts(&conn, 6, 2);
8047        let replacement = run_checkpoint_until_done(
8048            &pager,
8049            CheckpointMode::Passive {
8050                upper_bound_inclusive: Some(1),
8051            },
8052        );
8053        assert!(
8054            replacement.wal_total_backfilled > 0 && !replacement.everything_backfilled(),
8055            "replacement setup must create a new partial checkpoint after RESTART"
8056        );
8057
8058        let snapshot_after_replacement = authority.snapshot();
8059        let (db_size_after, db_crc_after) =
8060            super::read_database_identity_from_file_path(&db.io, wal_path_str)
8061                .unwrap()
8062                .unwrap();
8063        assert!(
8064            authority.validate_backfill_proof(
8065                snapshot_after_replacement,
8066                db_size_after,
8067                db_crc_after
8068            ),
8069            "partial checkpoint after RESTART must install a replacement proof for the new generation"
8070        );
8071        assert_ne!(
8072            snapshot_after_replacement.checkpoint_seq, snapshot_before_restart.checkpoint_seq,
8073            "replacement proof must belong to the restarted WAL generation"
8074        );
8075    }
8076
8077    #[cfg(host_shared_wal)]
8078    #[test]
8079    fn test_truncate_checkpoint_clears_backfill_proof_and_later_replaces_it() {
8080        let (db, path) = get_database();
8081        let wal_path = path.join("test.db-wal");
8082        let wal_path_str = wal_path.to_str().unwrap();
8083        let conn = db.connect().unwrap();
8084        conn.wal_auto_actions_disable();
8085        conn.execute("create table test(id integer primary key, value text)")
8086            .unwrap();
8087        bulk_inserts(&conn, 8, 2);
8088
8089        let pager = conn.pager.load();
8090        let partial = run_checkpoint_until_done(
8091            &pager,
8092            CheckpointMode::Passive {
8093                upper_bound_inclusive: Some(1),
8094            },
8095        );
8096        assert!(
8097            partial.wal_total_backfilled > 0 && !partial.everything_backfilled(),
8098            "setup must create a partial checkpoint with a positive durable backfill proof"
8099        );
8100
8101        let authority = db.shared_wal_coordination().unwrap().unwrap();
8102        let snapshot_before_truncate = authority.snapshot();
8103        let (db_size_before, db_crc_before) =
8104            super::read_database_identity_from_file_path(&db.io, wal_path_str)
8105                .unwrap()
8106                .unwrap();
8107        assert!(
8108            authority.validate_backfill_proof(
8109                snapshot_before_truncate,
8110                db_size_before,
8111                db_crc_before
8112            ),
8113            "setup must install a valid proof before TRUNCATE"
8114        );
8115
8116        let truncate = run_checkpoint_until_done(
8117            &pager,
8118            CheckpointMode::Truncate {
8119                upper_bound_inclusive: None,
8120            },
8121        );
8122        assert!(
8123            truncate.everything_backfilled(),
8124            "TRUNCATE should fully backfill before truncating the WAL"
8125        );
8126
8127        let snapshot_after_truncate = authority.snapshot();
8128        assert_eq!(snapshot_after_truncate.max_frame, 0);
8129        assert_eq!(snapshot_after_truncate.nbackfills, 0);
8130        assert!(
8131            !authority.validate_backfill_proof(
8132                snapshot_before_truncate,
8133                db_size_before,
8134                db_crc_before
8135            ),
8136            "TRUNCATE must clear the proof for the truncated WAL generation"
8137        );
8138        assert_eq!(
8139            std::fs::metadata(&wal_path).unwrap().len(),
8140            0,
8141            "TRUNCATE must leave the WAL file empty before the new generation begins"
8142        );
8143
8144        bulk_inserts(&conn, 6, 2);
8145        let replacement = run_checkpoint_until_done(
8146            &pager,
8147            CheckpointMode::Passive {
8148                upper_bound_inclusive: Some(1),
8149            },
8150        );
8151        assert!(
8152            replacement.wal_total_backfilled > 0 && !replacement.everything_backfilled(),
8153            "replacement setup must create a new partial checkpoint after TRUNCATE"
8154        );
8155
8156        let snapshot_after_replacement = authority.snapshot();
8157        let (db_size_after, db_crc_after) =
8158            super::read_database_identity_from_file_path(&db.io, wal_path_str)
8159                .unwrap()
8160                .unwrap();
8161        assert!(
8162            authority.validate_backfill_proof(
8163                snapshot_after_replacement,
8164                db_size_after,
8165                db_crc_after
8166            ),
8167            "partial checkpoint after TRUNCATE must install a replacement proof for the new generation"
8168        );
8169        assert_ne!(
8170            snapshot_after_replacement.checkpoint_seq, snapshot_before_truncate.checkpoint_seq,
8171            "replacement proof must belong to the truncated WAL generation"
8172        );
8173    }
8174
8175    #[cfg(host_shared_wal)]
8176    #[test]
8177    fn test_classify_authority_snapshot_marks_truncated_wal_for_rebuild() {
8178        let dir = tempfile::tempdir().unwrap();
8179        let wal_path = dir.path().join("test-truncated.db-wal");
8180        let io = shared_wal_test_io();
8181        let snapshot = write_test_wal_with_single_commit_frame(&io, &wal_path);
8182
8183        let wal_len = std::fs::metadata(&wal_path).unwrap().len();
8184        std::fs::OpenOptions::new()
8185            .write(true)
8186            .open(&wal_path)
8187            .unwrap()
8188            .set_len(wal_len - 1)
8189            .unwrap();
8190
8191        assert_eq!(
8192            classify_authority_snapshot_against_wal(
8193                &io,
8194                &io.open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8195                    .unwrap(),
8196                snapshot,
8197            )
8198            .unwrap(),
8199            AuthoritySnapshotValidation::RebuildFromDisk(
8200                AuthoritySnapshotRebuildReason::WalLengthMismatch
8201            )
8202        );
8203    }
8204
8205    #[cfg(host_shared_wal)]
8206    #[test]
8207    fn test_classify_authority_snapshot_marks_corrupt_header_for_rebuild() {
8208        let dir = tempfile::tempdir().unwrap();
8209        let wal_path = dir.path().join("test-corrupt-header.db-wal");
8210        let io = shared_wal_test_io();
8211        std::fs::write(&wal_path, [0u8; WAL_HEADER_SIZE]).unwrap();
8212
8213        let snapshot = SharedWalCoordinationHeader {
8214            max_frame: 0,
8215            nbackfills: 0,
8216            transaction_count: 9,
8217            visibility_generation: 1,
8218            checkpoint_seq: 5,
8219            checkpoint_epoch: 7,
8220            page_size: 4096,
8221            salt_1: 17,
8222            salt_2: 23,
8223            checksum_1: 31,
8224            checksum_2: 37,
8225            reader_slot_count: 64,
8226        };
8227
8228        assert_eq!(
8229            classify_authority_snapshot_against_wal(
8230                &io,
8231                &io.open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8232                    .unwrap(),
8233                snapshot,
8234            )
8235            .unwrap(),
8236            AuthoritySnapshotValidation::RebuildFromDisk(
8237                AuthoritySnapshotRebuildReason::WalHeaderUnreadable
8238            )
8239        );
8240    }
8241
8242    #[cfg(host_shared_wal)]
8243    #[test]
8244    fn test_open_shared_from_authority_keeps_zero_length_wal_uninitialized_after_exclusive_reopen()
8245    {
8246        let dir = tempfile::tempdir().unwrap();
8247        let wal_path = dir.path().join("test-empty.db-wal");
8248        let shm_path = dir.path().join("test-empty.db-tshm");
8249        let io = shared_wal_test_io();
8250
8251        io.open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8252            .unwrap();
8253        {
8254            let authority =
8255                Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8256            authority.install_snapshot(SharedWalCoordinationHeader {
8257                max_frame: 0,
8258                nbackfills: 0,
8259                transaction_count: 9,
8260                visibility_generation: 1,
8261                checkpoint_seq: 5,
8262                checkpoint_epoch: 7,
8263                page_size: 4096,
8264                salt_1: 17,
8265                salt_2: 23,
8266                checksum_1: 31,
8267                checksum_2: 37,
8268                reader_slot_count: 64,
8269            });
8270        }
8271        let reopened_authority =
8272            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8273        assert_eq!(
8274            reopened_authority.open_mode(),
8275            SharedWalCoordinationOpenMode::Exclusive
8276        );
8277
8278        let shared = WalFileShared::open_shared_from_authority_if_exists(
8279            &io,
8280            wal_path.to_str().unwrap(),
8281            crate::OpenFlags::Create,
8282            &reopened_authority,
8283            &open_test_db_file_for_wal(&io, &wal_path),
8284        )
8285        .unwrap();
8286
8287        let shared = shared.read();
8288        assert_eq!(shared.metadata.max_frame.load(Ordering::Acquire), 0);
8289        assert_eq!(shared.metadata.last_checksum, (31, 37));
8290        assert!(!shared.metadata.initialized.load(Ordering::Acquire));
8291        assert!(!shared
8292            .metadata
8293            .loaded_from_disk_scan
8294            .load(Ordering::Acquire));
8295    }
8296
8297    #[cfg(host_shared_wal)]
8298    #[test]
8299    #[cfg_attr(
8300        windows,
8301        ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8302    )]
8303    fn test_shm_coordination_secondary_disk_scan_does_not_reseed_authority_while_writer_active() {
8304        let dir = tempfile::tempdir().unwrap();
8305        let wal_path = dir.path().join("test.db-wal");
8306        let shm_path = dir.path().join("test.db-tshm");
8307        let io = shared_wal_test_io();
8308
8309        let file_a = io
8310            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8311            .unwrap();
8312        let shared_a = WalFileShared::new_shared(file_a).unwrap();
8313        let authoritative = WalSnapshot {
8314            max_frame: 5,
8315            nbackfills: 0,
8316            last_checksum: (31, 37),
8317            checkpoint_seq: 5,
8318            transaction_count: 9,
8319        };
8320        set_shared_snapshot(&shared_a, authoritative);
8321        {
8322            let shared = shared_a.write();
8323            let mut header = shared.metadata.wal_header.lock();
8324            header.page_size = 4096;
8325            header.salt_1 = 17;
8326            header.salt_2 = 23;
8327            header.checksum_1 = authoritative.last_checksum.0;
8328            header.checksum_2 = authoritative.last_checksum.1;
8329        }
8330        let (authority, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
8331        coordination_a.cache_frame(7, 2);
8332        coordination_a.cache_frame(7, 5);
8333        assert!(authority.try_acquire_writer(authority.owner_record()));
8334
8335        let file_b = io
8336            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8337            .unwrap();
8338        let shared_b = WalFileShared::new_shared(file_b).unwrap();
8339        let stale = WalSnapshot {
8340            max_frame: 2,
8341            nbackfills: 0,
8342            last_checksum: (11, 13),
8343            checkpoint_seq: 4,
8344            transaction_count: 3,
8345        };
8346        set_shared_snapshot(&shared_b, stale);
8347        {
8348            let shared = shared_b.write();
8349            shared
8350                .metadata
8351                .loaded_from_disk_scan
8352                .store(true, Ordering::Release);
8353            let mut header = shared.metadata.wal_header.lock();
8354            header.page_size = 4096;
8355            header.salt_1 = 17;
8356            header.salt_2 = 23;
8357            header.checksum_1 = stale.last_checksum.0;
8358            header.checksum_2 = stale.last_checksum.1;
8359            shared.runtime.frame_cache.lock().insert(7, vec![2]);
8360        }
8361
8362        let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
8363
8364        assert_eq!(coordination_b.load_snapshot(), authoritative);
8365        assert_eq!(authority.snapshot().max_frame, authoritative.max_frame);
8366        assert_eq!(
8367            authority.snapshot().transaction_count,
8368            authoritative.transaction_count
8369        );
8370        assert_eq!(coordination_b.find_frame(7, 0, 5, None), Some(5));
8371        authority.release_writer(authority.owner_record());
8372    }
8373
8374    #[cfg(host_shared_wal)]
8375    #[test]
8376    #[cfg_attr(
8377        windows,
8378        ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8379    )]
8380    fn test_shm_coordination_disk_scan_matching_authority_keeps_frame_index() {
8381        let dir = tempfile::tempdir().unwrap();
8382        let wal_path = dir.path().join("test.db-wal");
8383        let shm_path = dir.path().join("test.db-tshm");
8384        let io = shared_wal_test_io();
8385
8386        let file_a = io
8387            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8388            .unwrap();
8389        let shared_a = WalFileShared::new_shared(file_a).unwrap();
8390        let authoritative = WalSnapshot {
8391            max_frame: 5,
8392            nbackfills: 2,
8393            last_checksum: (31, 37),
8394            checkpoint_seq: 5,
8395            transaction_count: 9,
8396        };
8397        set_shared_snapshot(&shared_a, authoritative);
8398        {
8399            let shared = shared_a.write();
8400            let mut header = shared.metadata.wal_header.lock();
8401            header.page_size = 4096;
8402            header.salt_1 = 17;
8403            header.salt_2 = 23;
8404            header.checksum_1 = authoritative.last_checksum.0;
8405            header.checksum_2 = authoritative.last_checksum.1;
8406        }
8407        let (authority, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
8408        coordination_a.cache_frame(7, 2);
8409        coordination_a.cache_frame(9, 5);
8410
8411        let file_b = io
8412            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8413            .unwrap();
8414        let shared_b = WalFileShared::new_shared(file_b).unwrap();
8415        set_shared_snapshot(&shared_b, authoritative);
8416        {
8417            let shared = shared_b.write();
8418            shared
8419                .metadata
8420                .loaded_from_disk_scan
8421                .store(true, Ordering::Release);
8422            let mut header = shared.metadata.wal_header.lock();
8423            header.page_size = 4096;
8424            header.salt_1 = 17;
8425            header.salt_2 = 23;
8426            header.checksum_1 = authoritative.last_checksum.0;
8427            header.checksum_2 = authoritative.last_checksum.1;
8428            let mut frame_cache = shared.runtime.frame_cache.lock();
8429            frame_cache.insert(7, vec![2]);
8430            frame_cache.insert(9, vec![5]);
8431        }
8432
8433        let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
8434
8435        let reopened = coordination_b.load_snapshot();
8436        assert_eq!(reopened.max_frame, authoritative.max_frame);
8437        assert_eq!(reopened.last_checksum, authoritative.last_checksum);
8438        assert_eq!(reopened.checkpoint_seq, authoritative.checkpoint_seq);
8439        assert_eq!(reopened.transaction_count, authoritative.transaction_count);
8440        assert_eq!(
8441            reopened.nbackfills, 0,
8442            "disk-scan reconciliation must preserve the frame index without reviving positive nbackfills"
8443        );
8444        assert_eq!(authority.find_frame(7, 0, 5, None), Some(2));
8445        assert_eq!(authority.find_frame(9, 0, 5, None), Some(5));
8446        assert_eq!(
8447            authority.iter_latest_frames(0, authoritative.max_frame),
8448            vec![(7, 2), (9, 5)]
8449        );
8450    }
8451
8452    #[cfg(host_shared_wal)]
8453    #[test]
8454    #[cfg_attr(
8455        windows,
8456        ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8457    )]
8458    fn test_shm_coordination_disk_scan_matching_snapshot_rebuilds_stale_frame_index() {
8459        let dir = tempfile::tempdir().unwrap();
8460        let wal_path = dir.path().join("test.db-wal");
8461        let shm_path = dir.path().join("test.db-tshm");
8462        let io = shared_wal_test_io();
8463
8464        let file_a = io
8465            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8466            .unwrap();
8467        let shared_a = WalFileShared::new_shared(file_a).unwrap();
8468        let authoritative = WalSnapshot {
8469            max_frame: 5,
8470            nbackfills: 0,
8471            last_checksum: (31, 37),
8472            checkpoint_seq: 5,
8473            transaction_count: 9,
8474        };
8475        set_shared_snapshot(&shared_a, authoritative);
8476        {
8477            let shared = shared_a.write();
8478            let mut header = shared.metadata.wal_header.lock();
8479            header.page_size = 4096;
8480            header.salt_1 = 17;
8481            header.salt_2 = 23;
8482            header.checksum_1 = authoritative.last_checksum.0;
8483            header.checksum_2 = authoritative.last_checksum.1;
8484        }
8485        {
8486            let (_authority, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
8487            coordination_a.cache_frame(7, 2);
8488            coordination_a.cache_frame(9, 4);
8489        }
8490
8491        let file_b = io
8492            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8493            .unwrap();
8494        let shared_b = WalFileShared::new_shared(file_b).unwrap();
8495        set_shared_snapshot(&shared_b, authoritative);
8496        {
8497            let shared = shared_b.write();
8498            shared
8499                .metadata
8500                .loaded_from_disk_scan
8501                .store(true, Ordering::Release);
8502            let mut header = shared.metadata.wal_header.lock();
8503            header.page_size = 4096;
8504            header.salt_1 = 17;
8505            header.salt_2 = 23;
8506            header.checksum_1 = authoritative.last_checksum.0;
8507            header.checksum_2 = authoritative.last_checksum.1;
8508            shared.runtime.frame_cache.lock().insert(7, vec![2]);
8509            shared.runtime.frame_cache.lock().insert(9, vec![5]);
8510        }
8511
8512        let (authority, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
8513        assert_eq!(
8514            authority.open_mode(),
8515            SharedWalCoordinationOpenMode::Exclusive
8516        );
8517
8518        let reopened = coordination_b.load_snapshot();
8519        assert_eq!(reopened.max_frame, authoritative.max_frame);
8520        assert_eq!(reopened.last_checksum, authoritative.last_checksum);
8521        assert_eq!(reopened.checkpoint_seq, authoritative.checkpoint_seq);
8522        assert_eq!(reopened.transaction_count, authoritative.transaction_count);
8523        assert_eq!(authority.find_frame(7, 0, 5, None), Some(2));
8524        assert_eq!(
8525            authority.find_frame(9, 0, 5, None),
8526            Some(5),
8527            "matching snapshot metadata must not preserve a stale shared frame index across restart recovery"
8528        );
8529        assert_eq!(
8530            authority.iter_latest_frames(0, authoritative.max_frame),
8531            vec![(7, 2), (9, 5)]
8532        );
8533    }
8534
8535    #[cfg(host_shared_wal)]
8536    #[test]
8537    fn test_shm_coordination_empty_disk_scan_keeps_zero_frame_authority_metadata() {
8538        let dir = tempfile::tempdir().unwrap();
8539        let wal_path = dir.path().join("test.db-wal");
8540        let shm_path = dir.path().join("test.db-tshm");
8541        let io = shared_wal_test_io();
8542
8543        let authority =
8544            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8545        let authoritative = SharedWalCoordinationHeader {
8546            max_frame: 0,
8547            nbackfills: 0,
8548            transaction_count: 9,
8549            visibility_generation: 3,
8550            checkpoint_seq: 5,
8551            checkpoint_epoch: 7,
8552            page_size: 4096,
8553            salt_1: 17,
8554            salt_2: 23,
8555            checksum_1: 31,
8556            checksum_2: 37,
8557            reader_slot_count: 64,
8558        };
8559        authority.install_snapshot(authoritative);
8560
8561        let file = io
8562            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8563            .unwrap();
8564        let shared = WalFileShared::new_shared(file).unwrap();
8565        {
8566            let shared = shared.write();
8567            shared
8568                .metadata
8569                .loaded_from_disk_scan
8570                .store(true, Ordering::Release);
8571        }
8572
8573        let coordination = ShmWalCoordination::new(shared, authority.clone());
8574        let snapshot = authority.snapshot();
8575        assert_eq!(snapshot, authoritative);
8576        let header = coordination.wal_header();
8577        assert_eq!(header.page_size, 4096);
8578        assert_eq!(header.checkpoint_seq, authoritative.checkpoint_seq);
8579        assert_eq!(header.salt_1, authoritative.salt_1);
8580        assert_eq!(header.salt_2, authoritative.salt_2);
8581    }
8582
8583    #[cfg(host_shared_wal)]
8584    #[test]
8585    #[cfg_attr(
8586        windows,
8587        ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8588    )]
8589    fn test_shm_coordination_empty_disk_scan_does_not_clobber_positive_authority() {
8590        let dir = tempfile::tempdir().unwrap();
8591        let wal_path = dir.path().join("test.db-wal");
8592        let shm_path = dir.path().join("test.db-tshm");
8593        let io = shared_wal_test_io();
8594
8595        let file_a = io
8596            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8597            .unwrap();
8598        let shared_a = WalFileShared::new_shared(file_a).unwrap();
8599        let authoritative = WalSnapshot {
8600            max_frame: 5,
8601            nbackfills: 0,
8602            last_checksum: (31, 37),
8603            checkpoint_seq: 5,
8604            transaction_count: 9,
8605        };
8606        set_shared_snapshot(&shared_a, authoritative);
8607        {
8608            let shared = shared_a.write();
8609            let mut header = shared.metadata.wal_header.lock();
8610            header.page_size = 4096;
8611            header.salt_1 = 17;
8612            header.salt_2 = 23;
8613            header.checksum_1 = authoritative.last_checksum.0;
8614            header.checksum_2 = authoritative.last_checksum.1;
8615        }
8616        let (authority, coordination_a) = make_test_shm_coordination(&shared_a, &shm_path);
8617        coordination_a.cache_frame(7, 2);
8618        coordination_a.cache_frame(9, 5);
8619
8620        let file_b = io
8621            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8622            .unwrap();
8623        let shared_b = WalFileShared::new_shared(file_b).unwrap();
8624        {
8625            let shared = shared_b.write();
8626            shared
8627                .metadata
8628                .loaded_from_disk_scan
8629                .store(true, Ordering::Release);
8630            let mut header = shared.metadata.wal_header.lock();
8631            header.page_size = 4096;
8632            header.checkpoint_seq = authoritative.checkpoint_seq;
8633            header.salt_1 = 17;
8634            header.salt_2 = 23;
8635            header.checksum_1 = 11;
8636            header.checksum_2 = 13;
8637        }
8638
8639        let (_authority_b, coordination_b) = make_test_shm_coordination(&shared_b, &shm_path);
8640        let reopened = coordination_b.load_snapshot();
8641        assert_eq!(reopened.max_frame, authoritative.max_frame);
8642        assert_eq!(reopened.checkpoint_seq, authoritative.checkpoint_seq);
8643        assert_eq!(reopened.transaction_count, authoritative.transaction_count);
8644        assert_eq!(
8645            authority.find_frame(7, 0, authoritative.max_frame, None),
8646            Some(2)
8647        );
8648        assert_eq!(
8649            authority.find_frame(9, 0, authoritative.max_frame, None),
8650            Some(5)
8651        );
8652    }
8653
8654    #[cfg(host_shared_wal)]
8655    #[test]
8656    fn test_shm_zero_frame_authority_invalidates_stale_local_initialized_state() {
8657        let dir = tempfile::tempdir().unwrap();
8658        let wal_path = dir.path().join("test.db-wal");
8659        let shm_path = dir.path().join("test.db-tshm");
8660        let io = shared_wal_test_io();
8661
8662        let authority =
8663            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8664        let authoritative = SharedWalCoordinationHeader {
8665            max_frame: 0,
8666            nbackfills: 0,
8667            transaction_count: 9,
8668            visibility_generation: 3,
8669            checkpoint_seq: 5,
8670            checkpoint_epoch: 7,
8671            page_size: 4096,
8672            salt_1: 17,
8673            salt_2: 23,
8674            checksum_1: 31,
8675            checksum_2: 37,
8676            reader_slot_count: 64,
8677        };
8678        authority.install_snapshot(authoritative);
8679
8680        let file = io
8681            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8682            .unwrap();
8683        let shared = WalFileShared::new_shared(file).unwrap();
8684        {
8685            let mut shared = shared.write();
8686            shared.metadata.max_frame.store(11, Ordering::Release);
8687            shared.metadata.nbackfills.store(11, Ordering::Release);
8688            shared.metadata.last_checksum = (11, 13);
8689            shared
8690                .metadata
8691                .transaction_count
8692                .store(3, Ordering::Release);
8693            shared.metadata.initialized.store(true, Ordering::Release);
8694            let mut header = shared.metadata.wal_header.lock();
8695            header.checkpoint_seq = 2;
8696            header.page_size = 4096;
8697            header.salt_1 = 7;
8698            header.salt_2 = 13;
8699            header.checksum_1 = 11;
8700            header.checksum_2 = 13;
8701            shared.runtime.epoch.store(1, Ordering::Release);
8702        }
8703
8704        let coordination = ShmWalCoordination::new(shared.clone(), authority);
8705        assert!(
8706            !coordination.wal_is_initialized(),
8707            "a stale local initialized bit must not suppress the first header rewrite after RESTART/TRUNCATE"
8708        );
8709        {
8710            let shared = shared.read();
8711            assert!(
8712                !shared.metadata.initialized.load(Ordering::Acquire),
8713                "stale local initialized state must be cleared"
8714            );
8715            let header = shared.metadata.wal_header.lock();
8716            assert_eq!(header.checkpoint_seq, authoritative.checkpoint_seq);
8717            assert_eq!(header.page_size, authoritative.page_size);
8718            assert_eq!(header.salt_1, authoritative.salt_1);
8719            assert_eq!(header.salt_2, authoritative.salt_2);
8720            assert_eq!(header.checksum_1, authoritative.checksum_1);
8721            assert_eq!(header.checksum_2, authoritative.checksum_2);
8722        }
8723
8724        let prepared = coordination
8725            .prepare_wal_header(io.as_ref(), PageSize::new(4096).unwrap())
8726            .expect("zero-frame authority should force a header rewrite");
8727        assert_eq!(prepared.checkpoint_seq, authoritative.checkpoint_seq);
8728        coordination.mark_initialized();
8729        assert!(
8730            coordination.wal_is_initialized(),
8731            "once the current-generation header is durably rewritten, wal_is_initialized should succeed"
8732        );
8733    }
8734
8735    #[cfg(host_shared_wal)]
8736    #[test]
8737    fn test_shm_prepare_wal_header_seeds_uninitialized_authority_from_prepared_header() {
8738        let dir = tempfile::tempdir().unwrap();
8739        let wal_path = dir.path().join("test.db-wal");
8740        let shm_path = dir.path().join("test.db-tshm");
8741        let io = shared_wal_test_io();
8742
8743        let authority =
8744            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8745        let file = io
8746            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8747            .unwrap();
8748        let shared = WalFileShared::new_shared(file).unwrap();
8749        let coordination = ShmWalCoordination::new(shared, authority.clone());
8750
8751        let prepared = coordination
8752            .prepare_wal_header(io.as_ref(), PageSize::new(4096).unwrap())
8753            .expect("fresh authority should accept the first prepared header");
8754
8755        let snapshot = authority.snapshot();
8756        assert_eq!(
8757            snapshot.page_size, prepared.page_size,
8758            "authority must publish the prepared page size for later writers and checkpointers"
8759        );
8760        assert_eq!(
8761            snapshot.checkpoint_seq, prepared.checkpoint_seq,
8762            "authority must publish the prepared checkpoint generation"
8763        );
8764        assert_eq!(snapshot.salt_1, prepared.salt_1);
8765        assert_eq!(snapshot.salt_2, prepared.salt_2);
8766    }
8767
8768    #[cfg(host_shared_wal)]
8769    #[test]
8770    #[cfg_attr(
8771        windows,
8772        ignore = "Windows file locks are mandatory; opening the same WAL twice in one process clashes"
8773    )]
8774    fn test_shm_prepare_wal_header_does_not_clobber_zero_frame_authority_snapshot() {
8775        let dir = tempfile::tempdir().unwrap();
8776        let wal_path = dir.path().join("test.db-wal");
8777        let shm_path = dir.path().join("test.db-tshm");
8778        let io = shared_wal_test_io();
8779
8780        let file_a = io
8781            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8782            .unwrap();
8783        let shared_a = WalFileShared::new_shared(file_a).unwrap();
8784        let authoritative = SharedWalCoordinationHeader {
8785            max_frame: 0,
8786            nbackfills: 0,
8787            transaction_count: 9,
8788            visibility_generation: 3,
8789            checkpoint_seq: 5,
8790            checkpoint_epoch: 7,
8791            page_size: 4096,
8792            salt_1: 17,
8793            salt_2: 23,
8794            checksum_1: 31,
8795            checksum_2: 37,
8796            reader_slot_count: 64,
8797        };
8798        {
8799            let mut shared = shared_a.write();
8800            shared.metadata.max_frame.store(0, Ordering::Release);
8801            shared.metadata.nbackfills.store(0, Ordering::Release);
8802            shared
8803                .metadata
8804                .transaction_count
8805                .store(authoritative.transaction_count, Ordering::Release);
8806            shared.metadata.last_checksum = (31, 37);
8807            let mut header = shared.metadata.wal_header.lock();
8808            header.checkpoint_seq = authoritative.checkpoint_seq;
8809            header.page_size = authoritative.page_size;
8810            header.salt_1 = authoritative.salt_1;
8811            header.salt_2 = authoritative.salt_2;
8812            header.checksum_1 = authoritative.checksum_1;
8813            header.checksum_2 = authoritative.checksum_2;
8814            shared
8815                .runtime
8816                .epoch
8817                .store(authoritative.checkpoint_epoch, Ordering::Release);
8818            shared.metadata.initialized.store(false, Ordering::Release);
8819        }
8820        let authority =
8821            Arc::new(MappedSharedWalCoordination::create_or_open(&io, &shm_path, 64).unwrap());
8822        authority.install_snapshot(authoritative);
8823
8824        let file_b = io
8825            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8826            .unwrap();
8827        let shared_b = WalFileShared::new_shared(file_b).unwrap();
8828        let coordination_b = ShmWalCoordination::new(shared_b.clone(), authority.clone());
8829        // Simulate a long-lived process whose process-wide shared WAL metadata
8830        // fell behind the authority after another process checkpointed and
8831        // restarted the WAL back to frame 0.
8832        {
8833            let mut shared = shared_b.write();
8834            shared.metadata.max_frame.store(0, Ordering::Release);
8835            shared.metadata.nbackfills.store(0, Ordering::Release);
8836            shared.metadata.last_checksum = (11, 13);
8837            shared
8838                .metadata
8839                .transaction_count
8840                .store(3, Ordering::Release);
8841            let mut header = shared.metadata.wal_header.lock();
8842            header.checkpoint_seq = 2;
8843            header.page_size = 4096;
8844            header.salt_1 = 17;
8845            header.salt_2 = 23;
8846            header.checksum_1 = 11;
8847            header.checksum_2 = 13;
8848            shared.runtime.epoch.store(1, Ordering::Release);
8849            shared.metadata.initialized.store(false, Ordering::Release);
8850        }
8851
8852        let page_size = PageSize::new(4096).unwrap();
8853        let prepared = coordination_b
8854            .prepare_wal_header(io.as_ref(), page_size)
8855            .expect("prepare_wal_header should produce a header");
8856
8857        let snapshot = authority.snapshot();
8858        assert_eq!(
8859            snapshot.transaction_count, authoritative.transaction_count,
8860            "first writer after restart must not downgrade authority transaction_count"
8861        );
8862        assert_eq!(
8863            snapshot.checkpoint_seq, authoritative.checkpoint_seq,
8864            "first writer after restart must not downgrade checkpoint metadata"
8865        );
8866        assert_eq!(
8867            prepared.checkpoint_seq, authoritative.checkpoint_seq,
8868            "header written after restart must use authority checkpoint metadata"
8869        );
8870        assert_eq!(prepared.page_size, authoritative.page_size);
8871        assert_eq!(prepared.salt_1, authoritative.salt_1);
8872        assert_eq!(prepared.salt_2, authoritative.salt_2);
8873        let refreshed = authority.snapshot();
8874        assert_eq!(
8875            refreshed.checksum_1, prepared.checksum_1,
8876            "preparing the first zero-frame header must refresh the authoritative checksum seed"
8877        );
8878        assert_eq!(
8879            refreshed.checksum_2, prepared.checksum_2,
8880            "preparing the first zero-frame header must refresh the authoritative checksum seed"
8881        );
8882    }
8883
8884    #[test]
8885    fn test_in_process_coordination_lock_primitives() {
8886        let (shared, _wal) = make_test_wal();
8887        let coordination = make_test_coordination(&shared);
8888
8889        assert!(coordination.try_checkpoint_lock());
8890        coordination.unlock_checkpoint_lock();
8891
8892        assert!(coordination.try_write_lock());
8893        assert!(!coordination.try_write_lock());
8894        coordination.unlock_write_lock();
8895
8896        assert!(coordination.try_read_mark_exclusive(1));
8897        coordination.set_read_mark_value_exclusive(1, 42);
8898        assert_eq!(coordination.read_mark_value(1), 42);
8899        coordination.unlock_read_mark(1);
8900
8901        assert!(coordination.try_read_mark_shared(1));
8902        assert!(coordination.try_upgrade_read_mark(1));
8903        coordination.downgrade_read_mark(1);
8904        coordination.unlock_read_mark(1);
8905
8906        // The coordination backend should still observe the shared state underneath.
8907        assert_eq!(shared.read().runtime.read_locks[1].get_value(), 42);
8908    }
8909
8910    #[test]
8911    fn test_in_process_coordination_prepare_truncate_marks_wal_uninitialized() {
8912        let io = shared_wal_test_io();
8913        let dir = tempfile::tempdir().unwrap();
8914        let wal_path = dir.path().join("test.wal");
8915        let file = io
8916            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8917            .unwrap();
8918        let shared = WalFileShared::new_shared(file).unwrap();
8919        let coordination = make_test_coordination(&shared);
8920
8921        shared
8922            .read()
8923            .metadata
8924            .initialized
8925            .store(true, Ordering::Release);
8926        let file = coordination.prepare_truncate().unwrap();
8927
8928        assert!(file.size().is_ok());
8929        assert!(!shared.read().metadata.initialized.load(Ordering::Acquire));
8930    }
8931
8932    #[test]
8933    fn test_in_process_coordination_exposes_wal_io_state() {
8934        let io = shared_wal_test_io();
8935        let dir = tempfile::tempdir().unwrap();
8936        let wal_path = dir.path().join("test.wal");
8937        let file = io
8938            .open_file(wal_path.to_str().unwrap(), crate::OpenFlags::Create, false)
8939            .unwrap();
8940        let shared = WalFileShared::new_shared(file).unwrap();
8941        let coordination = make_test_coordination(&shared);
8942
8943        assert!(!coordination.wal_is_initialized());
8944        assert_eq!(coordination.wal_header().page_size, 0);
8945        assert!(coordination.wal_file().unwrap().size().is_ok());
8946
8947        let header = coordination
8948            .prepare_wal_header(io.as_ref(), PageSize::new(4096).unwrap())
8949            .unwrap();
8950        assert_eq!(header.page_size, 4096);
8951        assert_eq!(
8952            coordination.load_snapshot().last_checksum,
8953            (header.checksum_1, header.checksum_2)
8954        );
8955        assert!(!coordination.wal_is_initialized());
8956
8957        coordination.mark_initialized();
8958        assert!(coordination.wal_is_initialized());
8959        assert!(coordination
8960            .prepare_wal_header(io.as_ref(), PageSize::new(4096).unwrap())
8961            .is_none());
8962    }
8963
8964    #[test]
8965    fn test_vacuum_lock_blocks_new_read_transactions_until_release() {
8966        let (shared, vacuum_wal) = make_test_wal();
8967        let reader_wal = make_test_wal_from_shared(shared);
8968
8969        vacuum_wal.try_begin_vacuum_checkpoint_lock().unwrap();
8970        vacuum_wal.begin_vacuum_blocking_tx().unwrap();
8971
8972        assert!(
8973            matches!(reader_wal.try_begin_read_tx(), TryBeginReadResult::Busy),
8974            "VACUUM lock should block new WAL readers before they take a read-mark slot"
8975        );
8976        assert!(
8977            !vacuum_wal.holds_read_lock(),
8978            "exclusive VACUUM snapshot must not masquerade as a read-mark lock"
8979        );
8980        assert!(
8981            vacuum_wal.holds_write_lock(),
8982            "begin_vacuum_blocking_tx should acquire the source write lock"
8983        );
8984
8985        vacuum_wal.end_write_tx();
8986        vacuum_wal.release_vacuum_lock();
8987        vacuum_wal.release_vacuum_checkpoint_lock();
8988
8989        assert!(
8990            matches!(reader_wal.try_begin_read_tx(), TryBeginReadResult::Ok(_)),
8991            "reader should start after VACUUM releases the lock"
8992        );
8993        reader_wal.end_read_tx();
8994    }
8995
8996    #[test]
8997    fn test_active_reader_blocks_vacuum_exclusive_tx() {
8998        let (shared, reader_wal) = make_test_wal();
8999        let vacuum_wal = make_test_wal_from_shared(shared);
9000
9001        assert!(matches!(
9002            reader_wal.try_begin_read_tx(),
9003            TryBeginReadResult::Ok(_)
9004        ));
9005        vacuum_wal.try_begin_vacuum_checkpoint_lock().unwrap();
9006
9007        assert!(
9008            matches!(vacuum_wal.begin_vacuum_blocking_tx(), Err(LimboError::Busy)),
9009            "active reader should prevent VACUUM from acquiring its exclusive lock"
9010        );
9011
9012        reader_wal.end_read_tx();
9013        vacuum_wal.begin_vacuum_blocking_tx().unwrap();
9014        vacuum_wal.end_write_tx();
9015        vacuum_wal.release_vacuum_lock();
9016        vacuum_wal.release_vacuum_checkpoint_lock();
9017    }
9018
9019    #[test]
9020    fn test_read_retry_does_not_leak_vacuum_guard_or_block_vacuum() {
9021        let (shared, _) = make_test_wal();
9022        let retry_reader = make_test_wal_from_shared(shared.clone());
9023        let vacuum_wal = make_test_wal_from_shared(shared.clone());
9024
9025        set_shared_snapshot(
9026            &shared,
9027            WalSnapshot {
9028                max_frame: 5,
9029                nbackfills: 0,
9030                last_checksum: (0, 0),
9031                checkpoint_seq: 0,
9032                transaction_count: 1,
9033            },
9034        );
9035
9036        for idx in 1..5 {
9037            assert!(
9038                shared.read().runtime.read_locks[idx].write(),
9039                "expected setup to occupy read-mark slot {idx}"
9040            );
9041        }
9042
9043        assert!(
9044            matches!(retry_reader.try_begin_read_tx(), TryBeginReadResult::Retry),
9045            "reader should retry when all read-mark slots are transiently unavailable"
9046        );
9047        assert!(
9048            !retry_reader.has_vacuum_read_lock_guard(),
9049            "retry path must not retain a shared VACUUM lock guard"
9050        );
9051        assert_eq!(
9052            retry_reader
9053                .max_frame_read_lock_index
9054                .load(Ordering::Acquire),
9055            NO_LOCK_HELD,
9056            "retry path must not retain a read-mark slot"
9057        );
9058
9059        for idx in 1..5 {
9060            shared.read().runtime.read_locks[idx].unlock();
9061        }
9062
9063        vacuum_wal.try_begin_vacuum_checkpoint_lock().unwrap();
9064        vacuum_wal.begin_vacuum_blocking_tx().unwrap();
9065        vacuum_wal.end_write_tx();
9066        vacuum_wal.release_vacuum_lock();
9067        vacuum_wal.release_vacuum_checkpoint_lock();
9068    }
9069
9070    #[test]
9071    fn test_held_vacuum_checkpoint_locks_do_not_release_vacuum_lock() {
9072        let (shared, vacuum_wal) = make_test_wal();
9073        let contender_wal = make_test_wal_from_shared(shared);
9074
9075        vacuum_wal.try_begin_vacuum_checkpoint_lock().unwrap();
9076        vacuum_wal.begin_vacuum_blocking_tx().unwrap();
9077
9078        assert!(vacuum_wal.holds_write_lock());
9079        assert!(!vacuum_wal.holds_read_lock());
9080        assert!(
9081            matches!(
9082                contender_wal.try_begin_vacuum_checkpoint_lock(),
9083                Err(LimboError::Busy)
9084            ),
9085            "held checkpoint lock should block other checkpointers"
9086        );
9087
9088        vacuum_wal.end_write_tx();
9089        assert!(!vacuum_wal.holds_write_lock());
9090
9091        let guard =
9092            CheckpointLocks::from_held_vacuum_checkpoint_lock(vacuum_wal.coordination.clone())
9093                .unwrap();
9094        assert!(
9095            matches!(contender_wal.try_begin_read_tx(), TryBeginReadResult::Busy),
9096            "VACUUM lock should continue blocking readers during final checkpoint"
9097        );
9098
9099        drop(guard);
9100        assert!(
9101            contender_wal.try_begin_vacuum_checkpoint_lock().is_ok(),
9102            "checkpoint cleanup should release the checkpoint lock"
9103        );
9104        contender_wal.release_vacuum_checkpoint_lock();
9105        assert!(
9106            matches!(contender_wal.try_begin_read_tx(), TryBeginReadResult::Busy),
9107            "checkpoint cleanup must not release the VACUUM lock"
9108        );
9109
9110        vacuum_wal.release_vacuum_lock();
9111        assert!(matches!(
9112            contender_wal.try_begin_read_tx(),
9113            TryBeginReadResult::Ok(_)
9114        ));
9115        contender_wal.end_read_tx();
9116    }
9117
9118    #[test]
9119    fn restart_checkpoint_reset_wal_state_handling() {
9120        let (db, path) = get_database();
9121
9122        let walpath = {
9123            let mut p = path.clone().into_os_string().into_string().unwrap();
9124            p.push_str("/test.db-wal");
9125            std::path::PathBuf::from(p)
9126        };
9127
9128        let conn = db.connect().unwrap();
9129        conn.execute("create table test(id integer primary key, value text)")
9130            .unwrap();
9131        bulk_inserts(&conn, 20, 3);
9132        let IOResult::Done(completions) = conn.pager.load().cacheflush().unwrap() else {
9133            panic!()
9134        };
9135        for c in completions {
9136            db.io.wait_for_completion(c).unwrap();
9137        }
9138
9139        // Snapshot header & counters before the RESTART checkpoint.
9140        let wal_shared = db.shared_wal.clone();
9141        let (seq_before, salt1_before, salt2_before, _ps_before) = wal_header_snapshot(&wal_shared);
9142        let (mx_before, backfill_before) = {
9143            let s = wal_shared.read();
9144            (
9145                s.metadata.max_frame.load(Ordering::SeqCst),
9146                s.metadata.nbackfills.load(Ordering::SeqCst),
9147            )
9148        };
9149        assert!(mx_before > 0);
9150        assert_eq!(backfill_before, 0);
9151
9152        let meta_before = std::fs::metadata(&walpath).unwrap();
9153        #[cfg(unix)]
9154        let size_before = meta_before.blocks();
9155        #[cfg(not(unix))]
9156        let size_before = meta_before.len();
9157        // Run a RESTART checkpoint, should backfill everything and reset WAL counters,
9158        // but NOT truncate the file.
9159        {
9160            let pager = conn.pager.load();
9161            let res = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
9162            assert_eq!(res.wal_max_frame, mx_before);
9163            assert_eq!(res.wal_total_backfilled, mx_before);
9164            assert_eq!(res.wal_checkpoint_backfilled, mx_before);
9165        }
9166
9167        // Validate post‑RESTART header & counters.
9168        let (seq_after, salt1_after, salt2_after, _ps_after) = wal_header_snapshot(&wal_shared);
9169        assert_eq!(
9170            seq_after,
9171            seq_before.wrapping_add(1),
9172            "checkpoint_seq must increment on RESTART"
9173        );
9174        assert_eq!(
9175            salt1_after,
9176            salt1_before.wrapping_add(1),
9177            "salt_1 is incremented"
9178        );
9179        assert_ne!(salt2_after, salt2_before, "salt_2 is randomized");
9180
9181        let (mx_after, backfill_after) = {
9182            let s = wal_shared.read();
9183            (
9184                s.metadata.max_frame.load(Ordering::SeqCst),
9185                s.metadata.nbackfills.load(Ordering::SeqCst),
9186            )
9187        };
9188        assert_eq!(mx_after, 0, "mxFrame reset to 0 after RESTART");
9189        assert_eq!(backfill_after, 0, "nBackfill reset to 0 after RESTART");
9190
9191        // File size should be unchanged for RESTART (no truncate).
9192        let meta_after = std::fs::metadata(&walpath).unwrap();
9193        #[cfg(unix)]
9194        let size_after = meta_after.blocks();
9195        #[cfg(not(unix))]
9196        let size_after = meta_after.len();
9197        assert_eq!(
9198            size_before, size_after,
9199            "RESTART must not change WAL file size"
9200        );
9201
9202        // Next write should start a new sequence at frame 1.
9203        conn.execute("insert into test(value) values ('post_restart')")
9204            .unwrap();
9205        conn.pager
9206            .load()
9207            .wal
9208            .as_ref()
9209            .unwrap()
9210            .finish_append_frames_commit()
9211            .unwrap();
9212        let new_max = wal_shared.read().metadata.max_frame.load(Ordering::SeqCst);
9213        assert_eq!(new_max, 1, "first append after RESTART starts at frame 1");
9214
9215        std::fs::remove_dir_all(path).unwrap();
9216    }
9217
9218    #[test]
9219    fn test_wal_passive_partial_then_complete() {
9220        let (db, _tmp) = get_database();
9221        let conn1 = db.connect().unwrap();
9222        let conn2 = db.connect().unwrap();
9223
9224        conn1
9225            .execute("create table test(id integer primary key, value text)")
9226            .unwrap();
9227        bulk_inserts(&conn1, 15, 2);
9228        let IOResult::Done(completions) = conn1.pager.load().cacheflush().unwrap() else {
9229            panic!()
9230        };
9231        for c in completions {
9232            db.io.wait_for_completion(c).unwrap();
9233        }
9234
9235        // Force a read transaction that will freeze a lower read mark
9236        let readmark = {
9237            let pager = conn2.pager.load();
9238            let wal2 = pager.wal.as_ref().unwrap();
9239            wal2.begin_read_tx().unwrap();
9240            wal2.get_max_frame()
9241        };
9242
9243        // generate more frames that the reader will not see.
9244        bulk_inserts(&conn1, 15, 2);
9245        let IOResult::Done(completions) = conn1.pager.load().cacheflush().unwrap() else {
9246            panic!()
9247        };
9248        for c in completions {
9249            db.io.wait_for_completion(c).unwrap();
9250        }
9251
9252        // Run passive checkpoint, expect partial
9253        let (res1, max_before) = {
9254            let pager = conn1.pager.load();
9255            let res = run_checkpoint_until_done(
9256                &pager,
9257                CheckpointMode::Passive {
9258                    upper_bound_inclusive: None,
9259                },
9260            );
9261            let maxf = db
9262                .shared_wal
9263                .read()
9264                .metadata
9265                .max_frame
9266                .load(Ordering::SeqCst);
9267            (res, maxf)
9268        };
9269        assert_eq!(res1.wal_max_frame, max_before);
9270        assert!(
9271            res1.wal_total_backfilled < res1.wal_max_frame,
9272            "Partial backfill expected, {} : {}",
9273            res1.wal_total_backfilled,
9274            res1.wal_max_frame
9275        );
9276        assert_eq!(
9277            res1.wal_total_backfilled, readmark,
9278            "Checkpointed frames should match read mark"
9279        );
9280        // Release reader
9281        {
9282            let pager = conn2.pager.load();
9283            let wal2 = pager.wal.as_ref().unwrap();
9284            wal2.end_read_tx();
9285        }
9286
9287        // Second passive checkpoint should finish
9288        let pager = conn1.pager.load();
9289        let res2 = run_checkpoint_until_done(
9290            &pager,
9291            CheckpointMode::Passive {
9292                upper_bound_inclusive: None,
9293            },
9294        );
9295        assert_eq!(
9296            res2.wal_total_backfilled, res2.wal_max_frame,
9297            "Second checkpoint completes remaining frames"
9298        );
9299    }
9300
9301    #[test]
9302    fn test_wal_restart_blocks_readers() {
9303        let (db, _) = get_database();
9304        let conn1 = db.connect().unwrap();
9305        let conn2 = db.connect().unwrap();
9306
9307        // Start a read transaction
9308        conn2
9309            .pager
9310            .load()
9311            .wal
9312            .as_ref()
9313            .unwrap()
9314            .begin_read_tx()
9315            .unwrap();
9316
9317        // checkpoint should succeed here because the wal is fully checkpointed (empty)
9318        // so the reader is using readmark0 to read directly from the db file.
9319        let p = conn1.pager.load();
9320        let w = p.wal.as_ref().unwrap();
9321        loop {
9322            match w.checkpoint(&p, CheckpointMode::Restart) {
9323                Ok(IOResult::IO(io)) => {
9324                    io.wait(db.io.as_ref()).unwrap();
9325                }
9326                e => {
9327                    assert!(
9328                        matches!(e, Err(LimboError::Busy)),
9329                        "reader is holding readmark0 we should return Busy"
9330                    );
9331                    break;
9332                }
9333            }
9334        }
9335        conn2.pager.load().end_read_tx();
9336
9337        conn1
9338            .execute("create table test(id integer primary key, value text)")
9339            .unwrap();
9340        for i in 0..10 {
9341            conn1
9342                .execute(format!("insert into test(value) values ('value{i}')"))
9343                .unwrap();
9344        }
9345        // now that we have some frames to checkpoint, try again
9346        conn2.pager.load().begin_read_tx().unwrap();
9347        let p = conn1.pager.load();
9348        let w = p.wal.as_ref().unwrap();
9349        loop {
9350            match w.checkpoint(&p, CheckpointMode::Restart) {
9351                Ok(IOResult::IO(io)) => {
9352                    io.wait(db.io.as_ref()).unwrap();
9353                }
9354                Ok(IOResult::Done(_)) => {
9355                    panic!("Checkpoint should not have succeeded");
9356                }
9357                Err(e) => {
9358                    assert!(
9359                        matches!(e, LimboError::Busy),
9360                        "should return busy if we have readers"
9361                    );
9362                    break;
9363                }
9364            }
9365        }
9366    }
9367
9368    #[test]
9369    fn test_wal_read_marks_after_restart() {
9370        let (db, _path) = get_database();
9371        let wal_shared = db.shared_wal.clone();
9372
9373        let conn = db.connect().unwrap();
9374        conn.execute("create table test(id integer primary key, value text)")
9375            .unwrap();
9376        bulk_inserts(&conn, 10, 5);
9377        // Checkpoint with restart
9378        {
9379            let pager = conn.pager.load();
9380            let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
9381            assert!(result.everything_backfilled());
9382        }
9383
9384        // Verify read marks after restart
9385        let read_marks_after: Vec<_> = {
9386            let s = wal_shared.read();
9387            (0..5)
9388                .map(|i| s.runtime.read_locks[i].get_value())
9389                .collect()
9390        };
9391
9392        assert_eq!(read_marks_after[0], 0, "Slot 0 should remain 0");
9393        assert_eq!(
9394            read_marks_after[1], 0,
9395            "Slot 1 (default reader) should be reset to 0"
9396        );
9397        for (i, item) in read_marks_after.iter().take(5).skip(2).enumerate() {
9398            assert_eq!(
9399                *item, READMARK_NOT_USED,
9400                "Slot {i} should be READMARK_NOT_USED after restart",
9401            );
9402        }
9403    }
9404
9405    #[test]
9406    fn test_wal_concurrent_readers_during_checkpoint() {
9407        let (db, _path) = get_database();
9408        let conn_writer = db.connect().unwrap();
9409
9410        conn_writer
9411            .execute("create table test(id integer primary key, value text)")
9412            .unwrap();
9413        bulk_inserts(&conn_writer, 5, 10);
9414
9415        // Start multiple readers at different points
9416        let conn_r1 = db.connect().unwrap();
9417        let conn_r2 = db.connect().unwrap();
9418
9419        // R1 starts reading
9420        let r1_max_frame = {
9421            let pager = conn_r1.pager.load();
9422            let wal = pager.wal.as_ref().unwrap();
9423            wal.begin_read_tx().unwrap();
9424            wal.get_max_frame()
9425        };
9426        bulk_inserts(&conn_writer, 5, 10);
9427
9428        // R2 starts reading, sees more frames than R1
9429        let r2_max_frame = {
9430            let pager = conn_r2.pager.load();
9431            let wal = pager.wal.as_ref().unwrap();
9432            wal.begin_read_tx().unwrap();
9433            wal.get_max_frame()
9434        };
9435
9436        // try passive checkpoint, should only checkpoint up to R1's position
9437        let checkpoint_result = {
9438            let pager = conn_writer.pager.load();
9439            run_checkpoint_until_done(
9440                &pager,
9441                CheckpointMode::Passive {
9442                    upper_bound_inclusive: None,
9443                },
9444            )
9445        };
9446
9447        assert!(
9448            checkpoint_result.wal_total_backfilled < checkpoint_result.wal_max_frame,
9449            "Should not checkpoint all frames when readers are active"
9450        );
9451        assert_eq!(
9452            checkpoint_result.wal_total_backfilled, r1_max_frame,
9453            "Should have checkpointed up to R1's max frame"
9454        );
9455
9456        // Verify R2 still sees its frames
9457        assert_eq!(
9458            conn_r2.pager.load().wal.as_ref().unwrap().get_max_frame(),
9459            r2_max_frame,
9460            "Reader should maintain its snapshot"
9461        );
9462    }
9463
9464    #[test]
9465    fn test_wal_checkpoint_updates_read_marks() {
9466        let (db, _path) = get_database();
9467        let wal_shared = db.shared_wal.clone();
9468
9469        let conn = db.connect().unwrap();
9470        conn.execute("create table test(id integer primary key, value text)")
9471            .unwrap();
9472        bulk_inserts(&conn, 10, 5);
9473
9474        // get max frame before checkpoint
9475        let max_frame_before = wal_shared.read().metadata.max_frame.load(Ordering::SeqCst);
9476
9477        {
9478            let pager = conn.pager.load();
9479            let _result = run_checkpoint_until_done(
9480                &pager,
9481                CheckpointMode::Passive {
9482                    upper_bound_inclusive: None,
9483                },
9484            );
9485        }
9486
9487        // check that read mark 1 (default reader) was updated to max_frame
9488        let read_mark_1 = wal_shared.read().runtime.read_locks[1].get_value();
9489
9490        assert_eq!(
9491            read_mark_1 as u64, max_frame_before,
9492            "Read mark 1 should be updated to max frame during checkpoint"
9493        );
9494    }
9495
9496    #[test]
9497    fn test_wal_writer_blocks_restart_checkpoint() {
9498        let (db, _path) = get_database();
9499        let conn1 = db.connect().unwrap();
9500        let conn2 = db.connect().unwrap();
9501
9502        conn1
9503            .execute("create table test(id integer primary key, value text)")
9504            .unwrap();
9505        bulk_inserts(&conn1, 5, 5);
9506
9507        // start a write transaction
9508        {
9509            let pager = conn2.pager.load();
9510            let wal = pager.wal.as_ref().unwrap();
9511            let _ = wal.begin_read_tx().unwrap();
9512            wal.begin_write_tx(WalAutoActions::all_enabled()).unwrap();
9513        }
9514
9515        // should fail because writer lock is held
9516        let result = {
9517            let pager = conn1.pager.load();
9518            let wal = pager.wal.as_ref().unwrap();
9519            wal.checkpoint(&pager, CheckpointMode::Restart)
9520        };
9521
9522        assert!(
9523            matches!(result, Err(LimboError::Busy)),
9524            "Restart checkpoint should fail when write lock is held"
9525        );
9526
9527        conn2.pager.load().wal.as_ref().unwrap().end_read_tx();
9528        // release write lock
9529        conn2.pager.load().wal.as_ref().unwrap().end_write_tx();
9530
9531        // now restart should succeed
9532        let result = {
9533            let pager = conn1.pager.load();
9534            run_checkpoint_until_done(&pager, CheckpointMode::Restart)
9535        };
9536
9537        assert!(result.everything_backfilled());
9538    }
9539
9540    #[test]
9541    #[should_panic(expected = "must have a read transaction to begin a write transaction")]
9542    fn test_wal_read_transaction_required_before_write() {
9543        let (db, _path) = get_database();
9544        let conn = db.connect().unwrap();
9545
9546        conn.execute("create table test(id integer primary key, value text)")
9547            .unwrap();
9548
9549        // Attempt to start a write transaction without a read transaction
9550        let pager = conn.pager.load();
9551        let wal = pager.wal.as_ref().unwrap();
9552        let _ = wal.begin_write_tx(WalAutoActions::all_enabled());
9553    }
9554
9555    fn check_read_lock_slot(conn: &Arc<Connection>, _expected_slot: usize) -> bool {
9556        let pager = conn.pager.load();
9557        let _wal = pager.wal.as_ref().unwrap();
9558        #[cfg(debug_assertions)]
9559        {
9560            let wal_any = _wal.as_any();
9561            if let Some(wal_file) = wal_any.downcast_ref::<crate::WalFile>() {
9562                return wal_file.max_frame_read_lock_index.load(Ordering::Acquire)
9563                    == _expected_slot;
9564            }
9565        }
9566
9567        false
9568    }
9569
9570    #[test]
9571    fn test_wal_multiple_readers_at_different_frames() {
9572        let (db, _path) = get_database();
9573        let conn_writer = db.connect().unwrap();
9574
9575        conn_writer
9576            .execute("CREATE TABLE test(id INTEGER PRIMARY KEY, value TEXT)")
9577            .unwrap();
9578
9579        fn start_reader(conn: &Arc<Connection>) -> (u64, crate::Statement) {
9580            conn.execute("BEGIN").unwrap();
9581            let mut stmt = conn.prepare("SELECT * FROM test").unwrap();
9582            stmt.step().unwrap();
9583            let frame = conn.pager.load().wal.as_ref().unwrap().get_max_frame();
9584            (frame, stmt)
9585        }
9586
9587        bulk_inserts(&conn_writer, 3, 5);
9588
9589        let conn1 = &db.connect().unwrap();
9590        let (r1_frame, _stmt) = start_reader(conn1); // reader 1
9591
9592        bulk_inserts(&conn_writer, 3, 5);
9593
9594        let conn_r2 = db.connect().unwrap();
9595        let (r2_frame, _stmt2) = start_reader(&conn_r2); // reader 2
9596
9597        bulk_inserts(&conn_writer, 3, 5);
9598
9599        let conn_r3 = db.connect().unwrap();
9600        let (r3_frame, _stmt3) = start_reader(&conn_r3); // reader 3
9601
9602        assert!(r1_frame < r2_frame && r2_frame < r3_frame);
9603
9604        // passive checkpoint #1
9605        let result1 = {
9606            let pager = conn_writer.pager.load();
9607            run_checkpoint_until_done(
9608                &pager,
9609                CheckpointMode::Passive {
9610                    upper_bound_inclusive: None,
9611                },
9612            )
9613        };
9614        assert_eq!(result1.wal_total_backfilled, r1_frame);
9615
9616        // finish reader‑1
9617        conn1.execute("COMMIT").unwrap();
9618
9619        // passive checkpoint #2
9620        let result2 = {
9621            let pager = conn_writer.pager.load();
9622            run_checkpoint_until_done(
9623                &pager,
9624                CheckpointMode::Passive {
9625                    upper_bound_inclusive: None,
9626                },
9627            )
9628        };
9629        assert_eq!(
9630            result1.wal_checkpoint_backfilled + result2.wal_checkpoint_backfilled,
9631            r2_frame
9632        );
9633
9634        // verify visible rows
9635        let r2_cnt = count_test_table(&conn_r2);
9636        let r3_cnt = count_test_table(&conn_r3);
9637
9638        assert_eq!(r2_cnt, 30);
9639        assert_eq!(r3_cnt, 45);
9640    }
9641
9642    #[test]
9643    fn test_checkpoint_truncate_reset_handling() {
9644        let (db, path) = get_database();
9645        let conn = db.connect().unwrap();
9646
9647        let walpath = {
9648            let mut p = path.clone().into_os_string().into_string().unwrap();
9649            p.push_str("/test.db-wal");
9650            std::path::PathBuf::from(p)
9651        };
9652
9653        conn.execute("create table test(id integer primary key, value text)")
9654            .unwrap();
9655        bulk_inserts(&conn, 10, 10);
9656
9657        // Get size before checkpoint
9658        let size_before = std::fs::metadata(&walpath).unwrap().len();
9659        assert!(size_before > 0, "WAL file should have content");
9660
9661        // Do a TRUNCATE checkpoint
9662        {
9663            let pager = conn.pager.load();
9664            run_checkpoint_until_done(
9665                &pager,
9666                CheckpointMode::Truncate {
9667                    upper_bound_inclusive: None,
9668                },
9669            );
9670        }
9671
9672        // Check file size after truncate
9673        let size_after = std::fs::metadata(&walpath).unwrap().len();
9674        assert_eq!(size_after, 0, "WAL file should be truncated to 0 bytes");
9675
9676        // Verify we can still write to the database
9677        conn.execute("INSERT INTO test VALUES (1001, 'after-truncate')")
9678            .unwrap();
9679
9680        // Check WAL has new content
9681        let new_size = std::fs::metadata(&walpath).unwrap().len();
9682        assert!(new_size >= 32, "WAL file too small");
9683        let hdr = read_wal_header(&walpath);
9684        let expected_magic = if cfg!(target_endian = "big") {
9685            sqlite3_ondisk::WAL_MAGIC_BE
9686        } else {
9687            sqlite3_ondisk::WAL_MAGIC_LE
9688        };
9689        assert!(
9690            hdr.magic == expected_magic,
9691            "bad WAL magic: {:#X}, expected: {:#X}",
9692            hdr.magic,
9693            sqlite3_ondisk::WAL_MAGIC_BE
9694        );
9695        assert_eq!(hdr.file_format, 3007000);
9696        assert_eq!(hdr.page_size, 4096, "invalid page size");
9697        assert_eq!(hdr.checkpoint_seq, 1, "invalid checkpoint_seq");
9698        std::fs::remove_dir_all(path).unwrap();
9699    }
9700
9701    #[test]
9702    fn test_wal_checkpoint_truncate_db_file_contains_data() {
9703        let (db, path) = get_database();
9704        let conn = db.connect().unwrap();
9705
9706        let walpath = {
9707            let mut p = path.clone().into_os_string().into_string().unwrap();
9708            p.push_str("/test.db-wal");
9709            std::path::PathBuf::from(p)
9710        };
9711
9712        conn.execute("create table test(id integer primary key, value text)")
9713            .unwrap();
9714        bulk_inserts(&conn, 10, 100);
9715
9716        // Get size before checkpoint
9717        let size_before = std::fs::metadata(&walpath).unwrap().len();
9718        assert!(size_before > 0, "WAL file should have content");
9719
9720        // Do a TRUNCATE checkpoint
9721        {
9722            let pager = conn.pager.load();
9723            run_checkpoint_until_done(
9724                &pager,
9725                CheckpointMode::Truncate {
9726                    upper_bound_inclusive: None,
9727                },
9728            );
9729        }
9730
9731        // Check file size after truncate
9732        let size_after = std::fs::metadata(&walpath).unwrap().len();
9733        assert_eq!(size_after, 0, "WAL file should be truncated to 0 bytes");
9734
9735        // Verify we can still write to the database
9736        conn.execute("INSERT INTO test VALUES (1001, 'after-truncate')")
9737            .unwrap();
9738
9739        // Check WAL has new content
9740        let new_size = std::fs::metadata(&walpath).unwrap().len();
9741        assert!(new_size >= 32, "WAL file too small");
9742        let hdr = read_wal_header(&walpath);
9743        let expected_magic = if cfg!(target_endian = "big") {
9744            sqlite3_ondisk::WAL_MAGIC_BE
9745        } else {
9746            sqlite3_ondisk::WAL_MAGIC_LE
9747        };
9748        assert!(
9749            hdr.magic == expected_magic,
9750            "bad WAL magic: {:#X}, expected: {:#X}",
9751            hdr.magic,
9752            sqlite3_ondisk::WAL_MAGIC_BE
9753        );
9754        assert_eq!(hdr.file_format, 3007000);
9755        assert_eq!(hdr.page_size, 4096, "invalid page size");
9756        assert_eq!(hdr.checkpoint_seq, 1, "invalid checkpoint_seq");
9757        {
9758            let pager = conn.pager.load();
9759            run_checkpoint_until_done(
9760                &pager,
9761                CheckpointMode::Passive {
9762                    upper_bound_inclusive: None,
9763                },
9764            );
9765        }
9766        // delete the WAL file so we can read right from db and assert
9767        // that everything was backfilled properly
9768        std::fs::remove_file(&walpath).unwrap();
9769
9770        let count = count_test_table(&conn);
9771        assert_eq!(
9772            count, 1001,
9773            "we should have 1001 rows in the table all together"
9774        );
9775        std::fs::remove_dir_all(path).unwrap();
9776    }
9777
9778    fn read_wal_header(path: &std::path::Path) -> sqlite3_ondisk::WalHeader {
9779        use std::{fs::File, io::Read};
9780        let mut hdr = [0u8; 32];
9781        File::open(path).unwrap().read_exact(&mut hdr).unwrap();
9782        let be = |i| u32::from_be_bytes(hdr[i..i + 4].try_into().unwrap());
9783        sqlite3_ondisk::WalHeader {
9784            magic: be(0x00),
9785            file_format: be(0x04),
9786            page_size: be(0x08),
9787            checkpoint_seq: be(0x0C),
9788            salt_1: be(0x10),
9789            salt_2: be(0x14),
9790            checksum_1: be(0x18),
9791            checksum_2: be(0x1C),
9792        }
9793    }
9794
9795    #[test]
9796    fn test_wal_stale_snapshot_in_write_transaction() {
9797        let (db, _path) = get_database();
9798        let conn1 = db.connect().unwrap();
9799        let conn2 = db.connect().unwrap();
9800
9801        conn1
9802            .execute("create table test(id integer primary key, value text)")
9803            .unwrap();
9804        // Start a read transaction on conn2
9805        {
9806            let pager = conn2.pager.load();
9807            let wal = pager.wal.as_ref().unwrap();
9808            wal.begin_read_tx().unwrap();
9809        }
9810        // Make changes using conn1
9811        bulk_inserts(&conn1, 5, 5);
9812        // Try to start a write transaction on conn2 with a stale snapshot
9813        let result = {
9814            let pager = conn2.pager.load();
9815            let wal = pager.wal.as_ref().unwrap();
9816            wal.begin_write_tx(WalAutoActions::all_enabled())
9817        };
9818        // Should get BusySnapShot due to stale snapshot
9819        assert!(matches!(result, Err(LimboError::BusySnapshot)));
9820
9821        // End read transaction and start a fresh one
9822        {
9823            let pager = conn2.pager.load();
9824            let wal = pager.wal.as_ref().unwrap();
9825            wal.end_read_tx();
9826            wal.begin_read_tx().unwrap();
9827        }
9828        // Now write transaction should work
9829        let result = {
9830            let pager = conn2.pager.load();
9831            let wal = pager.wal.as_ref().unwrap();
9832            wal.begin_write_tx(WalAutoActions::all_enabled())
9833        };
9834        assert!(matches!(result, Ok(())));
9835    }
9836
9837    #[test]
9838    fn test_wal_readlock0_optimization_behavior() {
9839        let (db, _path) = get_database();
9840        let conn1 = db.connect().unwrap();
9841        let conn2 = db.connect().unwrap();
9842
9843        conn1
9844            .execute("create table test(id integer primary key, value text)")
9845            .unwrap();
9846        bulk_inserts(&conn1, 5, 5);
9847        // Do a full checkpoint to move all data to DB file
9848        {
9849            let pager = conn1.pager.load();
9850            run_checkpoint_until_done(
9851                &pager,
9852                CheckpointMode::Passive {
9853                    upper_bound_inclusive: None,
9854                },
9855            );
9856        }
9857
9858        // Start a read transaction on conn2
9859        {
9860            let pager = conn2.pager.load();
9861            let wal = pager.wal.as_ref().unwrap();
9862            wal.begin_read_tx().unwrap();
9863        }
9864        // should use slot 0, as everything is backfilled
9865        assert!(check_read_lock_slot(&conn2, 0));
9866        {
9867            let pager = conn1.pager.load();
9868            let wal = pager.wal.as_ref().unwrap();
9869            let frame = wal.find_frame(5, None);
9870            // since we hold readlock0, we should ignore the db file and find_frame should return none
9871            assert!(frame.is_ok_and(|f| f.is_none()));
9872        }
9873        // Try checkpoint, should fail because reader has slot 0
9874        {
9875            let pager = conn1.pager.load();
9876            let wal = pager.wal.as_ref().unwrap();
9877            let result = wal.checkpoint(&pager, CheckpointMode::Restart);
9878
9879            assert!(
9880                matches!(result, Err(LimboError::Busy)),
9881                "RESTART checkpoint should fail when a reader is using slot 0"
9882            );
9883        }
9884        // End the read transaction
9885        {
9886            let pager = conn2.pager.load();
9887            let wal = pager.wal.as_ref().unwrap();
9888            wal.end_read_tx();
9889        }
9890        {
9891            let pager = conn1.pager.load();
9892            let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
9893            assert!(
9894                result.everything_backfilled(),
9895                "RESTART checkpoint should succeed after reader releases slot 0"
9896            );
9897        }
9898    }
9899
9900    #[test]
9901    fn test_wal_full_backfills_all() {
9902        let (db, _tmp) = get_database();
9903        let conn = db.connect().unwrap();
9904
9905        // Write some data to put frames in the WAL
9906        conn.execute("create table test(id integer primary key, value text)")
9907            .unwrap();
9908        bulk_inserts(&conn, 8, 4);
9909
9910        // Ensure frames are flushed to the WAL
9911        let IOResult::Done(completions) = conn.pager.load().cacheflush().unwrap() else {
9912            panic!()
9913        };
9914        for c in completions {
9915            db.io.wait_for_completion(c).unwrap();
9916        }
9917
9918        // Snapshot the current mxFrame before running FULL
9919        let wal_shared = db.shared_wal.clone();
9920        let mx_before = wal_shared.read().metadata.max_frame.load(Ordering::SeqCst);
9921        assert!(mx_before > 0, "expected frames in WAL before FULL");
9922
9923        // Run FULL checkpoint - must backfill *all* frames up to mx_before
9924        let result = {
9925            let pager = conn.pager.load();
9926            run_checkpoint_until_done(&pager, CheckpointMode::Full)
9927        };
9928
9929        assert_eq!(result.wal_checkpoint_backfilled, mx_before);
9930        assert_eq!(result.wal_total_backfilled, mx_before);
9931    }
9932
9933    #[test]
9934    fn test_wal_full_waits_for_old_reader_then_succeeds() {
9935        let (db, _tmp) = get_database();
9936        let writer = db.connect().unwrap();
9937        let reader = db.connect().unwrap();
9938
9939        writer
9940            .execute("create table test(id integer primary key, value text)")
9941            .unwrap();
9942
9943        // First commit some data and flush (reader will snapshot here)
9944        bulk_inserts(&writer, 2, 3);
9945        let IOResult::Done(completions) = writer.pager.load().cacheflush().unwrap() else {
9946            panic!()
9947        };
9948        for c in completions {
9949            db.io.wait_for_completion(c).unwrap();
9950        }
9951
9952        // Start a read transaction pinned at the current snapshot
9953        {
9954            let pager = reader.pager.load();
9955            let wal = pager.wal.as_ref().unwrap();
9956            wal.begin_read_tx().unwrap();
9957        }
9958        let r_snapshot = {
9959            let pager = reader.pager.load();
9960            let wal = pager.wal.as_ref().unwrap();
9961            wal.get_max_frame()
9962        };
9963
9964        // Advance WAL beyond the reader's snapshot
9965        bulk_inserts(&writer, 3, 4);
9966        let IOResult::Done(completions) = writer.pager.load().cacheflush().unwrap() else {
9967            panic!()
9968        };
9969        for c in completions {
9970            db.io.wait_for_completion(c).unwrap();
9971        }
9972        let mx_now = db
9973            .shared_wal
9974            .read()
9975            .metadata
9976            .max_frame
9977            .load(Ordering::SeqCst);
9978        assert!(mx_now > r_snapshot);
9979
9980        // FULL must return Busy while a reader is stuck behind
9981        {
9982            let pager = writer.pager.load();
9983            let wal = pager.wal.as_ref().unwrap();
9984            loop {
9985                match wal.checkpoint(&pager, CheckpointMode::Full) {
9986                    Ok(IOResult::IO(io)) => {
9987                        // Drive any pending IO (should quickly become Busy or Done)
9988                        io.wait(db.io.as_ref()).unwrap();
9989                    }
9990                    Err(LimboError::Busy) => {
9991                        break;
9992                    }
9993                    other => panic!("expected Busy from FULL with old reader, got {other:?}"),
9994                }
9995            }
9996        }
9997        assert_eq!(
9998            db.shared_wal
9999                .read()
10000                .metadata
10001                .nbackfills
10002                .load(Ordering::SeqCst),
10003            0,
10004            "a FULL checkpoint that returns Busy must not publish positive nbackfills before DB sync"
10005        );
10006
10007        // Release the reader, now full mode should succeed and backfill everything
10008        {
10009            let pager = reader.pager.load();
10010            let wal = pager.wal.as_ref().unwrap();
10011            wal.end_read_tx();
10012        }
10013
10014        let result = {
10015            let pager = writer.pager.load();
10016            run_checkpoint_until_done(&pager, CheckpointMode::Full)
10017        };
10018
10019        assert_eq!(
10020            result.wal_checkpoint_backfilled, mx_now,
10021            "the successful FULL reruns from the last durable backfill point because the Busy attempt did not publish progress"
10022        );
10023        assert!(result.everything_backfilled());
10024    }
10025
10026    #[test]
10027    fn test_rollback_releases_read_lock() {
10028        let (db, _path) = get_database();
10029        let conn = db.connect().unwrap();
10030
10031        conn.execute("CREATE TABLE t(x)").unwrap();
10032        conn.execute("BEGIN").unwrap();
10033        conn.execute("INSERT INTO t VALUES(1)").unwrap();
10034
10035        {
10036            let pager = conn.pager.load();
10037            let wal = pager.wal.as_ref().unwrap();
10038            assert!(
10039                wal.holds_read_lock(),
10040                "read lock must be held during write tx"
10041            );
10042        }
10043
10044        conn.execute("ROLLBACK").unwrap();
10045
10046        {
10047            let pager = conn.pager.load();
10048            let wal = pager.wal.as_ref().unwrap();
10049            assert!(
10050                !wal.holds_read_lock(),
10051                "read lock must be released after ROLLBACK"
10052            );
10053        }
10054    }
10055
10056    #[test]
10057    fn test_rollback_releases_shared_read_lock_slot() {
10058        let (db, _path) = get_database();
10059        let conn = db.connect().unwrap();
10060
10061        conn.execute("CREATE TABLE t(x)").unwrap();
10062        conn.execute("BEGIN").unwrap();
10063        conn.execute("INSERT INTO t VALUES(1)").unwrap();
10064
10065        let locked_slots_before = {
10066            let shared = db.shared_wal.read();
10067            read_slots_with_readers(&shared)
10068        };
10069        assert_eq!(
10070            locked_slots_before.len(),
10071            1,
10072            "expected exactly one shared read-lock slot while transaction is active"
10073        );
10074
10075        conn.execute("ROLLBACK").unwrap();
10076
10077        let locked_slots_after = {
10078            let shared = db.shared_wal.read();
10079            read_slots_with_readers(&shared)
10080        };
10081        assert!(
10082            locked_slots_after.is_empty(),
10083            "ROLLBACK must release the shared read-lock slot"
10084        );
10085    }
10086
10087    #[test]
10088    fn test_rollback_releases_slot_zero_read_lock() {
10089        let (db, _path) = get_database();
10090        let conn = db.connect().unwrap();
10091
10092        conn.execute("CREATE TABLE test(id integer primary key, value text)")
10093            .unwrap();
10094        bulk_inserts(&conn, 3, 3);
10095        {
10096            let pager = conn.pager.load();
10097            let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
10098            assert!(
10099                result.everything_backfilled(),
10100                "restart checkpoint setup must fully backfill WAL"
10101            );
10102        }
10103
10104        conn.execute("BEGIN").unwrap();
10105        conn.execute("INSERT INTO test(value) VALUES('slot0')")
10106            .unwrap();
10107
10108        let locked_slots_before = {
10109            let shared = db.shared_wal.read();
10110            read_slots_with_readers(&shared)
10111        };
10112        assert_eq!(
10113            locked_slots_before,
10114            vec![0],
10115            "writer should use slot 0 when WAL is fully checkpointed"
10116        );
10117
10118        conn.execute("ROLLBACK").unwrap();
10119
10120        let locked_slots_after = {
10121            let shared = db.shared_wal.read();
10122            read_slots_with_readers(&shared)
10123        };
10124        assert!(
10125            locked_slots_after.is_empty(),
10126            "ROLLBACK must release slot 0 shared read-lock as well"
10127        );
10128    }
10129
10130    #[test]
10131    fn test_savepoint_rollback_preserves_read_lock() {
10132        let (db, _path) = get_database();
10133        let conn = db.connect().unwrap();
10134
10135        conn.execute("CREATE TABLE t(x INTEGER PRIMARY KEY)")
10136            .unwrap();
10137        conn.execute("BEGIN").unwrap();
10138        conn.execute("INSERT INTO t VALUES(1)").unwrap();
10139
10140        // Trigger a statement failure that causes savepoint rollback.
10141        // A duplicate primary key on the second INSERT will fail the
10142        // statement, rolling back to the anonymous savepoint while
10143        // keeping the write transaction open.
10144        let res = conn.execute("INSERT INTO t VALUES(1)");
10145        assert!(res.is_err(), "duplicate PK insert must fail");
10146
10147        {
10148            let pager = conn.pager.load();
10149            let wal = pager.wal.as_ref().unwrap();
10150            assert!(
10151                wal.holds_read_lock(),
10152                "read lock must still be held after savepoint rollback"
10153            );
10154            assert!(
10155                wal.holds_write_lock(),
10156                "write lock must still be held after savepoint rollback"
10157            );
10158        }
10159
10160        // The transaction should still be usable: commit succeeds and
10161        // the first insert is preserved.
10162        conn.execute("COMMIT").unwrap();
10163
10164        let mut stmt = conn.prepare("SELECT count(*) FROM t").unwrap();
10165        let mut count: i64 = 0;
10166        stmt.run_with_row_callback(|row| {
10167            count = row.get(0).unwrap();
10168            Ok(())
10169        })
10170        .unwrap();
10171        assert_eq!(count, 1, "first insert should survive savepoint rollback");
10172    }
10173
10174    #[test]
10175    fn test_savepoint_then_tx_rollback_allows_restart_checkpoint_from_other_connection() {
10176        let (db, _path) = get_database();
10177        let conn1 = db.connect().unwrap();
10178        let conn2 = db.connect().unwrap();
10179
10180        conn1
10181            .execute("CREATE TABLE test(id integer primary key, value text)")
10182            .unwrap();
10183        bulk_inserts(&conn1, 2, 2);
10184        let count_before = count_test_table(&conn1);
10185
10186        conn1.execute("BEGIN").unwrap();
10187        conn1
10188            .execute("INSERT INTO test(id, value) VALUES(1000, 'first')")
10189            .unwrap();
10190        let duplicate = conn1.execute("INSERT INTO test(id, value) VALUES(1000, 'dup')");
10191        assert!(duplicate.is_err(), "duplicate PK insert must fail");
10192
10193        {
10194            let pager = conn1.pager.load();
10195            let wal = pager.wal.as_ref().unwrap();
10196            assert!(
10197                wal.holds_read_lock(),
10198                "read lock must still be held after savepoint rollback"
10199            );
10200            assert!(
10201                wal.holds_write_lock(),
10202                "write lock must still be held after savepoint rollback"
10203            );
10204        }
10205
10206        conn1.execute("ROLLBACK").unwrap();
10207
10208        {
10209            let pager = conn1.pager.load();
10210            let wal = pager.wal.as_ref().unwrap();
10211            assert!(
10212                !wal.holds_read_lock(),
10213                "read lock must be released after transaction rollback"
10214            );
10215            assert!(
10216                !wal.holds_write_lock(),
10217                "write lock must be released after transaction rollback"
10218            );
10219        }
10220
10221        let locked_slots_after_rollback = {
10222            let shared = db.shared_wal.read();
10223            read_slots_with_readers(&shared)
10224        };
10225        assert!(
10226            locked_slots_after_rollback.is_empty(),
10227            "transaction rollback after savepoint failure must not leak shared read locks"
10228        );
10229        assert_eq!(
10230            count_test_table(&conn1),
10231            count_before,
10232            "transaction rollback should remove writes made before savepoint failure"
10233        );
10234
10235        let result = {
10236            let pager = conn2.pager.load();
10237            run_checkpoint_until_done(&pager, CheckpointMode::Restart)
10238        };
10239        assert!(
10240            result.everything_backfilled(),
10241            "restart checkpoint from another connection must succeed after full rollback"
10242        );
10243    }
10244
10245    #[test]
10246    fn test_checkpoint_succeeds_after_rollback() {
10247        let (db, _path) = get_database();
10248        let conn = db.connect().unwrap();
10249
10250        conn.execute("CREATE TABLE test(id integer primary key, value text)")
10251            .unwrap();
10252        bulk_inserts(&conn, 5, 3);
10253
10254        conn.execute("BEGIN").unwrap();
10255        conn.execute("INSERT INTO test(value) VALUES('rollback_me')")
10256            .unwrap();
10257        conn.execute("ROLLBACK").unwrap();
10258
10259        let pager = conn.pager.load();
10260        let result = run_checkpoint_until_done(&pager, CheckpointMode::Restart);
10261        assert!(
10262            result.everything_backfilled(),
10263            "checkpoint must succeed after rollback, not return Busy"
10264        );
10265    }
10266}