Skip to main content

fsqlite_core/
wal_adapter.rs

1//! Adapters bridging the WAL and pager crates at runtime.
2//!
3//! These adapters break the circular dependency between `fsqlite-pager` and
4//! `fsqlite-wal`:
5//!
6//! - [`WalBackendAdapter`] wraps `WalFile` to satisfy the pager's
7//!   [`WalBackend`] trait (pager -> WAL direction).
8//! - `CheckpointTargetAdapterRef` wraps `CheckpointPageWriter` to satisfy the
9//!   WAL executor's [`CheckpointTarget`] trait (WAL -> pager direction).
10
11use std::collections::{HashMap, HashSet};
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use fsqlite_error::{FrankenError, Result};
16use fsqlite_pager::traits::{
17    PreparedWalChecksumSeed, PreparedWalFinalizationState, PreparedWalFrameBatch,
18    PreparedWalFrameMeta, WalFrameRef, WalFuture, WalLogicalReadSnapshot,
19};
20use fsqlite_pager::{
21    CheckpointMode, CheckpointPageWriter, CheckpointResult, ParallelWalCommitReconciliation,
22    WalBackend, WalPublicationSnapshot,
23};
24use fsqlite_types::cx::Cx;
25use fsqlite_types::flags::{AccessFlags, SyncFlags, VfsOpenFlags};
26use fsqlite_types::{CommitSeq, PageNumber, PageSize};
27#[cfg(all(feature = "native", any(unix, windows)))]
28use fsqlite_vfs::DatabaseNamespaceBinding;
29use fsqlite_vfs::{SyncKind, Vfs, VfsFile, VfsWriteCompletion};
30use fsqlite_wal::checkpoint_executor::CheckpointTargetFuture;
31use fsqlite_wal::checksum::{SqliteWalChecksum, WAL_FRAME_HEADER_SIZE, WalChecksumTransform};
32use fsqlite_wal::wal::WalAppendFrameRef;
33use fsqlite_wal::{
34    CheckpointMode as WalCheckpointMode, CheckpointState, CheckpointTarget,
35    PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC, PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE,
36    ParallelWalCommitCertificate, ParallelWalDurableCertificateRecord,
37    ParallelWalFramePayloadDigestBuilder, TransactionConflictPageBaseline,
38    TransactionConflictSnapshot, WAL_HEADER_SIZE, WalFile, WalGenerationIdentity, WalHeader,
39    WalSalts, execute_checkpoint, validate_wal_header_checksum,
40};
41use tracing::debug;
42#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
43use tracing::warn;
44
45#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
46use crate::wal_fec_adapter::{FecCommitHook, FecCommitResult};
47
48#[cfg(test)]
49mod test_support {
50    use std::fmt::Debug;
51    use std::future::Future;
52
53    std::thread_local! {
54        static TEST_RUNTIME: asupersync::runtime::Runtime =
55            asupersync::runtime::RuntimeBuilder::current_thread()
56                .blocking_threads(1, 2)
57                .build()
58                .expect("WAL adapter test runtime should build");
59    }
60
61    fn block_on<F: Future>(future: F) -> F::Output {
62        TEST_RUNTIME.with(|runtime| runtime.block_on(future))
63    }
64
65    pub(super) trait FutureResultTestExt<T, E>:
66        Future<Output = std::result::Result<T, E>> + Sized
67    {
68        fn wait(self) -> std::result::Result<T, E> {
69            block_on(self)
70        }
71
72        fn expect(self, message: &str) -> T
73        where
74            E: Debug,
75        {
76            block_on(self).expect(message)
77        }
78
79        fn expect_err(self, message: &str) -> E
80        where
81            T: Debug,
82        {
83            block_on(self).expect_err(message)
84        }
85    }
86
87    impl<F, T, E> FutureResultTestExt<T, E> for F where
88        F: Future<Output = std::result::Result<T, E>> + Sized
89    {
90    }
91}
92
93#[cfg(test)]
94use self::test_support::FutureResultTestExt;
95
96// ---------------------------------------------------------------------------
97// WalBackendAdapter: WalFile -> WalBackend
98// ---------------------------------------------------------------------------
99
100/// Completes a tracked backend write as an error if it is discarded before
101/// ownership reaches the VFS source that performs the physical mutation.
102struct WalWriteCompletionPreflight<'a> {
103    completion: Option<&'a VfsWriteCompletion>,
104}
105
106impl<'a> WalWriteCompletionPreflight<'a> {
107    const fn new(completion: Option<&'a VfsWriteCompletion>) -> Self {
108        Self { completion }
109    }
110
111    fn hand_off(&mut self) {
112        self.completion = None;
113    }
114}
115
116impl Drop for WalWriteCompletionPreflight<'_> {
117    fn drop(&mut self) {
118        if let Some(completion) = self.completion {
119            completion.complete_error();
120        }
121    }
122}
123
124/// Adapter wrapping [`WalFile`] to implement the pager's [`WalBackend`] trait.
125///
126/// The pager calls `dyn WalBackend` during WAL-mode commits and page reads.
127/// This adapter delegates those calls to the concrete `WalFile<F>` from
128/// `fsqlite-wal`.
129/// Default steady-state page-index cap.
130///
131/// Normal runtime operation keeps the published WAL page index authoritative
132/// for the full visible generation. Tests can still lower this cap explicitly
133/// to exercise the bounded fallback path.
134const PAGE_INDEX_MAX_ENTRIES: usize = usize::MAX;
135
136fn sqlite_database_header_page_size(page_one: &[u8]) -> Option<u32> {
137    if page_one.len() < 18 || !page_one.starts_with(b"SQLite format 3\0") {
138        return None;
139    }
140    let encoded = u16::from_be_bytes([page_one[16], page_one[17]]);
141    let decoded = if encoded == 1 {
142        65_536
143    } else {
144        u32::from(encoded)
145    };
146    PageSize::new(decoded).map(PageSize::get)
147}
148
149/// How a visible page lookup was resolved for the current WAL generation.
150///
151/// The steady-state contract is that `Authoritative*` outcomes come from a
152/// complete per-generation index. `PartialIndexFallback*` outcomes are an
153/// explicit slow-path exception used only when a lowered cap makes the
154/// in-memory index incomplete.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156enum WalPageLookupResolution {
157    AuthoritativeHit { frame_index: usize },
158    AuthoritativeMiss,
159    PartialIndexFallbackHit { frame_index: usize },
160    PartialIndexFallbackMiss,
161}
162
163impl WalPageLookupResolution {
164    #[must_use]
165    const fn frame_index(self) -> Option<usize> {
166        match self {
167            Self::AuthoritativeHit { frame_index }
168            | Self::PartialIndexFallbackHit { frame_index } => Some(frame_index),
169            Self::AuthoritativeMiss | Self::PartialIndexFallbackMiss => None,
170        }
171    }
172
173    #[must_use]
174    const fn lookup_mode(self) -> &'static str {
175        match self {
176            Self::AuthoritativeHit { .. } | Self::AuthoritativeMiss => "authoritative_index",
177            Self::PartialIndexFallbackHit { .. } | Self::PartialIndexFallbackMiss => {
178                "partial_index_fallback"
179            }
180        }
181    }
182
183    #[must_use]
184    const fn fallback_reason(self) -> &'static str {
185        match self {
186            Self::AuthoritativeHit { .. } | Self::AuthoritativeMiss => "none",
187            Self::PartialIndexFallbackHit { .. } | Self::PartialIndexFallbackMiss => {
188                "partial_index_cap"
189            }
190        }
191    }
192}
193
194/// Immutable visibility snapshot published for one WAL generation.
195///
196/// Readers pin one of these snapshots at transaction start so page lookups stay
197/// bound to a stable committed horizon even if later commits advance the active
198/// publication plane.
199#[derive(Debug, Clone)]
200struct WalPublishedSnapshot {
201    publication_seq: u64,
202    generation: WalGenerationIdentity,
203    last_commit_frame: Option<usize>,
204    commit_count: u64,
205    page_index: Arc<HashMap<u32, usize>>,
206    index_is_partial: bool,
207}
208
209impl WalPublishedSnapshot {
210    #[must_use]
211    fn empty(publication_seq: u64, generation: WalGenerationIdentity) -> Self {
212        Self {
213            publication_seq,
214            generation,
215            last_commit_frame: None,
216            commit_count: 0,
217            page_index: Arc::new(HashMap::new()),
218            index_is_partial: false,
219        }
220    }
221}
222
223#[must_use]
224fn wal_publication_snapshot_from_published(
225    snapshot: &WalPublishedSnapshot,
226) -> WalPublicationSnapshot {
227    WalPublicationSnapshot {
228        publication_seq: snapshot.publication_seq,
229        generation: snapshot.generation,
230        last_commit_frame: snapshot.last_commit_frame,
231        commit_count: snapshot.commit_count,
232        latest_frame_entries: snapshot.page_index.len(),
233        index_is_partial: snapshot.index_is_partial,
234    }
235}
236
237#[derive(Debug, Clone, Copy)]
238struct PendingPublicationFrame {
239    page_number: u32,
240    frame_index: usize,
241    is_commit: bool,
242}
243
244pub struct WalBackendAdapter<F: VfsFile> {
245    wal: WalFile<F>,
246    /// Guard so commit-time append refresh runs only once per commit batch.
247    refresh_before_append: bool,
248    /// Active commit-published visibility plane for the current WAL generation.
249    published_snapshot: WalPublishedSnapshot,
250    /// Monotonic publication sequence assigned to the next published snapshot.
251    next_publication_seq: u64,
252    /// Transaction-bounded read snapshot pinned at `begin_transaction()`.
253    read_snapshot: Option<WalPublishedSnapshot>,
254    /// Frames appended after the last published commit horizon.
255    pending_publication_frames: Vec<PendingPublicationFrame>,
256    /// Highest commit frame staged by the append path but not yet published.
257    ///
258    /// Appends only stage this horizon; publication is deferred until
259    /// [`WalBackend::sync`] durably persists the frames. Preserved verbatim when
260    /// a sync fails so the next successful sync republishes the same batch.
261    pending_publication_commit: Option<usize>,
262    /// WAL generation observed when the pending frames were staged.
263    ///
264    /// Publication is refused if the generation moves before the sync lands,
265    /// because a checkpoint or restart invalidates the staged frame indices.
266    pending_publication_generation: Option<WalGenerationIdentity>,
267    /// Optional FEC commit hook for encoding repair symbols on commit.
268    #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
269    fec_hook: Option<FecCommitHook>,
270    /// Accumulated FEC commit results (for later sidecar persistence).
271    #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
272    fec_pending: Vec<FecCommitResult>,
273    /// Maximum number of unique pages the index will track. Defaults to a
274    /// full authoritative index in steady state. Tests can lower the cap to
275    /// exercise the partial-index fallback path explicitly.
276    page_index_cap: usize,
277}
278
279impl<F: VfsFile> WalBackendAdapter<F> {
280    /// Wrap an existing [`WalFile`] in the adapter (FEC disabled).
281    #[must_use]
282    pub fn new(wal: WalFile<F>) -> Self {
283        let generation = wal.generation_identity();
284        Self {
285            wal,
286            refresh_before_append: true,
287            published_snapshot: WalPublishedSnapshot::empty(0, generation),
288            next_publication_seq: 1,
289            read_snapshot: None,
290            pending_publication_frames: Vec::new(),
291            pending_publication_commit: None,
292            pending_publication_generation: None,
293            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
294            fec_hook: None,
295            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
296            fec_pending: Vec::new(),
297            page_index_cap: PAGE_INDEX_MAX_ENTRIES,
298        }
299    }
300
301    /// Wrap an existing [`WalFile`] with an FEC commit hook.
302    #[must_use]
303    #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
304    pub fn with_fec_hook(wal: WalFile<F>, hook: FecCommitHook) -> Self {
305        let generation = wal.generation_identity();
306        Self {
307            wal,
308            refresh_before_append: true,
309            published_snapshot: WalPublishedSnapshot::empty(0, generation),
310            next_publication_seq: 1,
311            read_snapshot: None,
312            pending_publication_frames: Vec::new(),
313            pending_publication_commit: None,
314            pending_publication_generation: None,
315            fec_hook: Some(hook),
316            fec_pending: Vec::new(),
317            page_index_cap: PAGE_INDEX_MAX_ENTRIES,
318        }
319    }
320
321    /// Whether staged, unpublished frames remain.
322    ///
323    /// Staged frames may already be durable — an intermediate sync makes them so
324    /// without committing them — but they are not yet part of the published
325    /// visibility plane. Callers that would discard, consume, or replace this
326    /// adapter must check this first: dropping the staged metadata loses the
327    /// batch, and a freshly wrapped adapter would republish those frames
328    /// straight from the WAL with no knowledge of their publication state
329    /// (GH #187).
330    #[must_use]
331    pub fn has_pending_publication(&self) -> bool {
332        self.pending_publication_commit.is_some() || !self.pending_publication_frames.is_empty()
333    }
334
335    /// Consume the adapter and return the inner [`WalFile`].
336    ///
337    /// Fails closed while staged, unpublished frames remain: consuming the
338    /// adapter discards the staged publication metadata, and the `WalFile` can
339    /// be rewrapped by an adapter that would then publish those frames without
340    /// knowing whether they were ever published or fsynced (GH #187). Drain the
341    /// batch with a successful commit sync first.
342    /// Returns [`FrankenError::Busy`]: this is a retryable ordering condition,
343    /// not database corruption.
344    pub fn into_inner(self) -> Result<WalFile<F>> {
345        if self.has_pending_publication() {
346            return Err(FrankenError::Busy);
347        }
348        Ok(self.wal)
349    }
350
351    /// Borrow the inner [`WalFile`].
352    #[must_use]
353    pub fn inner(&self) -> &WalFile<F> {
354        &self.wal
355    }
356
357    /// Mutably borrow the inner [`WalFile`] for explicit external mutation.
358    ///
359    /// Invalidates the publication plane, since the caller may mutate WAL state
360    /// arbitrarily. That invalidation discards any staged batch, so this fails
361    /// closed while one exists rather than silently dropping the commit horizon
362    /// (GH #187): after the discard, a later publish would see no pending state
363    /// and could expose frames that were never fsynced. Drain the batch with a
364    /// successful sync first.
365    pub fn inner_mut(&mut self) -> Result<&mut WalFile<F>> {
366        if self.has_pending_publication() {
367            return Err(FrankenError::Busy);
368        }
369        self.invalidate_publication();
370        Ok(&mut self.wal)
371    }
372
373    /// Capture the currently published WAL visibility summary for this handle.
374    ///
375    /// This is a cheap snapshot of the publication plane the adapter has
376    /// already materialized. Call [`Self::refresh_published_snapshot`] first if
377    /// the caller needs to bind to the latest on-disk committed prefix.
378    #[must_use]
379    pub fn published_snapshot(&self) -> WalPublicationSnapshot {
380        wal_publication_snapshot_from_published(&self.published_snapshot)
381    }
382
383    /// Capture the currently pinned read snapshot, if this handle has one.
384    #[must_use]
385    pub fn pinned_read_snapshot(&self) -> Option<WalPublicationSnapshot> {
386        self.read_snapshot
387            .as_ref()
388            .map(wal_publication_snapshot_from_published)
389    }
390
391    /// Refresh this handle from disk and republish the latest committed WAL
392    /// visibility summary without pinning a read transaction.
393    pub async fn refresh_published_snapshot(&mut self, cx: &Cx) -> Result<WalPublicationSnapshot> {
394        self.wal.refresh(cx).await?;
395        self.publish_latest_committed_snapshot(cx, "refresh_published_snapshot")
396            .await?;
397        Ok(self.published_snapshot())
398    }
399
400    /// Discard published and pinned snapshots after external WAL mutation.
401    fn invalidate_publication(&mut self) {
402        self.read_snapshot = None;
403        self.discard_pending_publication();
404        self.published_snapshot = WalPublishedSnapshot::empty(
405            self.published_snapshot.publication_seq,
406            self.published_snapshot.generation,
407        );
408    }
409
410    /// Publish an immutable visibility snapshot for the current committed WAL prefix.
411    ///
412    /// The commit path advances this plane directly, and readers pin a clone of
413    /// the published snapshot instead of mutating shared lookup state under an
414    /// active transaction.
415    async fn publish_visible_snapshot(
416        &mut self,
417        cx: &Cx,
418        last_commit_frame: Option<usize>,
419        scenario_id: &'static str,
420    ) -> Result<()> {
421        let generation = self.wal.generation_identity();
422        if self.published_snapshot.generation == generation
423            && self.published_snapshot.last_commit_frame == last_commit_frame
424        {
425            return Ok(());
426        }
427
428        let previous_generation = self.published_snapshot.generation;
429        let previous_last_commit = self.published_snapshot.last_commit_frame;
430        let previous_commit_count = if previous_generation == generation {
431            self.published_snapshot.commit_count
432        } else {
433            0
434        };
435        let mut page_index = if previous_generation == generation {
436            std::mem::replace(
437                &mut self.published_snapshot.page_index,
438                Arc::new(HashMap::new()),
439            )
440        } else {
441            Arc::new(HashMap::new())
442        };
443        let mut index_is_partial = if previous_generation == generation {
444            self.published_snapshot.index_is_partial
445        } else {
446            false
447        };
448
449        let frame_delta_count = match (previous_last_commit, last_commit_frame) {
450            (Some(prev), Some(curr)) if curr >= prev => curr.saturating_sub(prev),
451            (Some(_) | None, Some(curr)) => curr.saturating_add(1),
452            (Some(prev), None) => prev.saturating_add(1),
453            (None, None) => 0,
454        };
455
456        let scan_result = match last_commit_frame {
457            None => {
458                Arc::make_mut(&mut page_index).clear();
459                index_is_partial = false;
460                Ok(0)
461            }
462            Some(current_last_commit) => {
463                let (start, base_commit_count) =
464                    match (previous_generation == generation, previous_last_commit) {
465                        (true, Some(previous_last_commit))
466                            if previous_last_commit < current_last_commit =>
467                        {
468                            (
469                                previous_last_commit.saturating_add(1),
470                                previous_commit_count,
471                            )
472                        }
473                        (true, Some(previous_last_commit))
474                            if previous_last_commit == current_last_commit =>
475                        {
476                            (current_last_commit.saturating_add(1), previous_commit_count)
477                        }
478                        _ => {
479                            Arc::make_mut(&mut page_index).clear();
480                            index_is_partial = false;
481                            (0, 0)
482                        }
483                    };
484                if start <= current_last_commit {
485                    self.index_range_and_count_commits(
486                        cx,
487                        Arc::make_mut(&mut page_index),
488                        &mut index_is_partial,
489                        start,
490                        current_last_commit,
491                    )
492                    .await
493                    .map(|delta| base_commit_count.saturating_add(delta))
494                } else {
495                    Ok(base_commit_count)
496                }
497            }
498        };
499        let commit_count = match scan_result {
500            Ok(commit_count) => commit_count,
501            Err(error) => {
502                if previous_generation == generation {
503                    self.published_snapshot.page_index = page_index;
504                }
505                return Err(error);
506            }
507        };
508
509        let publication_seq = self.next_publication_seq;
510        self.next_publication_seq = self.next_publication_seq.saturating_add(1);
511        let latest_frame_entries = page_index.len();
512        self.published_snapshot = WalPublishedSnapshot {
513            publication_seq,
514            generation,
515            last_commit_frame,
516            commit_count,
517            page_index,
518            index_is_partial,
519        };
520
521        tracing::trace!(
522            target: "fsqlite.wal_publication",
523            trace_id = cx.trace_id(),
524            run_id = "wal-publication",
525            scenario_id,
526            wal_generation = generation.checkpoint_seq,
527            wal_salt1 = generation.salts.salt1,
528            wal_salt2 = generation.salts.salt2,
529            publication_seq,
530            frame_delta_count,
531            latest_frame_entries,
532            snapshot_age = 0_u64,
533            lookup_mode = "published_visibility_map",
534            fallback_reason = if index_is_partial {
535                "partial_index_cap"
536            } else {
537                "none"
538            },
539            "published WAL visibility snapshot"
540        );
541
542        Ok(())
543    }
544
545    /// Resolve the most recent visible frame for `page_number`.
546    ///
547    /// The normal contract is `Authoritative*`: the published page index fully
548    /// covers the visible WAL generation, so a miss means the page is absent.
549    /// `PartialIndexFallback*` is a bounded slow-path used only when the capped
550    /// index is known to be incomplete.
551    async fn resolve_visible_frame(
552        &self,
553        cx: &Cx,
554        snapshot: &WalPublishedSnapshot,
555        page_number: u32,
556    ) -> Result<WalPageLookupResolution> {
557        match snapshot.page_index.get(&page_number) {
558            Some(&frame_index) => Ok(WalPageLookupResolution::AuthoritativeHit { frame_index }),
559            None if !snapshot.index_is_partial => Ok(WalPageLookupResolution::AuthoritativeMiss),
560            None => match snapshot.last_commit_frame {
561                Some(last_commit_frame) => {
562                    match self
563                        .scan_backwards_for_page(cx, page_number, last_commit_frame)
564                        .await?
565                    {
566                        Some(frame_index) => {
567                            Ok(WalPageLookupResolution::PartialIndexFallbackHit { frame_index })
568                        }
569                        None => Ok(WalPageLookupResolution::PartialIndexFallbackMiss),
570                    }
571                }
572                None => Ok(WalPageLookupResolution::AuthoritativeMiss),
573            },
574        }
575    }
576
577    /// Scan frame headers from `start..=end` (inclusive), populate the page index,
578    /// and count commit frames in the same pass.
579    ///
580    /// Since we scan forward, later frames naturally overwrite earlier entries
581    /// for the same page number, ensuring "newest frame wins" semantics.
582    async fn index_range_and_count_commits(
583        &self,
584        cx: &Cx,
585        page_index: &mut HashMap<u32, usize>,
586        index_is_partial: &mut bool,
587        start: usize,
588        end: usize,
589    ) -> Result<u64> {
590        if start > end {
591            return Ok(0);
592        }
593
594        let mut commit_count = 0_u64;
595        for frame_index in start..=end {
596            let header = self.wal.read_frame_header(cx, frame_index).await?;
597            // Only insert if we haven't hit the capacity cap, or if this page
598            // is already tracked (update is free).
599            if page_index.len() < self.page_index_cap
600                || page_index.contains_key(&header.page_number)
601            {
602                page_index.insert(header.page_number, frame_index);
603            } else {
604                // A page was dropped because the index is full -- mark it as
605                // partial so that `read_page` knows a HashMap miss cannot be
606                // trusted and must fall back to a linear scan.
607                *index_is_partial = true;
608            }
609            if header.is_commit() {
610                commit_count = commit_count.saturating_add(1);
611            }
612        }
613        Ok(commit_count)
614    }
615
616    /// Backwards linear scan of committed frames to find a page that was not
617    /// captured by the capped page index.
618    ///
619    /// Scans from `last_commit_frame` down to frame 0 and returns the index
620    /// of the first (i.e., most recent) frame containing `page_number`, or
621    /// `None` if the page is not in the WAL at all.
622    async fn scan_backwards_for_page(
623        &self,
624        cx: &Cx,
625        page_number: u32,
626        last_commit_frame: usize,
627    ) -> Result<Option<usize>> {
628        for frame_index in (0..=last_commit_frame).rev() {
629            let header = self.wal.read_frame_header(cx, frame_index).await?;
630            if header.page_number == page_number {
631                return Ok(Some(frame_index));
632            }
633        }
634        Ok(None)
635    }
636
637    /// Take any pending FEC commit results for sidecar persistence.
638    #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
639    pub fn take_fec_pending(&mut self) -> Vec<FecCommitResult> {
640        std::mem::take(&mut self.fec_pending)
641    }
642
643    /// Whether FEC encoding is active.
644    #[must_use]
645    #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
646    pub fn fec_enabled(&self) -> bool {
647        self.fec_hook
648            .as_ref()
649            .is_some_and(FecCommitHook::is_enabled)
650    }
651
652    /// Discard buffered FEC pages (e.g. on transaction rollback).
653    #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
654    pub fn fec_discard(&mut self) {
655        if let Some(hook) = &mut self.fec_hook {
656            hook.discard_buffered();
657        }
658    }
659
660    /// Override the page index capacity (for testing only).
661    #[cfg(test)]
662    fn set_page_index_cap(&mut self, cap: usize) {
663        self.page_index_cap = cap;
664        // Invalidate so the next read rebuilds with the new cap.
665        self.invalidate_publication();
666    }
667
668    #[must_use]
669    fn current_prepared_finalization_state(&self) -> PreparedWalFinalizationState {
670        let generation = self.wal.generation_identity();
671        let seed = self.wal.running_checksum();
672        PreparedWalFinalizationState {
673            checkpoint_seq: generation.checkpoint_seq,
674            salt1: generation.salts.salt1,
675            salt2: generation.salts.salt2,
676            start_frame_index: self.wal.frame_count(),
677            seed: PreparedWalChecksumSeed {
678                s1: seed.s1,
679                s2: seed.s2,
680            },
681        }
682    }
683
684    #[must_use]
685    fn prepared_batch_matches_current_state(&self, prepared: &PreparedWalFrameBatch) -> bool {
686        prepared
687            .finalized_for
688            .is_some_and(|state| state == self.current_prepared_finalization_state())
689    }
690
691    async fn prepared_batch_matches_disk_state(
692        &self,
693        cx: &Cx,
694        prepared: &PreparedWalFrameBatch,
695    ) -> Result<bool> {
696        let Some(state) = prepared.finalized_for else {
697            return Ok(false);
698        };
699        let generation = WalGenerationIdentity {
700            checkpoint_seq: state.checkpoint_seq,
701            salts: fsqlite_wal::checksum::WalSalts {
702                salt1: state.salt1,
703                salt2: state.salt2,
704            },
705        };
706        self.wal
707            .prepared_append_window_still_current(cx, generation, state.start_frame_index)
708            .await
709    }
710
711    fn checksum_transforms_for_prepared(
712        prepared: &PreparedWalFrameBatch,
713    ) -> Vec<WalChecksumTransform> {
714        prepared
715            .checksum_transforms
716            .iter()
717            .map(|transform| WalChecksumTransform {
718                a11: transform.a11,
719                a12: transform.a12,
720                a21: transform.a21,
721                a22: transform.a22,
722                c1: transform.c1,
723                c2: transform.c2,
724            })
725            .collect()
726    }
727
728    fn finalize_prepared_batch_against_current_state(
729        &self,
730        prepared: &mut PreparedWalFrameBatch,
731    ) -> Result<()> {
732        let checksum_transforms = Self::checksum_transforms_for_prepared(prepared);
733        let final_running_checksum = self
734            .wal
735            .finalize_prepared_frame_bytes(&mut prepared.frame_bytes, &checksum_transforms)?;
736        prepared.finalized_for = Some(self.current_prepared_finalization_state());
737        prepared.finalized_running_checksum = Some(PreparedWalChecksumSeed {
738            s1: final_running_checksum.s1,
739            s2: final_running_checksum.s2,
740        });
741        Ok(())
742    }
743
744    fn finalized_running_checksum(prepared: &PreparedWalFrameBatch) -> Result<SqliteWalChecksum> {
745        let Some(checksum) = prepared.finalized_running_checksum else {
746            return Err(FrankenError::internal(
747                "prepared WAL batch missing finalized running checksum",
748            ));
749        };
750        Ok(SqliteWalChecksum {
751            s1: checksum.s1,
752            s2: checksum.s2,
753        })
754    }
755
756    async fn publish_latest_committed_snapshot(
757        &mut self,
758        cx: &Cx,
759        scenario_id: &'static str,
760    ) -> Result<()> {
761        let last_commit_frame = self.wal.last_commit_frame(cx)?;
762        // While a local batch is staged, the WAL's own commit horizon includes
763        // frames this handle appended but has not yet fsynced. Refresh and
764        // unpinned read paths must not expose them, so clamp to the durable
765        // prefix. With nothing staged the horizon is used unchanged, preserving
766        // publication of commits made durable elsewhere.
767        let last_commit_frame = if self.pending_publication_commit.is_some() {
768            let durable_frames = self.wal.last_fsynced_frame_count();
769            last_commit_frame.filter(|frame| {
770                frame
771                    .checked_add(1)
772                    .is_some_and(|frame_count| frame_count <= durable_frames)
773            })
774        } else {
775            last_commit_frame
776        };
777        self.publish_visible_snapshot(cx, last_commit_frame, scenario_id)
778            .await
779    }
780
781    async fn synchronize_publication_before_append(
782        &mut self,
783        cx: &Cx,
784        scenario_id: &'static str,
785    ) -> Result<()> {
786        // Fail closed. A non-empty staged batch means a durability barrier has
787        // not completed: either no sync has run, or one failed. Discarding it
788        // here would silently drop the horizon, and republishing straight from
789        // the WAL would expose frames that were never fsynced. Any path that
790        // sets `refresh_before_append` while a batch is staged — a failed sync
791        // followed by `begin_transaction`, or by `checkpoint` — funnels through
792        // here, so guarding this single choke point covers all of them.
793        if self.has_pending_publication() {
794            return Err(FrankenError::Busy);
795        }
796        self.wal.refresh(cx).await?;
797        self.discard_pending_publication();
798        self.publish_latest_committed_snapshot(cx, scenario_id)
799            .await
800    }
801
802    /// Drop every staged frame and the horizon that would have published it.
803    fn discard_pending_publication(&mut self) {
804        self.pending_publication_frames.clear();
805        self.pending_publication_commit = None;
806        self.pending_publication_generation = None;
807    }
808
809    /// Stage a commit horizon for publication without advancing visibility.
810    ///
811    /// Appends never publish directly: the frames may sit in the host page cache
812    /// with no durable backing, so exposing them to readers would surface a
813    /// commit that a crash could still erase. The horizon is retained until
814    /// [`WalBackend::sync`] persists the batch.
815    fn stage_pending_commit_publication(&mut self, last_commit_frame: usize) -> Result<()> {
816        let generation = self.wal.generation_identity();
817        // Fail closed rather than silently overwriting: staged frame indices are
818        // only meaningful within one generation, so a mixed-generation batch
819        // must never be merged into a single publishable horizon.
820        if self
821            .pending_publication_generation
822            .is_some_and(|staged| staged != generation)
823        {
824            return Err(FrankenError::WalCorrupt {
825                detail: "cannot stage a commit horizon across differing WAL generations".to_owned(),
826            });
827        }
828        let staged = self
829            .pending_publication_commit
830            .map_or(last_commit_frame, |staged| staged.max(last_commit_frame));
831        self.pending_publication_commit = Some(staged);
832        self.pending_publication_generation = Some(generation);
833        Ok(())
834    }
835
836    /// Confirm the staged horizon is still publishable against the live WAL.
837    ///
838    /// Refuses when the generation moved (a checkpoint or restart reindexes the
839    /// WAL, invalidating staged frame indices), when the WAL is shorter than the
840    /// staged horizon, or when the WAL does not yet report the staged commit.
841    /// Callers must leave the pending state untouched on refusal so a later sync
842    /// can retry the same batch.
843    fn assert_pending_horizon_matches_wal(
844        &mut self,
845        cx: &Cx,
846        last_commit_frame: usize,
847    ) -> Result<()> {
848        let generation = self.wal.generation_identity();
849        if self
850            .pending_publication_generation
851            .is_some_and(|staged| staged != generation)
852        {
853            return Err(FrankenError::WalCorrupt {
854                detail: "WAL generation changed before the staged commit horizon was published"
855                    .to_owned(),
856            });
857        }
858
859        let frame_count = self.wal.frame_count();
860        if last_commit_frame >= frame_count {
861            return Err(FrankenError::WalCorrupt {
862                detail: format!(
863                    "staged commit horizon {last_commit_frame} exceeds WAL frame count {frame_count}"
864                ),
865            });
866        }
867
868        let live_last_commit = self.wal.last_commit_frame(cx)?;
869        if live_last_commit.is_none_or(|live| live < last_commit_frame) {
870            return Err(FrankenError::WalCorrupt {
871                detail: format!(
872                    "WAL does not report staged commit horizon {last_commit_frame} as committed"
873                ),
874            });
875        }
876
877        if self
878            .pending_publication_frames
879            .iter()
880            .any(|frame| frame.frame_index >= frame_count)
881        {
882            return Err(FrankenError::WalCorrupt {
883                detail: format!(
884                    "a staged publication frame lies beyond WAL frame count {frame_count}"
885                ),
886            });
887        }
888
889        Ok(())
890    }
891
892    fn assert_publish_safe(&mut self, cx: &Cx, last_commit_frame: usize) -> Result<()> {
893        self.assert_pending_horizon_matches_wal(cx, last_commit_frame)?;
894
895        // Delegate to the WAL's own durability tracker rather than duplicating
896        // it: only it knows how far a successful fsync actually reached.
897        let publish_frame_count =
898            last_commit_frame
899                .checked_add(1)
900                .ok_or_else(|| FrankenError::WalCorrupt {
901                    detail: "staged commit horizon overflows the publishable frame count"
902                        .to_owned(),
903                })?;
904        self.wal.assert_publish_safe(publish_frame_count)?;
905
906        Ok(())
907    }
908
909    /// Publish a logically authorized `synchronous=NORMAL` commit without
910    /// claiming that an fsync occurred.
911    ///
912    /// The pager calls this only after the parallel-WAL certificate and both
913    /// tracked write completions are terminal. A failed-sync path never reaches
914    /// this hook, so its pending horizon remains fail-closed for a later retry.
915    fn publish_authorized_deferred_commit(&mut self, cx: &Cx) -> Result<()> {
916        let Some(last_commit_frame) = self.pending_publication_commit else {
917            return Ok(());
918        };
919        self.assert_pending_horizon_matches_wal(cx, last_commit_frame)?;
920        self.publish_pending_commit_snapshot(cx, last_commit_frame, "authorized_deferred_commit");
921        self.pending_publication_commit = None;
922        self.pending_publication_generation = None;
923        Ok(())
924    }
925
926    /// Publish the staged commit horizon after a successful durability barrier.
927    ///
928    /// Fully synchronous: the staged frames already carry every page/frame pair
929    /// the snapshot needs, so no WAL scan — and therefore no async I/O — is
930    /// required. On refusal or failure the pending state is preserved verbatim
931    /// so the next successful sync retries the identical batch.
932    fn publish_pending_after_sync(&mut self, cx: &Cx) -> Result<()> {
933        let Some(last_commit_frame) = self.pending_publication_commit else {
934            return Ok(());
935        };
936        self.assert_publish_safe(cx, last_commit_frame)?;
937        self.publish_pending_commit_snapshot(cx, last_commit_frame, "sync_publish_commit");
938        self.pending_publication_commit = None;
939        self.pending_publication_generation = None;
940        Ok(())
941    }
942
943    fn record_appended_frames<I>(&mut self, start_frame_index: usize, frames: I) -> Option<usize>
944    where
945        I: IntoIterator<Item = (u32, u32)>,
946    {
947        let mut last_commit_frame = None;
948        for (offset, (page_number, db_size_if_commit)) in frames.into_iter().enumerate() {
949            let frame_index = start_frame_index.saturating_add(offset);
950            self.pending_publication_frames
951                .push(PendingPublicationFrame {
952                    page_number,
953                    frame_index,
954                    is_commit: db_size_if_commit != 0,
955                });
956            if db_size_if_commit != 0 {
957                last_commit_frame = Some(frame_index);
958            }
959        }
960        last_commit_frame
961    }
962
963    /// Install a published snapshot from staged frames alone.
964    ///
965    /// Deliberately synchronous. Every page/frame pair needed for the delta is
966    /// already staged by `record_appended_frames`, so this never scans the WAL
967    /// and never performs I/O; that keeps it callable from the synchronous
968    /// [`WalBackend::sync`] path without a runtime or `block_on`.
969    fn publish_pending_commit_snapshot(
970        &mut self,
971        cx: &Cx,
972        last_commit_frame: usize,
973        scenario_id: &'static str,
974    ) {
975        let generation = self.wal.generation_identity();
976        let previous_last_commit = self.published_snapshot.last_commit_frame;
977        let can_extend_previous = self.published_snapshot.generation == generation
978            && self
979                .published_snapshot
980                .last_commit_frame
981                .is_none_or(|previous_last_commit| previous_last_commit < last_commit_frame);
982        let mut page_index = if can_extend_previous {
983            std::mem::replace(
984                &mut self.published_snapshot.page_index,
985                Arc::new(HashMap::new()),
986            )
987        } else {
988            Arc::new(HashMap::new())
989        };
990        let mut index_is_partial = if can_extend_previous {
991            self.published_snapshot.index_is_partial
992        } else {
993            false
994        };
995        let previous_last_commit = if can_extend_previous {
996            previous_last_commit
997        } else {
998            None
999        };
1000        let previous_commit_count = if can_extend_previous {
1001            self.published_snapshot.commit_count
1002        } else {
1003            0
1004        };
1005
1006        let mut frame_delta_count = 0_usize;
1007        let mut commit_delta_count = 0_u64;
1008        for frame in &self.pending_publication_frames {
1009            if previous_last_commit
1010                .is_some_and(|previous_last_commit| frame.frame_index <= previous_last_commit)
1011                || frame.frame_index > last_commit_frame
1012            {
1013                continue;
1014            }
1015
1016            frame_delta_count = frame_delta_count.saturating_add(1);
1017            let page_index_map = Arc::make_mut(&mut page_index);
1018            if page_index_map.len() < self.page_index_cap
1019                || page_index_map.contains_key(&frame.page_number)
1020            {
1021                page_index_map.insert(frame.page_number, frame.frame_index);
1022            } else {
1023                index_is_partial = true;
1024            }
1025            if frame.is_commit {
1026                commit_delta_count = commit_delta_count.saturating_add(1);
1027            }
1028        }
1029
1030        if frame_delta_count == 0 {
1031            // No staged frame advances the horizon, which can only happen when
1032            // the published plane already covers `last_commit_frame` (an
1033            // extendable plane always contains the staged commit frame itself).
1034            // Rebuilding here would need a WAL scan, and this path must stay
1035            // synchronous, so leave visibility untouched. External refresh paths
1036            // retain the async rebuild for the cases that genuinely need it.
1037            if can_extend_previous {
1038                self.published_snapshot.page_index = page_index;
1039            }
1040            self.pending_publication_frames.clear();
1041            return;
1042        }
1043
1044        let publication_seq = self.next_publication_seq;
1045        self.next_publication_seq = self.next_publication_seq.saturating_add(1);
1046        let latest_frame_entries = page_index.len();
1047        self.published_snapshot = WalPublishedSnapshot {
1048            publication_seq,
1049            generation,
1050            last_commit_frame: Some(last_commit_frame),
1051            commit_count: previous_commit_count.saturating_add(commit_delta_count),
1052            page_index,
1053            index_is_partial,
1054        };
1055        self.pending_publication_frames.clear();
1056
1057        tracing::trace!(
1058            target: "fsqlite.wal_publication",
1059            trace_id = cx.trace_id(),
1060            run_id = "wal-publication",
1061            scenario_id,
1062            wal_generation = generation.checkpoint_seq,
1063            wal_salt1 = generation.salts.salt1,
1064            wal_salt2 = generation.salts.salt2,
1065            publication_seq,
1066            frame_delta_count,
1067            latest_frame_entries,
1068            snapshot_age = 0_u64,
1069            lookup_mode = "published_visibility_map",
1070            fallback_reason = if index_is_partial {
1071                "partial_index_cap"
1072            } else {
1073                "none"
1074            },
1075            "published WAL visibility snapshot from commit path"
1076        );
1077    }
1078}
1079
1080/// Convert pager checkpoint mode to WAL checkpoint mode.
1081fn to_wal_mode(mode: CheckpointMode) -> WalCheckpointMode {
1082    match mode {
1083        CheckpointMode::Passive => WalCheckpointMode::Passive,
1084        CheckpointMode::Full => WalCheckpointMode::Full,
1085        CheckpointMode::Restart => WalCheckpointMode::Restart,
1086        CheckpointMode::Truncate => WalCheckpointMode::Truncate,
1087    }
1088}
1089
1090impl<F: VfsFile> WalBackend for WalBackendAdapter<F> {
1091    fn begin_transaction<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
1092        Box::pin(async move {
1093            // Reject at the earliest illegal transition: before `wal.refresh`,
1094            // before pinning `read_snapshot`, and before re-arming
1095            // `refresh_before_append`. Beginning a transaction on top of staged,
1096            // unpublished frames would otherwise leave a half-transition whose
1097            // only symptom is a later append failure.
1098            if self.has_pending_publication() {
1099                return Err(FrankenError::Busy);
1100            }
1101            // Establish a transaction-bounded snapshot once, instead of doing an
1102            // expensive refresh for every page read.
1103            self.wal.refresh(cx).await?;
1104            self.publish_latest_committed_snapshot(cx, "begin_transaction")
1105                .await?;
1106            self.read_snapshot = Some(self.published_snapshot.clone());
1107            self.refresh_before_append = true;
1108            Ok(())
1109        })
1110    }
1111
1112    fn published_snapshot(&self) -> Option<WalPublicationSnapshot> {
1113        Some(Self::published_snapshot(self))
1114    }
1115
1116    fn pinned_read_snapshot(&self) -> Option<WalPublicationSnapshot> {
1117        Self::pinned_read_snapshot(self)
1118    }
1119
1120    fn refresh_published_snapshot<'a>(
1121        &'a mut self,
1122        cx: &'a Cx,
1123    ) -> WalFuture<'a, Option<WalPublicationSnapshot>> {
1124        Box::pin(async move { Self::refresh_published_snapshot(self, cx).await.map(Some) })
1125    }
1126
1127    fn publish_authorized_deferred_commit<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
1128        Box::pin(async move { Self::publish_authorized_deferred_commit(self, cx) })
1129    }
1130
1131    fn append_frame<'a>(
1132        &'a mut self,
1133        cx: &'a Cx,
1134        page_number: u32,
1135        page_data: &'a [u8],
1136        db_size_if_commit: u32,
1137    ) -> WalFuture<'a, ()> {
1138        Box::pin(async move {
1139            if self.refresh_before_append {
1140                // Refresh and synchronize the published base snapshot once before
1141                // the commit batch starts, then publish local frame deltas directly
1142                // from the append path.
1143                self.synchronize_publication_before_append(cx, "append_frame_pre_refresh")
1144                    .await?;
1145            }
1146            let start_frame_index = self.wal.frame_count();
1147            self.wal
1148                .append_frame(cx, page_number, page_data, db_size_if_commit)
1149                .await?;
1150            self.refresh_before_append = false;
1151            let last_commit_frame =
1152                self.record_appended_frames(start_frame_index, [(page_number, db_size_if_commit)]);
1153
1154            // Feed the frame to the FEC hook.  On commit, it encodes repair
1155            // symbols and stores them for later sidecar persistence.
1156            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1157            if let Some(hook) = &mut self.fec_hook {
1158                match hook.on_frame(cx, page_number, page_data, db_size_if_commit) {
1159                    Ok(Some(result)) => {
1160                        debug!(
1161                            pages = result.page_numbers.len(),
1162                            k_source = result.k_source,
1163                            symbols = result.symbols.len(),
1164                            "FEC commit group encoded"
1165                        );
1166                        self.fec_pending.push(result);
1167                    }
1168                    Ok(None) => {}
1169                    Err(e) => {
1170                        // FEC encoding failure is non-fatal -- log and continue.
1171                        warn!(error = %e, "FEC encoding failed; commit proceeds without repair symbols");
1172                    }
1173                }
1174            }
1175
1176            if let Some(last_commit_frame) = last_commit_frame {
1177                // Stage only: the frames are not durable until `sync`, so
1178                // publishing here would expose a commit a crash could erase.
1179                self.stage_pending_commit_publication(last_commit_frame)?;
1180            }
1181
1182            Ok(())
1183        })
1184    }
1185
1186    fn append_frames<'a>(
1187        &'a mut self,
1188        cx: &'a Cx,
1189        frames: &'a [WalFrameRef<'a>],
1190    ) -> WalFuture<'a, ()> {
1191        Box::pin(async move {
1192            if frames.is_empty() {
1193                return Ok(());
1194            }
1195
1196            if self.refresh_before_append {
1197                self.synchronize_publication_before_append(cx, "append_frames_pre_refresh")
1198                    .await?;
1199            }
1200
1201            let start_frame_index = self.wal.frame_count();
1202            let mut wal_frames = Vec::with_capacity(frames.len());
1203            for frame in frames {
1204                wal_frames.push(WalAppendFrameRef {
1205                    page_number: frame.page_number,
1206                    page_data: frame.page_data,
1207                    db_size_if_commit: frame.db_size_if_commit,
1208                });
1209            }
1210            self.wal.append_frames(cx, &wal_frames).await?;
1211            self.refresh_before_append = false;
1212            let last_commit_frame = self.record_appended_frames(
1213                start_frame_index,
1214                frames
1215                    .iter()
1216                    .map(|frame| (frame.page_number, frame.db_size_if_commit)),
1217            );
1218
1219            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1220            if let Some(hook) = &mut self.fec_hook {
1221                for frame in frames {
1222                    match hook.on_frame(
1223                        cx,
1224                        frame.page_number,
1225                        frame.page_data,
1226                        frame.db_size_if_commit,
1227                    ) {
1228                        Ok(Some(result)) => {
1229                            debug!(
1230                                pages = result.page_numbers.len(),
1231                                k_source = result.k_source,
1232                                symbols = result.symbols.len(),
1233                                "FEC commit group encoded"
1234                            );
1235                            self.fec_pending.push(result);
1236                        }
1237                        Ok(None) => {}
1238                        Err(e) => {
1239                            warn!(
1240                                error = %e,
1241                                "FEC encoding failed; commit proceeds without repair symbols"
1242                            );
1243                        }
1244                    }
1245                }
1246            }
1247
1248            if let Some(last_commit_frame) = last_commit_frame {
1249                // Stage only: publication is deferred to the durability barrier.
1250                self.stage_pending_commit_publication(last_commit_frame)?;
1251            }
1252
1253            Ok(())
1254        })
1255    }
1256
1257    fn append_frames_tracked<'a>(
1258        &'a mut self,
1259        cx: &'a Cx,
1260        frames: &'a [WalFrameRef<'a>],
1261        completion: VfsWriteCompletion,
1262    ) -> WalFuture<'a, ()> {
1263        Box::pin(async move {
1264            let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
1265            if frames.is_empty() {
1266                completion.complete_success();
1267                preflight.hand_off();
1268                return Ok(());
1269            }
1270
1271            if self.refresh_before_append {
1272                self.synchronize_publication_before_append(cx, "append_frames_pre_refresh")
1273                    .await?;
1274            }
1275
1276            let start_frame_index = self.wal.frame_count();
1277            let mut wal_frames = Vec::with_capacity(frames.len());
1278            for frame in frames {
1279                wal_frames.push(WalAppendFrameRef {
1280                    page_number: frame.page_number,
1281                    page_data: frame.page_data,
1282                    db_size_if_commit: frame.db_size_if_commit,
1283                });
1284            }
1285            preflight.hand_off();
1286            drop(preflight);
1287            self.wal
1288                .append_frames_tracked(cx, &wal_frames, completion)
1289                .await?;
1290            self.refresh_before_append = false;
1291            let last_commit_frame = self.record_appended_frames(
1292                start_frame_index,
1293                frames
1294                    .iter()
1295                    .map(|frame| (frame.page_number, frame.db_size_if_commit)),
1296            );
1297
1298            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1299            if let Some(hook) = &mut self.fec_hook {
1300                for frame in frames {
1301                    match hook.on_frame(
1302                        cx,
1303                        frame.page_number,
1304                        frame.page_data,
1305                        frame.db_size_if_commit,
1306                    ) {
1307                        Ok(Some(result)) => {
1308                            debug!(
1309                                pages = result.page_numbers.len(),
1310                                k_source = result.k_source,
1311                                symbols = result.symbols.len(),
1312                                "FEC commit group encoded"
1313                            );
1314                            self.fec_pending.push(result);
1315                        }
1316                        Ok(None) => {}
1317                        Err(e) => {
1318                            warn!(
1319                                error = %e,
1320                                "FEC encoding failed; commit proceeds without repair symbols"
1321                            );
1322                        }
1323                    }
1324                }
1325            }
1326
1327            if let Some(last_commit_frame) = last_commit_frame {
1328                // Stage only: publication is deferred to the durability barrier.
1329                self.stage_pending_commit_publication(last_commit_frame)?;
1330            }
1331
1332            Ok(())
1333        })
1334    }
1335
1336    fn prepare_append_frames(
1337        &self,
1338        frames: &[WalFrameRef<'_>],
1339    ) -> Result<Option<PreparedWalFrameBatch>> {
1340        if frames.is_empty() {
1341            return Ok(None);
1342        }
1343
1344        let mut frame_bytes = Vec::new();
1345        let mut checksum_transforms = Vec::new();
1346        let last_commit_frame_offset = self.wal.prepare_frame_bytes_with_transforms_into(
1347            frames.len(),
1348            frames.iter().map(|frame| WalAppendFrameRef {
1349                page_number: frame.page_number,
1350                page_data: frame.page_data,
1351                db_size_if_commit: frame.db_size_if_commit,
1352            }),
1353            &mut frame_bytes,
1354            &mut checksum_transforms,
1355        )?;
1356        let frame_metas = frames
1357            .iter()
1358            .map(|frame| PreparedWalFrameMeta {
1359                page_number: frame.page_number,
1360                db_size_if_commit: frame.db_size_if_commit,
1361            })
1362            .collect();
1363
1364        Ok(Some(PreparedWalFrameBatch {
1365            frame_size: self.wal.frame_size(),
1366            page_data_offset: WAL_FRAME_HEADER_SIZE,
1367            big_endian_checksum: self.wal.big_endian_checksum(),
1368            frame_metas,
1369            checksum_transforms,
1370            frame_bytes,
1371            last_commit_frame_offset,
1372            finalized_for: None,
1373            finalized_running_checksum: None,
1374        }))
1375    }
1376
1377    fn finalize_prepared_frames(
1378        &self,
1379        _cx: &Cx,
1380        prepared: &mut PreparedWalFrameBatch,
1381    ) -> Result<()> {
1382        if prepared.frame_count() == 0 {
1383            return Ok(());
1384        }
1385        // Optimistically finalize against the adapter's current WAL state.
1386        // The append path still validates against both local and on-disk state
1387        // and will refresh/reseed if another writer advanced the append window.
1388        self.finalize_prepared_batch_against_current_state(prepared)
1389    }
1390
1391    fn append_prepared_frames<'a>(
1392        &'a mut self,
1393        cx: &'a Cx,
1394        prepared: &'a mut PreparedWalFrameBatch,
1395    ) -> WalFuture<'a, ()> {
1396        Box::pin(async move {
1397            if prepared.frame_count() == 0 {
1398                return Ok(());
1399            }
1400
1401            let can_reuse_prelock_finalize = self.refresh_before_append
1402                && self.prepared_batch_matches_current_state(prepared)
1403                && self.prepared_batch_matches_disk_state(cx, prepared).await?;
1404            if self.refresh_before_append && !can_reuse_prelock_finalize {
1405                self.synchronize_publication_before_append(cx, "append_prepared_pre_refresh")
1406                    .await?;
1407            }
1408
1409            if !self.prepared_batch_matches_current_state(prepared) {
1410                self.finalize_prepared_batch_against_current_state(prepared)?;
1411            }
1412
1413            let start_frame_index = self.wal.frame_count();
1414            self.wal
1415                .append_finalized_prepared_frame_bytes(
1416                    cx,
1417                    &prepared.frame_bytes,
1418                    prepared.frame_count(),
1419                    Self::finalized_running_checksum(prepared)?,
1420                    prepared.last_commit_frame_offset,
1421                )
1422                .await?;
1423            self.refresh_before_append = false;
1424            let last_commit_frame = self.record_appended_frames(
1425                start_frame_index,
1426                prepared
1427                    .frame_metas
1428                    .iter()
1429                    .map(|frame| (frame.page_number, frame.db_size_if_commit)),
1430            );
1431
1432            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1433            if let Some(hook) = &mut self.fec_hook {
1434                for (index, frame) in prepared.frame_metas.iter().enumerate() {
1435                    match hook.on_frame(
1436                        cx,
1437                        frame.page_number,
1438                        prepared.page_data(index),
1439                        frame.db_size_if_commit,
1440                    ) {
1441                        Ok(Some(result)) => {
1442                            debug!(
1443                                pages = result.page_numbers.len(),
1444                                k_source = result.k_source,
1445                                symbols = result.symbols.len(),
1446                                "FEC commit group encoded"
1447                            );
1448                            self.fec_pending.push(result);
1449                        }
1450                        Ok(None) => {}
1451                        Err(e) => {
1452                            warn!(
1453                                error = %e,
1454                                "FEC encoding failed; commit proceeds without repair symbols"
1455                            );
1456                        }
1457                    }
1458                }
1459            }
1460
1461            if let Some(last_commit_frame) = last_commit_frame {
1462                // Stage only: publication is deferred to the durability barrier.
1463                self.stage_pending_commit_publication(last_commit_frame)?;
1464            }
1465
1466            Ok(())
1467        })
1468    }
1469
1470    fn append_prepared_frames_tracked<'a>(
1471        &'a mut self,
1472        cx: &'a Cx,
1473        prepared: &'a mut PreparedWalFrameBatch,
1474        completion: VfsWriteCompletion,
1475    ) -> WalFuture<'a, ()> {
1476        Box::pin(async move {
1477            let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
1478            if prepared.frame_count() == 0 {
1479                completion.complete_success();
1480                preflight.hand_off();
1481                return Ok(());
1482            }
1483
1484            let can_reuse_prelock_finalize = self.refresh_before_append
1485                && self.prepared_batch_matches_current_state(prepared)
1486                && self.prepared_batch_matches_disk_state(cx, prepared).await?;
1487            if self.refresh_before_append && !can_reuse_prelock_finalize {
1488                self.synchronize_publication_before_append(cx, "append_prepared_pre_refresh")
1489                    .await?;
1490            }
1491
1492            if !self.prepared_batch_matches_current_state(prepared) {
1493                self.finalize_prepared_batch_against_current_state(prepared)?;
1494            }
1495
1496            let start_frame_index = self.wal.frame_count();
1497            let final_running_checksum = Self::finalized_running_checksum(prepared)?;
1498            preflight.hand_off();
1499            drop(preflight);
1500            self.wal
1501                .append_finalized_prepared_frame_bytes_tracked(
1502                    cx,
1503                    &prepared.frame_bytes,
1504                    prepared.frame_count(),
1505                    final_running_checksum,
1506                    prepared.last_commit_frame_offset,
1507                    completion,
1508                )
1509                .await?;
1510            self.refresh_before_append = false;
1511            let last_commit_frame = self.record_appended_frames(
1512                start_frame_index,
1513                prepared
1514                    .frame_metas
1515                    .iter()
1516                    .map(|frame| (frame.page_number, frame.db_size_if_commit)),
1517            );
1518
1519            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1520            if let Some(hook) = &mut self.fec_hook {
1521                for (index, frame) in prepared.frame_metas.iter().enumerate() {
1522                    match hook.on_frame(
1523                        cx,
1524                        frame.page_number,
1525                        prepared.page_data(index),
1526                        frame.db_size_if_commit,
1527                    ) {
1528                        Ok(Some(result)) => {
1529                            debug!(
1530                                pages = result.page_numbers.len(),
1531                                k_source = result.k_source,
1532                                symbols = result.symbols.len(),
1533                                "FEC commit group encoded"
1534                            );
1535                            self.fec_pending.push(result);
1536                        }
1537                        Ok(None) => {}
1538                        Err(e) => {
1539                            warn!(
1540                                error = %e,
1541                                "FEC encoding failed; commit proceeds without repair symbols"
1542                            );
1543                        }
1544                    }
1545                }
1546            }
1547
1548            if let Some(last_commit_frame) = last_commit_frame {
1549                // Stage only: publication is deferred to the durability barrier.
1550                self.stage_pending_commit_publication(last_commit_frame)?;
1551            }
1552
1553            Ok(())
1554        })
1555    }
1556
1557    fn read_page<'a>(&'a mut self, cx: &'a Cx, page_number: u32) -> WalFuture<'a, Option<Vec<u8>>> {
1558        Box::pin(async move {
1559            let snapshot = if let Some(snapshot) = self.read_snapshot.clone() {
1560                snapshot
1561            } else {
1562                self.publish_latest_committed_snapshot(cx, "read_page_unpinned")
1563                    .await?;
1564                self.published_snapshot.clone()
1565            };
1566            if snapshot.last_commit_frame.is_none() {
1567                return Ok(None);
1568            }
1569            let snapshot_age = self
1570                .published_snapshot
1571                .publication_seq
1572                .saturating_sub(snapshot.publication_seq);
1573
1574            let resolution = self
1575                .resolve_visible_frame(cx, &snapshot, page_number)
1576                .await?;
1577            let Some(frame_index) = resolution.frame_index() else {
1578                debug!(
1579                    page_number,
1580                    wal_checkpoint_seq = snapshot.generation.checkpoint_seq,
1581                    wal_salt1 = snapshot.generation.salts.salt1,
1582                    wal_salt2 = snapshot.generation.salts.salt2,
1583                    publication_seq = snapshot.publication_seq,
1584                    snapshot_age,
1585                    lookup_mode = resolution.lookup_mode(),
1586                    fallback_reason = resolution.fallback_reason(),
1587                    "WAL adapter: page absent from current generation"
1588                );
1589                return Ok(None);
1590            };
1591
1592            // Read the frame data at the resolved position.
1593            let mut frame_buf = vec![0u8; self.wal.frame_size()];
1594            let header = self
1595                .wal
1596                .read_frame_into(cx, frame_index, &mut frame_buf)
1597                .await?;
1598
1599            // Runtime integrity check: verify the frame actually contains our page.
1600            // This guards against index corruption or stale entries.
1601            if header.page_number != page_number {
1602                return Err(FrankenError::WalCorrupt {
1603                    detail: format!(
1604                        "WAL page index integrity failure: expected page {page_number} \
1605                         at frame {frame_index}, found page {}",
1606                        header.page_number
1607                    ),
1608                });
1609            }
1610
1611            // Strip the 24-byte frame header in place rather than
1612            // allocating a second page-sized Vec. Mirrors the fix in
1613            // `read_page_pinned` (`d9c410bb`): `frame_buf[HEADER..].to_vec()`
1614            // allocates a fresh 4 KiB buffer, memcpys the page payload into
1615            // it, then drops the original 4 KiB+24 B scratch — an alloc/free
1616            // round-trip on the hot WAL read path. Using `copy_within` +
1617            // `truncate` reuses the already-populated buffer: one memmove
1618            // (over the same bytes `to_vec` would have copied) and no new
1619            // allocation. `read_page` is the `&mut self` fallback path taken
1620            // when the caller does not hold a pinned snapshot — still hot
1621            // under mixed OLTP and write-path conflict resolution.
1622            let header_size = fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE;
1623            let page_size = self.wal.page_size();
1624            frame_buf.copy_within(header_size.., 0);
1625            frame_buf.truncate(page_size);
1626            debug!(
1627                page_number,
1628                frame_index,
1629                wal_checkpoint_seq = snapshot.generation.checkpoint_seq,
1630                wal_salt1 = snapshot.generation.salts.salt1,
1631                wal_salt2 = snapshot.generation.salts.salt2,
1632                publication_seq = snapshot.publication_seq,
1633                snapshot_age,
1634                lookup_mode = resolution.lookup_mode(),
1635                fallback_reason = resolution.fallback_reason(),
1636                "WAL adapter: resolved page from current WAL generation"
1637            );
1638            Ok(Some(frame_buf))
1639        })
1640    }
1641
1642    // bd-db300.3.8.7: shared-lock read path for pinned snapshots.
1643    fn read_page_pinned<'a>(
1644        &'a self,
1645        cx: &'a Cx,
1646        page_number: u32,
1647    ) -> WalFuture<'a, Option<Vec<u8>>> {
1648        Box::pin(async move {
1649            let snapshot = self.read_snapshot.as_ref().ok_or_else(|| {
1650                FrankenError::internal(
1651                    "read_page_pinned called without a pinned read snapshot; \
1652                     use read_page(&mut self) or call begin_transaction first",
1653                )
1654            })?;
1655            if snapshot.last_commit_frame.is_none() {
1656                return Ok(None);
1657            }
1658
1659            let resolution = self
1660                .resolve_visible_frame(cx, snapshot, page_number)
1661                .await?;
1662            let Some(frame_index) = resolution.frame_index() else {
1663                return Ok(None);
1664            };
1665
1666            let mut frame_buf = vec![0u8; self.wal.frame_size()];
1667            let header = self
1668                .wal
1669                .read_frame_into(cx, frame_index, &mut frame_buf)
1670                .await?;
1671
1672            if header.page_number != page_number {
1673                return Err(FrankenError::WalCorrupt {
1674                    detail: format!(
1675                        "WAL page index integrity failure: expected page {page_number} \
1676                         at frame {frame_index}, found page {}",
1677                        header.page_number
1678                    ),
1679                });
1680            }
1681
1682            // Strip the 24-byte frame header in place instead of allocating
1683            // a fresh page-sized Vec. The pre-existing pattern did
1684            // `frame_buf[HEADER..].to_vec()` — on a 4 KiB page that
1685            // allocated a second 4 KiB buffer plus a 4 KiB memcpy and then
1686            // dropped the original 4 KiB+24 B frame_buf. On an MT pinned-
1687            // read workload every page served from the WAL paid that per-
1688            // read alloc/free round-trip; `_int_malloc` and `cfree` already
1689            // showed up in recent 2-thread profiles. Here we keep the
1690            // already-populated `frame_buf`, memmove the page bytes over
1691            // the header, truncate to `page_size`, and return it — one
1692            // allocation per read instead of two.
1693            let header_size = fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE;
1694            let page_size = self.wal.page_size();
1695            frame_buf.copy_within(header_size.., 0);
1696            frame_buf.truncate(page_size);
1697            Ok(Some(frame_buf))
1698        })
1699    }
1700
1701    fn supports_pinned_reads(&self) -> bool {
1702        self.read_snapshot.is_some()
1703    }
1704
1705    fn committed_txns_since_page<'a>(
1706        &'a mut self,
1707        cx: &'a Cx,
1708        page_number: u32,
1709    ) -> WalFuture<'a, u64> {
1710        Box::pin(async move {
1711            let snapshot = if let Some(snapshot) = self.read_snapshot.clone() {
1712                snapshot
1713            } else {
1714                self.publish_latest_committed_snapshot(cx, "committed_txns_since_page")
1715                    .await?;
1716                self.published_snapshot.clone()
1717            };
1718            let Some(last_commit_frame) = snapshot.last_commit_frame else {
1719                return Ok(0);
1720            };
1721
1722            let resolution = self
1723                .resolve_visible_frame(cx, &snapshot, page_number)
1724                .await?;
1725            let Some(last_page_frame) = resolution.frame_index() else {
1726                let mut total_commits = 0_u64;
1727                for frame_index in 0..=last_commit_frame {
1728                    if self
1729                        .wal
1730                        .read_frame_header(cx, frame_index)
1731                        .await?
1732                        .is_commit()
1733                    {
1734                        total_commits = total_commits.saturating_add(1);
1735                    }
1736                }
1737                return Ok(total_commits);
1738            };
1739
1740            let mut page_commit_frame = None;
1741            for frame_index in last_page_frame..=last_commit_frame {
1742                if self
1743                    .wal
1744                    .read_frame_header(cx, frame_index)
1745                    .await?
1746                    .is_commit()
1747                {
1748                    page_commit_frame = Some(frame_index);
1749                    break;
1750                }
1751            }
1752
1753            let Some(page_commit_frame) = page_commit_frame else {
1754                return Ok(0);
1755            };
1756
1757            let mut committed_txns_after_page = 0_u64;
1758            for frame_index in page_commit_frame.saturating_add(1)..=last_commit_frame {
1759                if self
1760                    .wal
1761                    .read_frame_header(cx, frame_index)
1762                    .await?
1763                    .is_commit()
1764                {
1765                    committed_txns_after_page = committed_txns_after_page.saturating_add(1);
1766                }
1767            }
1768
1769            Ok(committed_txns_after_page)
1770        })
1771    }
1772
1773    fn conflicting_pages_since_snapshot<'a>(
1774        &'a mut self,
1775        cx: &'a Cx,
1776        snapshot: TransactionConflictSnapshot,
1777        page_numbers: &'a [u32],
1778        _page_baselines: &'a [TransactionConflictPageBaseline],
1779    ) -> WalFuture<'a, Vec<u32>> {
1780        Box::pin(async move {
1781            if page_numbers.is_empty() {
1782                return Ok(Vec::new());
1783            }
1784
1785            let mut candidates = page_numbers
1786                .iter()
1787                .copied()
1788                .filter(|page| *page != 0)
1789                .collect::<Vec<_>>();
1790            candidates.sort_unstable();
1791            candidates.dedup();
1792            if candidates.is_empty() {
1793                return Ok(Vec::new());
1794            }
1795
1796            self.wal.refresh(cx).await?;
1797            self.publish_latest_committed_snapshot(cx, "conflicting_pages_since_snapshot")
1798                .await?;
1799            let latest = self.published_snapshot();
1800            if latest.commit_count <= snapshot.commit_count
1801                && latest.generation == snapshot.generation
1802                && latest.last_commit_frame <= snapshot.last_commit_frame
1803            {
1804                return Ok(Vec::new());
1805            }
1806
1807            if latest.generation != snapshot.generation {
1808                return Ok(candidates);
1809            }
1810
1811            let Some(latest_last_commit_frame) = latest.last_commit_frame else {
1812                return Ok(Vec::new());
1813            };
1814            let start_frame = snapshot
1815                .last_commit_frame
1816                .map_or(0, |frame| frame.saturating_add(1));
1817            if start_frame > latest_last_commit_frame {
1818                return Ok(Vec::new());
1819            }
1820
1821            let candidate_set = candidates.iter().copied().collect::<HashSet<_>>();
1822            let mut conflicts = HashSet::<u32>::new();
1823            for frame_index in start_frame..=latest_last_commit_frame {
1824                let header = self.wal.read_frame_header(cx, frame_index).await?;
1825                if candidate_set.contains(&header.page_number) {
1826                    conflicts.insert(header.page_number);
1827                }
1828            }
1829
1830            let mut conflicts = conflicts.into_iter().collect::<Vec<_>>();
1831            conflicts.sort_unstable();
1832            Ok(conflicts)
1833        })
1834    }
1835
1836    fn committed_txn_count<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, u64> {
1837        Box::pin(async move {
1838            let snapshot = if let Some(snapshot) = self.read_snapshot.clone() {
1839                snapshot
1840            } else {
1841                self.publish_latest_committed_snapshot(cx, "committed_txn_count")
1842                    .await?;
1843                self.published_snapshot.clone()
1844            };
1845            Ok(snapshot.commit_count)
1846        })
1847    }
1848
1849    fn sync(&mut self, cx: &Cx) -> Result<()> {
1850        // Durability first. Only once the frames are on stable storage may the
1851        // staged commit horizon become visible to readers.
1852        //
1853        // `refresh_before_append` is deliberately NOT set on the failure paths.
1854        // Setting it would let the next append run
1855        // `synchronize_publication_before_append`, which discards the preserved
1856        // batch and republishes straight from the WAL — reinstating exactly the
1857        // publish-before-fsync hazard this guard exists to prevent. Leaving it
1858        // clear keeps the staged batch intact for a later retry.
1859        self.wal.sync(cx, SyncFlags::NORMAL)?;
1860        self.publish_pending_after_sync(cx)?;
1861        // Re-arm the pre-append resynchronization only when nothing is staged.
1862        //
1863        // Syncing mid-transaction makes the appended frames durable but does not
1864        // commit them: with no commit marker yet, `publish_pending_after_sync`
1865        // correctly publishes nothing and the frames stay staged for the commit
1866        // still to come. Re-arming here would send the next append through
1867        // `synchronize_publication_before_append`, whose fail-closed guard would
1868        // then reject every further append — including the commit marker — and
1869        // strand the transaction permanently.
1870        if !self.has_pending_publication() {
1871            self.refresh_before_append = true;
1872        }
1873        Ok(())
1874    }
1875
1876    fn frame_count(&self) -> usize {
1877        self.wal.frame_count()
1878    }
1879
1880    fn checkpoint<'a>(
1881        &'a mut self,
1882        cx: &'a Cx,
1883        mode: CheckpointMode,
1884        writer: &'a mut dyn CheckpointPageWriter,
1885        backfilled_frames: u32,
1886        oldest_reader_frame: Option<u32>,
1887    ) -> WalFuture<'a, CheckpointResult> {
1888        Box::pin(async move {
1889            // Fail closed BEFORE `wal.refresh` or any writer mutation. Checkpoint
1890            // backfills and may reset the WAL, and its inner paths can call
1891            // `invalidate_publication`, which discards the staged batch. Running
1892            // any of that against frames that were never fsynced would both lose
1893            // the staged horizon and risk backfilling non-durable frames, so the
1894            // batch must be drained by a successful sync first.
1895            if self.has_pending_publication() {
1896                return Err(FrankenError::CheckpointFailed {
1897                    detail: "staged, unpublished frames remain; a successful commit sync must \
1898                             drain them before checkpointing"
1899                        .to_owned(),
1900                });
1901            }
1902            // Refresh so planner state reflects the latest on-disk WAL shape.
1903            self.wal.refresh(cx).await?;
1904            self.refresh_before_append = true;
1905            let total_frames = u32::try_from(self.wal.frame_count()).unwrap_or(u32::MAX);
1906
1907            // Build checkpoint state for the planner.
1908            let state = CheckpointState {
1909                total_frames,
1910                backfilled_frames,
1911                oldest_reader_frame,
1912            };
1913
1914            // Wrap the CheckpointPageWriter in a CheckpointTargetAdapter.
1915            let mut target = CheckpointTargetAdapterRef { writer };
1916
1917            // Execute the checkpoint.
1918            let result =
1919                execute_checkpoint(cx, &mut self.wal, to_wal_mode(mode), state, &mut target)
1920                    .await?;
1921
1922            // Checkpoint-aware FEC lifecycle: once frames are backfilled to the
1923            // database file, their FEC symbols are no longer needed.  Clear
1924            // pending FEC results for the checkpointed range.
1925            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1926            if result.frames_backfilled > 0 {
1927                let drained = self.fec_pending.len();
1928                self.fec_pending.clear();
1929                if drained > 0 {
1930                    debug!(
1931                        drained_groups = drained,
1932                        frames_backfilled = result.frames_backfilled,
1933                        "FEC symbols reclaimed after checkpoint"
1934                    );
1935                }
1936            }
1937
1938            // If the WAL was fully reset, also discard any buffered FEC pages
1939            // and invalidate the page index (salts changed).
1940            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1941            if result.wal_was_reset {
1942                self.fec_discard();
1943            }
1944            if result.wal_was_reset {
1945                self.invalidate_publication();
1946            }
1947
1948            self.publish_latest_committed_snapshot(cx, "checkpoint")
1949                .await?;
1950
1951            Ok(CheckpointResult {
1952                total_frames,
1953                frames_backfilled: result.frames_backfilled,
1954                completed: result.plan.completes_checkpoint(),
1955                wal_was_reset: result.wal_was_reset,
1956                requested_mode: mode,
1957                effective_mode: mode,
1958            })
1959        })
1960    }
1961}
1962
1963const MIN_DURABLE_CERTIFICATE_RECORD_SIZE: usize =
1964    ParallelWalDurableCertificateRecord::MIN_ENCODED_SIZE;
1965const DURABLE_CERTIFICATE_RECORD_HEADER_SIZE: usize = 14;
1966const MAX_ORPHAN_CERTIFICATE_LOOKBACK: usize = 64;
1967
1968fn durable_certificate_declared_len(bytes: &[u8]) -> Option<usize> {
1969    let length_bytes = bytes.get(10..DURABLE_CERTIFICATE_RECORD_HEADER_SIZE)?;
1970    usize::try_from(u32::from_le_bytes([
1971        length_bytes[0],
1972        length_bytes[1],
1973        length_bytes[2],
1974        length_bytes[3],
1975    ]))
1976    .ok()
1977}
1978
1979fn durable_certificate_declares_len(bytes: &[u8], expected: usize) -> bool {
1980    durable_certificate_declared_len(bytes).is_some_and(|actual| actual.cmp(&expected).is_eq())
1981}
1982
1983fn decode_durable_certificate_record(
1984    bytes: &[u8],
1985    location: &str,
1986) -> Result<ParallelWalDurableCertificateRecord> {
1987    ParallelWalDurableCertificateRecord::from_bytes(bytes).map_err(|error| {
1988        FrankenError::WalCorrupt {
1989            detail: format!("parallel WAL certificate {location} is invalid: {error}"),
1990        }
1991    })
1992}
1993
1994fn validate_incomplete_certificate_suffix(bytes: &[u8], anchored: bool) -> Result<()> {
1995    if bytes.is_empty() {
1996        return Ok(());
1997    }
1998    if bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
1999        return Err(FrankenError::WalCorrupt {
2000            detail: format!(
2001                "parallel WAL certificate torn suffix exceeds {} bytes",
2002                PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2003            ),
2004        });
2005    }
2006
2007    if bytes.len() < PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.len() {
2008        // A failed append can leave fewer bytes than the magic itself. Once a
2009        // strict record boundary anchors the suffix, those bytes are
2010        // unambiguously one incomplete append (including legacy one-byte
2011        // fault injections that predate the magic prefix).
2012        if anchored || PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.starts_with(bytes) {
2013            return Ok(());
2014        }
2015        return Err(FrankenError::WalCorrupt {
2016            detail: "parallel WAL certificate sidecar starts with non-record garbage".to_owned(),
2017        });
2018    }
2019    if !bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC) {
2020        return Err(FrankenError::WalCorrupt {
2021            detail: "parallel WAL certificate suffix does not start at a record boundary"
2022                .to_owned(),
2023        });
2024    }
2025    if bytes.len() < 10 {
2026        return Ok(());
2027    }
2028    let version = u16::from_le_bytes([bytes[8], bytes[9]]);
2029    if version != fsqlite_wal::PARALLEL_WAL_DURABLE_CERTIFICATE_RECORD_VERSION {
2030        return Err(FrankenError::WalCorrupt {
2031            detail: format!(
2032                "parallel WAL certificate suffix has unsupported record version {version}"
2033            ),
2034        });
2035    }
2036    if bytes.len() < DURABLE_CERTIFICATE_RECORD_HEADER_SIZE {
2037        return Ok(());
2038    }
2039    let declared_len =
2040        durable_certificate_declared_len(bytes).ok_or_else(|| FrankenError::WalCorrupt {
2041            detail: "parallel WAL certificate suffix length exceeds usize".to_owned(),
2042        })?;
2043    if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2044        .contains(&declared_len)
2045    {
2046        return Err(FrankenError::WalCorrupt {
2047            detail: format!(
2048                "parallel WAL certificate suffix declares invalid record length {declared_len}"
2049            ),
2050        });
2051    }
2052    if bytes.len() < declared_len {
2053        return Ok(());
2054    }
2055
2056    // A complete envelope is corruption, not a torn suffix. Strict decoding
2057    // gives a precise CRC/footer/version diagnostic. A valid complete record
2058    // here would mean more than one suffix record escaped the footer walk,
2059    // which is equally outside the one-torn-append recovery contract.
2060    decode_durable_certificate_record(&bytes[..declared_len], "suffix")?;
2061    Err(FrankenError::WalCorrupt {
2062        detail:
2063            "parallel WAL certificate sidecar contains a complete record outside the footer chain"
2064                .to_owned(),
2065    })
2066}
2067
2068fn combine_sidecar_io_results<const N: usize>(
2069    context: &str,
2070    results: [(&str, Result<()>); N],
2071) -> Result<()> {
2072    let failures = results
2073        .into_iter()
2074        .filter_map(|(stage, result)| result.err().map(|error| (stage, error)))
2075        .collect::<Vec<_>>();
2076    if failures.is_empty() {
2077        return Ok(());
2078    }
2079    if failures.len() == 1 {
2080        return failures
2081            .into_iter()
2082            .next()
2083            .map_or(Ok(()), |(_, error)| Err(error));
2084    }
2085    let details = failures
2086        .iter()
2087        .map(|(stage, error)| format!("{stage}={error}"))
2088        .collect::<Vec<_>>()
2089        .join("; ");
2090    Err(FrankenError::internal(format!("{context}: {details}")))
2091}
2092
2093/// WAL backend that can recover when the path-visible `-wal` sidecar is
2094/// removed or replaced while this process still owns an old file descriptor.
2095///
2096/// Real SQLite can checkpoint and unlink/reset `db-wal` when it does not know
2097/// about a live FrankenSQLite handle. `WalFile::refresh` is intentionally
2098/// descriptor-local, so it cannot notice that path-level mutation. This wrapper
2099/// performs a path probe before mutable WAL operations and swaps in a freshly
2100/// opened/created `WalFile` when the path-visible sidecar no longer matches the
2101/// open handle.
2102pub struct PathRefreshingWalBackend<V: Vfs>
2103where
2104    V::File: Send + Sync + 'static,
2105{
2106    vfs: V,
2107    db_path: PathBuf,
2108    wal_path: PathBuf,
2109    page_size: u32,
2110    create_missing: bool,
2111    #[cfg(all(feature = "native", any(unix, windows)))]
2112    namespace_binding: Option<Arc<DatabaseNamespaceBinding>>,
2113    inner: WalBackendAdapter<V::File>,
2114}
2115
2116impl<V> PathRefreshingWalBackend<V>
2117where
2118    V: Vfs + 'static,
2119    V::File: Send + Sync + 'static,
2120{
2121    #[must_use]
2122    pub fn new(
2123        vfs: V,
2124        db_path: impl AsRef<Path>,
2125        wal_path: impl AsRef<Path>,
2126        page_size: u32,
2127        wal: WalFile<V::File>,
2128        create_missing: bool,
2129        #[cfg(all(feature = "native", any(unix, windows)))] namespace_binding: Option<
2130            Arc<DatabaseNamespaceBinding>,
2131        >,
2132    ) -> Self {
2133        Self {
2134            vfs,
2135            db_path: db_path.as_ref().to_path_buf(),
2136            wal_path: wal_path.as_ref().to_path_buf(),
2137            page_size,
2138            create_missing,
2139            #[cfg(all(feature = "native", any(unix, windows)))]
2140            namespace_binding,
2141            inner: WalBackendAdapter::new(wal),
2142        }
2143    }
2144
2145    #[must_use]
2146    pub fn into_inner(self) -> WalBackendAdapter<V::File> {
2147        self.inner
2148    }
2149
2150    /// Swap in a replacement WAL, discarding the previous adapter.
2151    ///
2152    /// Fails closed while the outgoing adapter still holds a staged batch: the
2153    /// replacement would consume away the pending metadata, and the freshly
2154    /// wrapped adapter would republish those frames from the WAL without knowing
2155    /// they were never fsynced (GH #187). A successful sync must drain the batch
2156    /// before a path-visible replacement can proceed.
2157    fn replace_inner(&mut self, cx: &Cx, wal: WalFile<V::File>) -> Result<()> {
2158        if self.inner.has_pending_publication() {
2159            let cleanup_cx = cx.create_child();
2160            let _cleanup_mask = cleanup_cx.masked();
2161            let _ = wal.close(&cleanup_cx);
2162            return Err(FrankenError::Busy);
2163        }
2164        let old = std::mem::replace(&mut self.inner, WalBackendAdapter::new(wal));
2165        let old_wal = old.into_inner()?;
2166        let _ = old_wal.close(cx);
2167        Ok(())
2168    }
2169
2170    async fn create_replacement_wal(&self, cx: &Cx) -> Result<WalFile<V::File>> {
2171        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2172        let (file, _) = self.vfs.open(cx, Some(&self.wal_path), flags)?;
2173        // Random salts (GH #201): the replacement WAL must reject frames
2174        // from the file it replaces.
2175        let wal = WalFile::create(cx, file, self.page_size, 0, WalSalts::generate()).await?;
2176        if let Err(error) = self.vfs.sync_parent_directory(cx, &self.wal_path) {
2177            let cleanup_cx = cx.create_child();
2178            let _cleanup_mask = cleanup_cx.masked();
2179            let _ = wal.close(&cleanup_cx);
2180            return Err(error);
2181        }
2182        Ok(wal)
2183    }
2184
2185    async fn replace_with_created_wal(&mut self, cx: &Cx) -> Result<()> {
2186        let wal = self.create_replacement_wal(cx).await?;
2187        self.replace_inner(cx, wal)
2188    }
2189
2190    async fn open_replacement_wal(&self, cx: &Cx, path_file: V::File) -> Result<WalFile<V::File>> {
2191        let wal = WalFile::open(cx, path_file).await?;
2192        if u32::try_from(wal.page_size()).ok() != Some(self.page_size) {
2193            let actual_page_size = wal.page_size();
2194            let expected_page_size = self.page_size;
2195            let _ = wal.close(cx);
2196            return Err(FrankenError::WalCorrupt {
2197                detail: format!(
2198                    "WAL page size {actual_page_size} does not match database page size {expected_page_size} during path refresh"
2199                ),
2200            });
2201        }
2202        Ok(wal)
2203    }
2204
2205    async fn path_header_matches_current_handle(
2206        &self,
2207        cx: &Cx,
2208        path_file: &V::File,
2209    ) -> Result<bool> {
2210        let mut header_buf = [0_u8; WAL_HEADER_SIZE];
2211        let bytes_read = path_file.read(cx, &mut header_buf, 0).await?;
2212        if bytes_read < WAL_HEADER_SIZE {
2213            return Ok(false);
2214        }
2215
2216        let path_header = WalHeader::from_bytes(&header_buf)?;
2217        if !validate_wal_header_checksum(&header_buf, path_header.big_endian_checksum())? {
2218            return Err(FrankenError::WalCorrupt {
2219                detail: "WAL header checksum mismatch during path refresh".to_owned(),
2220            });
2221        }
2222
2223        let current_header = self.inner.inner().header();
2224        Ok(path_header.magic == current_header.magic
2225            && path_header.format_version == current_header.format_version
2226            && path_header.page_size == current_header.page_size
2227            && path_header.checkpoint_seq == current_header.checkpoint_seq
2228            && path_header.salts == current_header.salts)
2229    }
2230
2231    /// Revalidate conflict candidates across a WAL-generation transition.
2232    ///
2233    /// A stock SQLite reader may checkpoint and replace an otherwise
2234    /// unchanged WAL while a FrankenSQLite transaction is open. Treating the
2235    /// generation change itself as a write conflict produces a false
2236    /// `BusySnapshot`. Conversely, blindly accepting the new generation can
2237    /// overwrite a real external commit that was checkpointed into the main
2238    /// database. The only safe admission proof is therefore page-specific:
2239    /// every candidate must have a transaction-snapshot baseline, and its
2240    /// latest committed full-page image (new WAL first, main DB otherwise)
2241    /// must hash identically.
2242    ///
2243    /// Any missing/ambiguous baseline, unreadable or short main page, invalid
2244    /// database header, page-size change, WAL read error, or close failure
2245    /// fails closed by returning every candidate as conflicting.
2246    async fn conflicts_after_generation_change(
2247        &mut self,
2248        cx: &Cx,
2249        page_numbers: &[u32],
2250        page_baselines: &[TransactionConflictPageBaseline],
2251    ) -> Vec<u32> {
2252        let mut candidates = page_numbers
2253            .iter()
2254            .copied()
2255            .filter(|page| *page != 0)
2256            .collect::<Vec<_>>();
2257        candidates.sort_unstable();
2258        candidates.dedup();
2259        if candidates.is_empty() {
2260            return Vec::new();
2261        }
2262
2263        let mut baselines = HashMap::<u32, [u8; 32]>::new();
2264        let mut ambiguous_baselines = HashSet::<u32>::new();
2265        for baseline in page_baselines {
2266            if baseline.page_number == 0 {
2267                continue;
2268            }
2269            if let Some(previous) = baselines.insert(baseline.page_number, baseline.page_hash)
2270                && previous != baseline.page_hash
2271            {
2272                ambiguous_baselines.insert(baseline.page_number);
2273            }
2274        }
2275
2276        let main_db_flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
2277        let (mut db_file, _) = match self.vfs.open(cx, Some(&self.db_path), main_db_flags) {
2278            Ok(opened) => opened,
2279            Err(_) => return candidates,
2280        };
2281        let page_size = match usize::try_from(self.page_size) {
2282            Ok(page_size) if page_size > 0 => page_size,
2283            _ => {
2284                let _ = db_file.close(cx);
2285                return candidates;
2286            }
2287        };
2288
2289        // Validate the current main-database header before trusting offsets.
2290        // SQLite encodes a 64 KiB page as the u16 value 1.
2291        let mut page_one = vec![0_u8; page_size];
2292        let page_one_read = match db_file.read(cx, &mut page_one, 0).await {
2293            Ok(bytes_read) => bytes_read,
2294            Err(_) => {
2295                let _ = db_file.close(cx);
2296                return candidates;
2297            }
2298        };
2299        let header_page_size =
2300            (page_one_read == page_size).then(|| sqlite_database_header_page_size(&page_one));
2301        if header_page_size.flatten() != Some(self.page_size) {
2302            let _ = db_file.close(cx);
2303            return candidates;
2304        }
2305
2306        let mut conflicts = Vec::new();
2307        for &page_number in &candidates {
2308            let Some(expected_hash) = baselines.get(&page_number).copied() else {
2309                conflicts.push(page_number);
2310                continue;
2311            };
2312            if ambiguous_baselines.contains(&page_number) {
2313                conflicts.push(page_number);
2314                continue;
2315            }
2316
2317            let current_page = match self.inner.read_page(cx, page_number).await {
2318                Ok(Some(page)) if page.len() == page_size => page,
2319                Ok(Some(_)) | Err(_) => {
2320                    conflicts.push(page_number);
2321                    continue;
2322                }
2323                Ok(None) => {
2324                    let mut page = vec![0_u8; page_size];
2325                    let page_offset = u64::from(page_number.saturating_sub(1))
2326                        .saturating_mul(u64::from(self.page_size));
2327                    match db_file.read(cx, &mut page, page_offset).await {
2328                        Ok(bytes_read) if bytes_read == page_size => page,
2329                        Ok(_) | Err(_) => {
2330                            conflicts.push(page_number);
2331                            continue;
2332                        }
2333                    }
2334                }
2335            };
2336            let current_hash = *blake3::hash(&current_page).as_bytes();
2337            if current_hash != expected_hash {
2338                conflicts.push(page_number);
2339            }
2340        }
2341
2342        if db_file.close(cx).is_err() {
2343            return candidates;
2344        }
2345        conflicts.sort_unstable();
2346        conflicts.dedup();
2347        conflicts
2348    }
2349
2350    async fn ensure_current_wal_path(&mut self, cx: &Cx) -> Result<()> {
2351        #[cfg(all(feature = "native", any(unix, windows)))]
2352        if let Some(binding) = &self.namespace_binding {
2353            binding.validate_path_identity()?;
2354        }
2355        if !self.vfs.access(cx, &self.wal_path, AccessFlags::EXISTS)? {
2356            if self.create_missing {
2357                return self.replace_with_created_wal(cx).await;
2358            }
2359            return Ok(());
2360        }
2361
2362        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::WAL;
2363        let (mut path_file, _) = self.vfs.open(cx, Some(&self.wal_path), flags)?;
2364        let path_size = path_file.file_size(cx)?;
2365        if path_size < u64::try_from(WAL_HEADER_SIZE).unwrap_or(32) {
2366            let _ = path_file.close(cx);
2367            if self.create_missing {
2368                return self.replace_with_created_wal(cx).await;
2369            }
2370            return Ok(());
2371        }
2372
2373        let current_size = self.inner.inner().file().file_size(cx).unwrap_or(u64::MAX);
2374        let path_matches_current = if path_size == current_size {
2375            match self
2376                .path_header_matches_current_handle(cx, &path_file)
2377                .await
2378            {
2379                Ok(matches) => matches,
2380                Err(err) => {
2381                    let _ = path_file.close(cx);
2382                    return Err(err);
2383                }
2384            }
2385        } else {
2386            false
2387        };
2388        if !path_matches_current {
2389            let wal = self.open_replacement_wal(cx, path_file).await?;
2390            self.replace_inner(cx, wal)?;
2391        } else {
2392            let _ = path_file.close(cx);
2393        }
2394        Ok(())
2395    }
2396
2397    fn certificate_sidecar_path(&self) -> PathBuf {
2398        let mut path = self.wal_path.as_os_str().to_owned();
2399        path.push("-cert");
2400        PathBuf::from(path)
2401    }
2402
2403    fn certificate_checkpoint_handoff_path(&self) -> PathBuf {
2404        let mut path = self.wal_path.as_os_str().to_owned();
2405        path.push("-cert-head");
2406        PathBuf::from(path)
2407    }
2408
2409    async fn read_certificate_sidecar_exact(
2410        file: &V::File,
2411        cx: &Cx,
2412        offset: u64,
2413        len: usize,
2414        location: &str,
2415    ) -> Result<Vec<u8>> {
2416        let mut bytes = vec![0_u8; len];
2417        let bytes_read = file.read(cx, &mut bytes, offset).await?;
2418        if bytes_read != len {
2419            return Err(FrankenError::WalCorrupt {
2420                detail: format!(
2421                    "parallel WAL certificate {location} at offset {offset} was short-read: got {bytes_read} of {len}"
2422                ),
2423            });
2424        }
2425        Ok(bytes)
2426    }
2427
2428    async fn read_certificate_record_ending_at(
2429        file: &V::File,
2430        cx: &Cx,
2431        record_end: u64,
2432    ) -> Result<(u64, ParallelWalDurableCertificateRecord)> {
2433        let footer_size =
2434            u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE).unwrap_or(4);
2435        let footer_offset = record_end.checked_sub(footer_size).ok_or_else(|| {
2436            FrankenError::WalCorrupt {
2437                detail: format!(
2438                    "parallel WAL certificate record ending at {record_end} has no length footer"
2439                ),
2440            }
2441        })?;
2442        let footer = Self::read_certificate_sidecar_exact(
2443            file,
2444            cx,
2445            footer_offset,
2446            ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
2447            "length footer",
2448        )
2449        .await?;
2450        let record_len = usize::try_from(u32::from_le_bytes([
2451            footer[0], footer[1], footer[2], footer[3],
2452        ]))
2453        .map_err(|_| FrankenError::WalCorrupt {
2454            detail: "parallel WAL certificate footer length exceeds usize".to_owned(),
2455        })?;
2456        if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2457            .contains(&record_len)
2458        {
2459            return Err(FrankenError::WalCorrupt {
2460                detail: format!(
2461                    "parallel WAL certificate footer declares invalid record length {record_len}"
2462                ),
2463            });
2464        }
2465        let record_len_u64 = u64::try_from(record_len).map_err(|_| FrankenError::WalCorrupt {
2466            detail: "parallel WAL certificate record length exceeds u64".to_owned(),
2467        })?;
2468        let record_start =
2469            record_end
2470                .checked_sub(record_len_u64)
2471                .ok_or_else(|| FrankenError::WalCorrupt {
2472                    detail: format!(
2473                        "parallel WAL certificate record length {record_len} exceeds end offset {record_end}"
2474                    ),
2475                })?;
2476        let bytes =
2477            Self::read_certificate_sidecar_exact(file, cx, record_start, record_len, "record")
2478                .await?;
2479        let record = decode_durable_certificate_record(&bytes, "record")?;
2480        Ok((record_start, record))
2481    }
2482
2483    /// Return a safe append boundary, repairing exactly one validated torn
2484    /// suffix in place.
2485    ///
2486    /// The caller must hold the database's external writer or maintenance
2487    /// gate for the whole scan/truncate/append sequence. Keeping this helper
2488    /// private prevents a scan/reopen race from becoming part of the API.
2489    async fn prepare_certificate_sidecar_for_append(file: &mut V::File, cx: &Cx) -> Result<u64> {
2490        let file_size = file.file_size(cx)?;
2491        if file_size == 0 {
2492            return Ok(0);
2493        }
2494
2495        let footer_size =
2496            u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE).unwrap_or(4);
2497        if file_size >= footer_size {
2498            let footer = Self::read_certificate_sidecar_exact(
2499                file,
2500                cx,
2501                file_size - footer_size,
2502                ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
2503                "append-boundary length footer",
2504            )
2505            .await?;
2506            let record_len = usize::try_from(u32::from_le_bytes([
2507                footer[0], footer[1], footer[2], footer[3],
2508            ]))
2509            .unwrap_or(usize::MAX);
2510            let record_len_u64 = u64::try_from(record_len).unwrap_or(u64::MAX);
2511            if (MIN_DURABLE_CERTIFICATE_RECORD_SIZE
2512                ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2513                .contains(&record_len)
2514                && record_len_u64 <= file_size
2515            {
2516                let record_start = file_size - record_len_u64;
2517                let bytes = Self::read_certificate_sidecar_exact(
2518                    file,
2519                    cx,
2520                    record_start,
2521                    record_len,
2522                    "append-boundary record",
2523                )
2524                .await?;
2525                if bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
2526                    || durable_certificate_declares_len(&bytes, record_len)
2527                {
2528                    decode_durable_certificate_record(&bytes, "append-boundary record")?;
2529                    return Ok(file_size);
2530                }
2531            }
2532        }
2533
2534        // The EOF footer was not a complete valid record. Locate at most one
2535        // complete anchor plus one maximum-sized suffix, using exact footer
2536        // boundaries rather than a free-form magic scan.
2537        let recovery_window_size =
2538            PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE.saturating_mul(2);
2539        let recovery_window_size_u64 = u64::try_from(recovery_window_size).unwrap_or(u64::MAX);
2540        let tail_offset = file_size.saturating_sub(recovery_window_size_u64);
2541        let tail_len =
2542            usize::try_from(file_size - tail_offset).map_err(|_| FrankenError::WalCorrupt {
2543                detail: "parallel WAL certificate append-repair window exceeds usize".to_owned(),
2544            })?;
2545        let tail = Self::read_certificate_sidecar_exact(
2546            file,
2547            cx,
2548            tail_offset,
2549            tail_len,
2550            "append-repair window",
2551        )
2552        .await?;
2553        let minimum_candidate_end = tail
2554            .len()
2555            .saturating_sub(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2556            .max(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE);
2557        let mut anchor_end = None;
2558        for candidate_end in (minimum_candidate_end..tail.len()).rev() {
2559            let footer_start =
2560                candidate_end - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
2561            let footer = &tail[footer_start..candidate_end];
2562            let record_len = usize::try_from(u32::from_le_bytes([
2563                footer[0], footer[1], footer[2], footer[3],
2564            ]))
2565            .unwrap_or(usize::MAX);
2566            if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE
2567                ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2568                .contains(&record_len)
2569                || record_len > candidate_end
2570            {
2571                continue;
2572            }
2573            let record_start = candidate_end - record_len;
2574            let record_bytes = &tail[record_start..candidate_end];
2575            if !record_bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
2576                || !durable_certificate_declares_len(record_bytes, record_len)
2577            {
2578                continue;
2579            }
2580            if ParallelWalDurableCertificateRecord::from_bytes(record_bytes).is_ok() {
2581                anchor_end = Some(candidate_end);
2582                break;
2583            }
2584        }
2585
2586        let safe_end = if let Some(anchor_end) = anchor_end {
2587            validate_incomplete_certificate_suffix(&tail[anchor_end..], true)?;
2588            tail_offset
2589                .checked_add(u64::try_from(anchor_end).unwrap_or(u64::MAX))
2590                .ok_or_else(|| FrankenError::WalCorrupt {
2591                    detail: "parallel WAL certificate append-repair boundary overflow".to_owned(),
2592                })?
2593        } else {
2594            if file_size
2595                > u64::try_from(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2596                    .unwrap_or(u64::MAX)
2597            {
2598                return Err(FrankenError::WalCorrupt {
2599                    detail: format!(
2600                        "parallel WAL certificate sidecar has no valid append boundary within its bounded {}-byte recovery suffix",
2601                        PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2602                    ),
2603                });
2604            }
2605            validate_incomplete_certificate_suffix(&tail, false)?;
2606            0
2607        };
2608
2609        if safe_end < file_size {
2610            file.truncate(cx, safe_end)?;
2611        }
2612        Ok(safe_end)
2613    }
2614
2615    async fn append_durable_certificate_record(
2616        &self,
2617        cx: &Cx,
2618        certificate: &ParallelWalCommitCertificate,
2619        wal_frame_start: u64,
2620        wal_frame_end: u64,
2621        sync: bool,
2622    ) -> Result<()> {
2623        self.append_durable_certificate_record_with_completion(
2624            cx,
2625            certificate,
2626            wal_frame_start,
2627            wal_frame_end,
2628            sync,
2629            None,
2630        )
2631        .await
2632    }
2633
2634    async fn append_durable_certificate_record_with_completion(
2635        &self,
2636        cx: &Cx,
2637        certificate: &ParallelWalCommitCertificate,
2638        wal_frame_start: u64,
2639        wal_frame_end: u64,
2640        sync: bool,
2641        completion: Option<&VfsWriteCompletion>,
2642    ) -> Result<()> {
2643        let mut preflight = WalWriteCompletionPreflight::new(completion);
2644        let expected_frame_start = u64::try_from(self.inner.frame_count())
2645            .unwrap_or(u64::MAX)
2646            .saturating_add(1);
2647        if wal_frame_start != expected_frame_start {
2648            return Err(FrankenError::internal(format!(
2649                "parallel WAL certificate starts at frame {wal_frame_start}, expected {expected_frame_start}"
2650            )));
2651        }
2652        let record = ParallelWalDurableCertificateRecord::new(
2653            self.inner.inner().generation_identity(),
2654            wal_frame_start,
2655            wal_frame_end,
2656            certificate.clone(),
2657        )
2658        .map_err(|error| {
2659            FrankenError::internal(format!(
2660                "could not encode parallel WAL durability certificate: {error}"
2661            ))
2662        })?;
2663        let record_bytes = record.to_bytes();
2664        if record_bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2665            return Err(FrankenError::WalCorrupt {
2666                detail: format!(
2667                    "parallel WAL certificate record is {} bytes; maximum is {}",
2668                    record_bytes.len(),
2669                    PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2670                ),
2671            });
2672        }
2673        let certificate_path = self.certificate_sidecar_path();
2674        let existed = self
2675            .vfs
2676            .access(cx, &certificate_path, AccessFlags::EXISTS)?;
2677        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2678        let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
2679        let append_offset = Self::prepare_certificate_sidecar_for_append(&mut file, cx).await?;
2680        preflight.hand_off();
2681        drop(preflight);
2682        let write_result = if let Some(completion) = completion {
2683            file.write_tracked(cx, &record_bytes, append_offset, completion.clone())
2684                .await
2685        } else {
2686            file.write(cx, &record_bytes, append_offset).await
2687        };
2688        if let Err(write_error) = write_result {
2689            // A VFS may report a failed write after changing a prefix of the
2690            // destination. Restore the append boundary under a masked child
2691            // context so a cooperative cancellation cannot leave a torn tail
2692            // when the failed future itself is allowed to finish.
2693            let cleanup_cx = cx.create_child();
2694            let _cleanup_mask = cleanup_cx.masked();
2695            let cleanup_result = file.truncate(&cleanup_cx, append_offset);
2696            let close_result = file.close(&cleanup_cx);
2697            return combine_sidecar_io_results(
2698                "parallel WAL certificate append cleanup failed",
2699                [
2700                    ("write", Err(write_error)),
2701                    ("truncate", cleanup_result),
2702                    ("close", close_result),
2703                ],
2704            );
2705        }
2706
2707        // Match the WAL's configured synchronous policy exactly. Even when
2708        // `sync` is false, this ordered sidecar write precedes the WAL marker;
2709        // neither write then claims power-loss-stable persistence.
2710        let finalization_cx = cx.create_child();
2711        let _finalization_mask = finalization_cx.masked();
2712        let sync_result = if sync {
2713            file.durable_sync(&finalization_cx, SyncKind::FullDurable)
2714        } else {
2715            Ok(())
2716        };
2717        let directory_sync_result = if sync && !existed && sync_result.is_ok() {
2718            self.vfs
2719                .sync_parent_directory(&finalization_cx, &certificate_path)
2720        } else {
2721            Ok(())
2722        };
2723        let close_result = file.close(&finalization_cx);
2724        combine_sidecar_io_results(
2725            "parallel WAL certificate append finalization failed",
2726            [
2727                ("file_sync", sync_result),
2728                ("directory_sync", directory_sync_result),
2729                ("close", close_result),
2730            ],
2731        )
2732    }
2733
2734    async fn reconcile_certificate_sidecar_record(
2735        &self,
2736        cx: &Cx,
2737        expected: &ParallelWalDurableCertificateRecord,
2738        remove_expected_orphan: bool,
2739        sync: bool,
2740    ) -> Result<bool> {
2741        let certificate_path = self.certificate_sidecar_path();
2742        if !self
2743            .vfs
2744            .access(cx, &certificate_path, AccessFlags::EXISTS)?
2745        {
2746            return Ok(false);
2747        }
2748
2749        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::WAL;
2750        let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
2751        let reconciliation_result = async {
2752            let original_size = file.file_size(cx)?;
2753            let safe_end = Self::prepare_certificate_sidecar_for_append(&mut file, cx).await?;
2754            let latest = if safe_end == 0 {
2755                None
2756            } else {
2757                Some(Self::read_certificate_record_ending_at(&file, cx, safe_end).await?)
2758            };
2759            let latest_is_expected = latest
2760                .as_ref()
2761                .is_some_and(|(_, record)| record == expected);
2762            let sidecar_changed = if remove_expected_orphan
2763                && let Some((record_start, _)) = latest.as_ref()
2764                && latest_is_expected
2765            {
2766                file.truncate(cx, *record_start)?;
2767                true
2768            } else {
2769                safe_end != original_size
2770            };
2771            if sync && (latest_is_expected || sidecar_changed) {
2772                file.durable_sync(cx, SyncKind::FullDurable)?;
2773            }
2774            if sync && latest_is_expected && !remove_expected_orphan {
2775                // The original write may have created the sidecar but been
2776                // dropped before its directory entry was fenced.
2777                self.vfs.sync_parent_directory(cx, &certificate_path)?;
2778            }
2779            Ok(latest_is_expected)
2780        }
2781        .await;
2782
2783        let cleanup_cx = cx.create_child();
2784        let _cleanup_mask = cleanup_cx.masked();
2785        let close_result = file.close(&cleanup_cx);
2786        match (reconciliation_result, close_result) {
2787            (Ok(latest_is_expected), Ok(())) => Ok(latest_is_expected),
2788            (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
2789            (Err(reconciliation_error), Err(close_error)) => Err(FrankenError::internal(format!(
2790                "parallel WAL certificate reconciliation failed and close also failed: reconciliation={reconciliation_error}; close={close_error}"
2791            ))),
2792        }
2793    }
2794
2795    async fn persist_checkpoint_certificate_handoff(
2796        &self,
2797        cx: &Cx,
2798        record: &ParallelWalDurableCertificateRecord,
2799    ) -> Result<()> {
2800        let handoff_path = self.certificate_checkpoint_handoff_path();
2801        let existed = self.vfs.access(cx, &handoff_path, AccessFlags::EXISTS)?;
2802        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2803        let (mut file, _) = self.vfs.open(cx, Some(&handoff_path), flags)?;
2804        let record_bytes = record.to_bytes();
2805        if record_bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2806            let cleanup_cx = cx.create_child();
2807            let _cleanup_mask = cleanup_cx.masked();
2808            let close_result = file.close(&cleanup_cx);
2809            return combine_sidecar_io_results(
2810                "parallel WAL checkpoint certificate handoff is oversized",
2811                [
2812                    (
2813                        "record_size",
2814                        Err(FrankenError::WalCorrupt {
2815                            detail: format!(
2816                                "parallel WAL checkpoint certificate handoff is {} bytes; maximum is {}",
2817                                record_bytes.len(),
2818                                PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2819                            ),
2820                        }),
2821                    ),
2822                    ("close", close_result),
2823                ],
2824            );
2825        }
2826        // GH #294: when the handoff sidecar already carries exactly this
2827        // record, rewriting it would only bump its mtime/ctime on every
2828        // close-time checkpoint. The existing bytes were durably synced by
2829        // the write that produced them, so the fence already holds.
2830        if existed {
2831            let file_size = file.file_size(cx)?;
2832            if file_size == u64::try_from(record_bytes.len()).unwrap_or(u64::MAX) {
2833                let mut current = vec![0_u8; record_bytes.len()];
2834                let unchanged = file
2835                    .read(cx, &mut current, 0)
2836                    .await
2837                    .is_ok_and(|bytes_read| {
2838                        bytes_read == record_bytes.len() && current == record_bytes
2839                    });
2840                if unchanged {
2841                    let cleanup_cx = cx.create_child();
2842                    let _cleanup_mask = cleanup_cx.masked();
2843                    return file.close(&cleanup_cx);
2844                }
2845            }
2846        }
2847        // This fence is written before the checkpoint is allowed to reset the
2848        // WAL generation. Once the in-place replacement starts, finish it
2849        // under a cancellation mask; if any stage fails, the checkpoint
2850        // returns before reset and the old WAL remains authoritative.
2851        let mutation_cx = cx.create_child();
2852        let _mutation_mask = mutation_cx.masked();
2853        let truncate_result = file.truncate(&mutation_cx, 0);
2854        let write_result = if truncate_result.is_ok() {
2855            file.write(&mutation_cx, &record_bytes, 0).await
2856        } else {
2857            Ok(())
2858        };
2859        if let Err(write_error) = write_result {
2860            let cleanup_result = file.truncate(&mutation_cx, 0);
2861            let close_result = file.close(&mutation_cx);
2862            return combine_sidecar_io_results(
2863                "parallel WAL checkpoint certificate handoff cleanup failed",
2864                [
2865                    ("truncate_before_write", truncate_result),
2866                    ("write", Err(write_error)),
2867                    ("truncate_after_write", cleanup_result),
2868                    ("close", close_result),
2869                ],
2870            );
2871        }
2872        let sync_result = if truncate_result.is_ok() {
2873            file.durable_sync(&mutation_cx, SyncKind::FullDurable)
2874        } else {
2875            Ok(())
2876        };
2877        let directory_sync_result = if !existed && truncate_result.is_ok() && sync_result.is_ok() {
2878            self.vfs.sync_parent_directory(&mutation_cx, &handoff_path)
2879        } else {
2880            Ok(())
2881        };
2882        let close_result = file.close(&mutation_cx);
2883        combine_sidecar_io_results(
2884            "parallel WAL checkpoint certificate handoff finalization failed",
2885            [
2886                ("truncate", truncate_result),
2887                ("file_sync", sync_result),
2888                ("directory_sync", directory_sync_result),
2889                ("close", close_result),
2890            ],
2891        )
2892    }
2893
2894    async fn checkpoint_certificate_handoff(
2895        &self,
2896        cx: &Cx,
2897    ) -> Result<Option<ParallelWalCommitCertificate>> {
2898        let handoff_path = self.certificate_checkpoint_handoff_path();
2899        if !self.vfs.access(cx, &handoff_path, AccessFlags::EXISTS)? {
2900            return Ok(None);
2901        }
2902        let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
2903        let (mut file, _) = self.vfs.open(cx, Some(&handoff_path), flags)?;
2904        let read_result = async {
2905            let file_size =
2906                usize::try_from(file.file_size(cx)?).map_err(|_| FrankenError::WalCorrupt {
2907                    detail: "parallel WAL checkpoint certificate handoff exceeds usize".to_owned(),
2908                })?;
2909            if file_size == 0 || file_size > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2910                return Err(FrankenError::WalCorrupt {
2911                    detail: format!(
2912                        "parallel WAL checkpoint certificate handoff has invalid size {file_size}"
2913                    ),
2914                });
2915            }
2916            let mut bytes = vec![0_u8; file_size];
2917            let bytes_read = file.read(cx, &mut bytes, 0).await?;
2918            if bytes_read != bytes.len() {
2919                return Err(FrankenError::WalCorrupt {
2920                    detail: "parallel WAL checkpoint certificate handoff was short-read".to_owned(),
2921                });
2922            }
2923            let record =
2924                ParallelWalDurableCertificateRecord::from_bytes(&bytes).map_err(|error| {
2925                    FrankenError::WalCorrupt {
2926                        detail: format!(
2927                            "parallel WAL checkpoint certificate handoff is invalid: {error}"
2928                        ),
2929                    }
2930                })?;
2931            Ok(Some(record.certificate))
2932        }
2933        .await;
2934        let cleanup_cx = cx.create_child();
2935        let _cleanup_mask = cleanup_cx.masked();
2936        let close_result = file.close(&cleanup_cx);
2937        match (read_result, close_result) {
2938            (Ok(certificate), Ok(())) => Ok(certificate),
2939            (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
2940            (Err(read_error), Err(close_error)) => Err(FrankenError::internal(format!(
2941                "parallel WAL checkpoint handoff read failed and close also failed: read={read_error}; close={close_error}"
2942            ))),
2943        }
2944    }
2945
2946    async fn wal_frame_payload_digest(
2947        &self,
2948        cx: &Cx,
2949        wal_frame_start: u64,
2950        wal_frame_end: u64,
2951    ) -> Result<[u8; 32]> {
2952        if wal_frame_start == 0 || wal_frame_end < wal_frame_start {
2953            return Err(FrankenError::WalCorrupt {
2954                detail: format!(
2955                    "invalid parallel WAL digest interval {wal_frame_start}..={wal_frame_end}"
2956                ),
2957            });
2958        }
2959
2960        let mut digest = ParallelWalFramePayloadDigestBuilder::new();
2961        for frame_number in wal_frame_start..=wal_frame_end {
2962            let frame_index = usize::try_from(frame_number.saturating_sub(1)).map_err(|_| {
2963                FrankenError::WalCorrupt {
2964                    detail: format!(
2965                        "parallel WAL digest frame number {frame_number} exceeds usize"
2966                    ),
2967                }
2968            })?;
2969            let (header, page_data) = self.inner.inner().read_frame(cx, frame_index).await?;
2970            let page_number =
2971                PageNumber::new(header.page_number).ok_or_else(|| FrankenError::WalCorrupt {
2972                    detail: format!(
2973                        "parallel WAL digest frame {frame_number} has invalid page number {}",
2974                        header.page_number
2975                    ),
2976                })?;
2977            digest.update(page_number, header.db_size, &page_data);
2978        }
2979        Ok(digest.finalize())
2980    }
2981
2982    async fn latest_authorized_durable_certificate_record(
2983        &self,
2984        cx: &Cx,
2985    ) -> Result<Option<ParallelWalDurableCertificateRecord>> {
2986        let certificate_path = self.certificate_sidecar_path();
2987        if !self
2988            .vfs
2989            .access(cx, &certificate_path, AccessFlags::EXISTS)?
2990        {
2991            return Ok(None);
2992        }
2993        let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
2994        let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
2995        let read_result = async {
2996            let file_size = file.file_size(cx)?;
2997            if file_size == 0 {
2998                return Ok(None);
2999            }
3000
3001            // Healthy operation is O(1): the final four bytes identify the
3002            // exact newest record, so only its footer and bytes are read.
3003            let footer_size =
3004                u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE)
3005                    .unwrap_or(4);
3006            let mut newest = None;
3007            if file_size >= footer_size {
3008                let footer_offset = file_size - footer_size;
3009                let footer = Self::read_certificate_sidecar_exact(
3010                    &file,
3011                    cx,
3012                    footer_offset,
3013                    ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
3014                    "newest length footer",
3015                )
3016                .await?;
3017                let record_len = usize::try_from(u32::from_le_bytes([
3018                    footer[0], footer[1], footer[2], footer[3],
3019                ]))
3020                .map_err(|_| FrankenError::WalCorrupt {
3021                    detail: "parallel WAL certificate newest footer length exceeds usize"
3022                        .to_owned(),
3023                })?;
3024                let record_len_u64 = u64::try_from(record_len).unwrap_or(u64::MAX);
3025                if (MIN_DURABLE_CERTIFICATE_RECORD_SIZE
3026                    ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3027                    .contains(&record_len)
3028                    && record_len_u64 <= file_size
3029                {
3030                    let record_start = file_size - record_len_u64;
3031                    let bytes = Self::read_certificate_sidecar_exact(
3032                        &file,
3033                        cx,
3034                        record_start,
3035                        record_len,
3036                        "newest record",
3037                    )
3038                    .await?;
3039                    // A matching magic or self-declared length makes this a
3040                    // fully-present envelope candidate. Strict decoding is
3041                    // mandatory even when its magic/version/CRC/footer is
3042                    // corrupt; complete corruption is never a torn suffix.
3043                    if bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
3044                        || durable_certificate_declares_len(&bytes, record_len)
3045                    {
3046                        let record =
3047                            decode_durable_certificate_record(&bytes, "newest record")?;
3048                        newest = Some((record_start, record));
3049                    }
3050                }
3051            }
3052
3053            if newest.is_none() {
3054                // Invalid EOF footer means the last append may have torn.
3055                // Search only footer-derived candidates within one maximum
3056                // suffix, retaining enough preceding bytes for one maximum
3057                // anchor record. Magic is only a cheap validation after a
3058                // candidate footer establishes an exact boundary; it is never
3059                // used as a free-form scan key.
3060                let recovery_window_size =
3061                    PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE.saturating_mul(2);
3062                let recovery_window_size_u64 =
3063                    u64::try_from(recovery_window_size).unwrap_or(u64::MAX);
3064                let tail_offset = file_size.saturating_sub(recovery_window_size_u64);
3065                let tail_len =
3066                    usize::try_from(file_size - tail_offset).map_err(|_| {
3067                        FrankenError::WalCorrupt {
3068                            detail: "parallel WAL certificate recovery window exceeds usize"
3069                                .to_owned(),
3070                        }
3071                    })?;
3072                let tail = Self::read_certificate_sidecar_exact(
3073                    &file,
3074                    cx,
3075                    tail_offset,
3076                    tail_len,
3077                    "recovery window",
3078                )
3079                .await?;
3080                let minimum_candidate_end = tail
3081                    .len()
3082                    .saturating_sub(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3083                    .max(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE);
3084                let mut anchor = None;
3085                for candidate_end in (minimum_candidate_end..tail.len()).rev() {
3086                    let footer_start = candidate_end
3087                        - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
3088                    let footer = &tail[footer_start..candidate_end];
3089                    let record_len = usize::try_from(u32::from_le_bytes([
3090                        footer[0], footer[1], footer[2], footer[3],
3091                    ]))
3092                    .unwrap_or(usize::MAX);
3093                    if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE
3094                        ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3095                        .contains(&record_len)
3096                        || record_len > candidate_end
3097                    {
3098                        continue;
3099                    }
3100                    let record_start = candidate_end - record_len;
3101                    let record_bytes = &tail[record_start..candidate_end];
3102                    if !record_bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
3103                        || !durable_certificate_declares_len(record_bytes, record_len)
3104                    {
3105                        continue;
3106                    }
3107                    if let Ok(record) =
3108                        ParallelWalDurableCertificateRecord::from_bytes(record_bytes)
3109                    {
3110                        anchor = Some((record_start, candidate_end, record));
3111                        break;
3112                    }
3113                }
3114
3115                if let Some((record_start, record_end, record)) = anchor {
3116                    validate_incomplete_certificate_suffix(&tail[record_end..], true)?;
3117                    let absolute_start = tail_offset
3118                        .checked_add(u64::try_from(record_start).unwrap_or(u64::MAX))
3119                        .ok_or_else(|| FrankenError::WalCorrupt {
3120                            detail:
3121                                "parallel WAL certificate recovery anchor offset overflow"
3122                                    .to_owned(),
3123                        })?;
3124                    newest = Some((absolute_start, record));
3125                } else {
3126                    if file_size
3127                        > u64::try_from(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3128                            .unwrap_or(u64::MAX)
3129                    {
3130                        return Err(FrankenError::WalCorrupt {
3131                            detail: format!(
3132                                "parallel WAL certificate sidecar has no valid record within its bounded {}-byte recovery suffix",
3133                                PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
3134                            ),
3135                        });
3136                    }
3137                    validate_incomplete_certificate_suffix(&tail, false)?;
3138                    return Ok(None);
3139                }
3140            }
3141
3142            let valid_frame_count = u64::try_from(self.inner.frame_count()).unwrap_or(u64::MAX);
3143            let wal_generation = self.inner.inner().generation_identity();
3144            let (mut record_start, mut record) = newest.ok_or_else(|| {
3145                FrankenError::WalCorrupt {
3146                    detail: "parallel WAL certificate recovery produced no record".to_owned(),
3147                }
3148            })?;
3149            let mut unauthorized_records = 0_usize;
3150            loop {
3151                // Append order is generation order. Once the newest tail
3152                // belongs to a prior reset generation, no earlier sidecar
3153                // record can authorize the current WAL; checkpoint clock
3154                // continuation comes from the fixed handoff anchor instead.
3155                if record.wal_generation != wal_generation {
3156                    return Ok(None);
3157                }
3158                let frame_index =
3159                    usize::try_from(record.wal_frame_end.saturating_sub(1)).map_err(|_| {
3160                        FrankenError::WalCorrupt {
3161                            detail: "parallel WAL certificate commit-marker index exceeds usize"
3162                                .to_owned(),
3163                        }
3164                    })?;
3165                let commit_marker_frame = if frame_index < self.inner.frame_count()
3166                    && self
3167                        .inner
3168                        .inner()
3169                        .read_frame_header(cx, frame_index)
3170                        .await?
3171                        .is_commit()
3172                {
3173                    record.wal_frame_end
3174                } else {
3175                    0
3176                };
3177                let actual_wal_frame_payload_digest =
3178                    if commit_marker_frame == record.wal_frame_end {
3179                        Some(
3180                            self.wal_frame_payload_digest(
3181                                cx,
3182                                record.wal_frame_start,
3183                                record.wal_frame_end,
3184                            )
3185                            .await?,
3186                        )
3187                    } else {
3188                        None
3189                    };
3190                if actual_wal_frame_payload_digest.is_some_and(|actual_digest| {
3191                    record.authorizes_wal_boundary(
3192                        wal_generation,
3193                        valid_frame_count,
3194                        commit_marker_frame,
3195                        actual_digest,
3196                    )
3197                }) {
3198                    return Ok(Some(record));
3199                }
3200
3201                unauthorized_records = unauthorized_records.saturating_add(1);
3202                if unauthorized_records > MAX_ORPHAN_CERTIFICATE_LOOKBACK {
3203                    return Err(FrankenError::WalCorrupt {
3204                        detail: format!(
3205                            "parallel WAL certificate sidecar exceeded bounded orphan lookback {MAX_ORPHAN_CERTIFICATE_LOOKBACK}"
3206                        ),
3207                    });
3208                }
3209                tracing::debug!(
3210                    target: "fsqlite::wal::durability_combiner",
3211                    orphan_certificate_epoch = record.certificate.certificate_epoch,
3212                    orphan_commit_seq_hi = record.certificate.commit_seq_hi.get(),
3213                    orphan_wal_frame_end = record.wal_frame_end,
3214                    lookback = unauthorized_records,
3215                    "ignored unauthorized parallel WAL certificate tail"
3216                );
3217                if record_start == 0 {
3218                    return Ok(None);
3219                }
3220                (record_start, record) =
3221                    Self::read_certificate_record_ending_at(&file, cx, record_start).await?;
3222            }
3223        }
3224        .await;
3225        let cleanup_cx = cx.create_child();
3226        let _cleanup_mask = cleanup_cx.masked();
3227        let close_result = file.close(&cleanup_cx);
3228        match (read_result, close_result) {
3229            (Ok(certificate), Ok(())) => Ok(certificate),
3230            (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
3231            (Err(read_error), Err(close_error)) => Err(FrankenError::internal(format!(
3232                "parallel WAL certificate tail read failed and close also failed: read={read_error}; close={close_error}"
3233            ))),
3234        }
3235    }
3236}
3237
3238impl<V> WalBackend for PathRefreshingWalBackend<V>
3239where
3240    V: Vfs + 'static,
3241    V::File: Send + Sync + 'static,
3242{
3243    fn begin_transaction<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
3244        Box::pin(async move {
3245            self.ensure_current_wal_path(cx).await?;
3246            self.inner.begin_transaction(cx).await
3247        })
3248    }
3249
3250    fn published_snapshot(&self) -> Option<WalPublicationSnapshot> {
3251        Some(self.inner.published_snapshot())
3252    }
3253
3254    fn pinned_read_snapshot(&self) -> Option<WalPublicationSnapshot> {
3255        self.inner.pinned_read_snapshot()
3256    }
3257
3258    fn pinned_logical_read_snapshot<'a>(
3259        &'a self,
3260        cx: &'a Cx,
3261    ) -> WalFuture<'a, Option<WalLogicalReadSnapshot>> {
3262        Box::pin(async move {
3263            let Some(pinned) = self.inner.pinned_read_snapshot() else {
3264                return Ok(None);
3265            };
3266            let Some(last_commit_frame) = pinned.last_commit_frame else {
3267                return Ok(None);
3268            };
3269            let Some(record) = self
3270                .latest_authorized_durable_certificate_record(cx)
3271                .await?
3272            else {
3273                return Ok(None);
3274            };
3275            if record.wal_generation != pinned.generation {
3276                return Err(FrankenError::WalCorrupt {
3277                    detail: "current logical WAL certificate generation differs from pinned reader"
3278                        .to_owned(),
3279                });
3280            }
3281            let certificate_commit_frame =
3282                usize::try_from(record.wal_frame_end.checked_sub(1).ok_or_else(|| {
3283                    FrankenError::WalCorrupt {
3284                        detail: "current logical WAL certificate ends at frame zero".to_owned(),
3285                    }
3286                })?)
3287                .map_err(|_| FrankenError::WalCorrupt {
3288                    detail: "current logical WAL certificate frame exceeds usize".to_owned(),
3289                })?;
3290            if certificate_commit_frame > last_commit_frame {
3291                return Err(FrankenError::WalCorrupt {
3292                    detail: "current logical WAL certificate extends past pinned reader horizon"
3293                        .to_owned(),
3294                });
3295            }
3296
3297            let first_tail_frame =
3298                usize::try_from(record.wal_frame_end).map_err(|_| FrankenError::WalCorrupt {
3299                    detail: "logical WAL tail frame exceeds usize".to_owned(),
3300                })?;
3301            let mut tail_commit_count = 0_u64;
3302            if first_tail_frame <= last_commit_frame {
3303                for frame_index in first_tail_frame..=last_commit_frame {
3304                    if self
3305                        .inner
3306                        .inner()
3307                        .read_frame_header(cx, frame_index)
3308                        .await?
3309                        .is_commit()
3310                    {
3311                        tail_commit_count = tail_commit_count.checked_add(1).ok_or_else(|| {
3312                            FrankenError::WalCorrupt {
3313                                detail: "logical WAL tail commit count overflow".to_owned(),
3314                            }
3315                        })?;
3316                    }
3317                }
3318            }
3319            let visible_commit_seq = CommitSeq::new(
3320                record
3321                    .certificate
3322                    .commit_seq_hi
3323                    .get()
3324                    .checked_add(tail_commit_count)
3325                    .ok_or_else(|| FrankenError::WalCorrupt {
3326                        detail: "logical WAL visible commit sequence overflow".to_owned(),
3327                    })?,
3328            );
3329            Ok(Some(WalLogicalReadSnapshot {
3330                generation: pinned.generation,
3331                last_commit_frame: pinned.last_commit_frame,
3332                visible_commit_seq,
3333            }))
3334        })
3335    }
3336
3337    fn refresh_published_snapshot<'a>(
3338        &'a mut self,
3339        cx: &'a Cx,
3340    ) -> WalFuture<'a, Option<WalPublicationSnapshot>> {
3341        Box::pin(async move {
3342            self.ensure_current_wal_path(cx).await?;
3343            self.inner.refresh_published_snapshot(cx).await.map(Some)
3344        })
3345    }
3346
3347    fn publish_authorized_deferred_commit<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
3348        Box::pin(async move { self.inner.publish_authorized_deferred_commit(cx) })
3349    }
3350
3351    fn append_frame<'a>(
3352        &'a mut self,
3353        cx: &'a Cx,
3354        page_number: u32,
3355        page_data: &'a [u8],
3356        db_size_if_commit: u32,
3357    ) -> WalFuture<'a, ()> {
3358        Box::pin(async move {
3359            self.ensure_current_wal_path(cx).await?;
3360            self.inner
3361                .append_frame(cx, page_number, page_data, db_size_if_commit)
3362                .await
3363        })
3364    }
3365
3366    fn append_frames<'a>(
3367        &'a mut self,
3368        cx: &'a Cx,
3369        frames: &'a [WalFrameRef<'a>],
3370    ) -> WalFuture<'a, ()> {
3371        Box::pin(async move {
3372            self.ensure_current_wal_path(cx).await?;
3373            self.inner.append_frames(cx, frames).await
3374        })
3375    }
3376
3377    fn append_frames_tracked<'a>(
3378        &'a mut self,
3379        cx: &'a Cx,
3380        frames: &'a [WalFrameRef<'a>],
3381        completion: VfsWriteCompletion,
3382    ) -> WalFuture<'a, ()> {
3383        Box::pin(async move {
3384            let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3385            self.ensure_current_wal_path(cx).await?;
3386            preflight.hand_off();
3387            drop(preflight);
3388            self.inner
3389                .append_frames_tracked(cx, frames, completion)
3390                .await
3391        })
3392    }
3393
3394    fn prepare_append_frames(
3395        &self,
3396        frames: &[WalFrameRef<'_>],
3397    ) -> Result<Option<PreparedWalFrameBatch>> {
3398        self.inner.prepare_append_frames(frames)
3399    }
3400
3401    fn finalize_prepared_frames(
3402        &self,
3403        cx: &Cx,
3404        prepared: &mut PreparedWalFrameBatch,
3405    ) -> Result<()> {
3406        self.inner.finalize_prepared_frames(cx, prepared)
3407    }
3408
3409    fn append_prepared_frames<'a>(
3410        &'a mut self,
3411        cx: &'a Cx,
3412        prepared: &'a mut PreparedWalFrameBatch,
3413    ) -> WalFuture<'a, ()> {
3414        Box::pin(async move {
3415            self.ensure_current_wal_path(cx).await?;
3416            self.inner.append_prepared_frames(cx, prepared).await
3417        })
3418    }
3419
3420    fn append_prepared_frames_tracked<'a>(
3421        &'a mut self,
3422        cx: &'a Cx,
3423        prepared: &'a mut PreparedWalFrameBatch,
3424        completion: VfsWriteCompletion,
3425    ) -> WalFuture<'a, ()> {
3426        Box::pin(async move {
3427            let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3428            self.ensure_current_wal_path(cx).await?;
3429            preflight.hand_off();
3430            drop(preflight);
3431            self.inner
3432                .append_prepared_frames_tracked(cx, prepared, completion)
3433                .await
3434        })
3435    }
3436
3437    fn persist_parallel_wal_commit_certificate<'a>(
3438        &'a mut self,
3439        cx: &'a Cx,
3440        certificate: &'a ParallelWalCommitCertificate,
3441        wal_frame_start: u64,
3442        wal_frame_end: u64,
3443        sync: bool,
3444    ) -> WalFuture<'a, ()> {
3445        Box::pin(async move {
3446            self.ensure_current_wal_path(cx).await?;
3447            self.append_durable_certificate_record(
3448                cx,
3449                certificate,
3450                wal_frame_start,
3451                wal_frame_end,
3452                sync,
3453            )
3454            .await
3455        })
3456    }
3457
3458    fn persist_parallel_wal_commit_certificate_tracked<'a>(
3459        &'a mut self,
3460        cx: &'a Cx,
3461        certificate: &'a ParallelWalCommitCertificate,
3462        wal_frame_start: u64,
3463        wal_frame_end: u64,
3464        sync: bool,
3465        completion: VfsWriteCompletion,
3466    ) -> WalFuture<'a, ()> {
3467        Box::pin(async move {
3468            let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3469            self.ensure_current_wal_path(cx).await?;
3470            preflight.hand_off();
3471            drop(preflight);
3472            self.append_durable_certificate_record_with_completion(
3473                cx,
3474                certificate,
3475                wal_frame_start,
3476                wal_frame_end,
3477                sync,
3478                Some(&completion),
3479            )
3480            .await
3481        })
3482    }
3483
3484    fn reconcile_parallel_wal_commit<'a>(
3485        &'a mut self,
3486        cx: &'a Cx,
3487        certificate: &'a ParallelWalCommitCertificate,
3488        wal_frame_start: u64,
3489        wal_frame_end: u64,
3490        sync: bool,
3491    ) -> WalFuture<'a, ParallelWalCommitReconciliation> {
3492        Box::pin(async move {
3493            self.ensure_current_wal_path(cx).await?;
3494            self.inner.wal.refresh(cx).await?;
3495            let wal_generation = self.inner.wal.generation_identity();
3496            let expected_record = ParallelWalDurableCertificateRecord::new(
3497                wal_generation,
3498                wal_frame_start,
3499                wal_frame_end,
3500                certificate.clone(),
3501            )
3502            .map_err(|error| {
3503                FrankenError::internal(format!(
3504                    "could not reconstruct in-doubt parallel WAL certificate: {error}"
3505                ))
3506            })?;
3507
3508            let valid_frame_count = u64::try_from(self.inner.wal.frame_count()).unwrap_or(u64::MAX);
3509            let target_commit_present = if valid_frame_count < wal_frame_end {
3510                false
3511            } else {
3512                let target_index =
3513                    usize::try_from(wal_frame_end.saturating_sub(1)).map_err(|_| {
3514                        FrankenError::WalCorrupt {
3515                            detail: "in-doubt WAL commit-marker index exceeds usize".to_owned(),
3516                        }
3517                    })?;
3518                self.inner
3519                    .wal
3520                    .read_frame_header(cx, target_index)
3521                    .await?
3522                    .is_commit()
3523            };
3524
3525            if target_commit_present {
3526                if valid_frame_count != wal_frame_end {
3527                    return Err(FrankenError::WalCorrupt {
3528                        detail: format!(
3529                            "in-doubt parallel WAL interval ends at frame {wal_frame_end}, but the retained writer gate observed committed frame count {valid_frame_count}"
3530                        ),
3531                    });
3532                }
3533                let actual_wal_frame_payload_digest = self
3534                    .wal_frame_payload_digest(cx, wal_frame_start, wal_frame_end)
3535                    .await?;
3536                if !expected_record.authorizes_wal_boundary(
3537                    wal_generation,
3538                    valid_frame_count,
3539                    wal_frame_end,
3540                    actual_wal_frame_payload_digest,
3541                ) {
3542                    return Err(FrankenError::WalCorrupt {
3543                        detail: format!(
3544                            "in-doubt parallel WAL interval {wal_frame_start}..={wal_frame_end} does not match its content-bound certificate"
3545                        ),
3546                    });
3547                }
3548                let sidecar_is_exact = self
3549                    .reconcile_certificate_sidecar_record(cx, &expected_record, false, sync)
3550                    .await?;
3551                if !sidecar_is_exact {
3552                    return Err(FrankenError::WalCorrupt {
3553                        detail: format!(
3554                            "parallel WAL commit marker at frame {wal_frame_end} has no exact durable certificate"
3555                        ),
3556                    });
3557                }
3558                if sync {
3559                    self.inner.wal.sync(cx, SyncFlags::NORMAL)?;
3560                    self.vfs.sync_parent_directory(cx, &self.wal_path)?;
3561                }
3562                return Ok(ParallelWalCommitReconciliation::Authorized);
3563            }
3564
3565            let committed_prefix_before =
3566                wal_frame_start
3567                    .checked_sub(1)
3568                    .ok_or_else(|| FrankenError::WalCorrupt {
3569                        detail: "parallel WAL recovery interval starts at frame zero".to_owned(),
3570                    })?;
3571            if valid_frame_count != committed_prefix_before {
3572                return Err(FrankenError::WalCorrupt {
3573                    detail: format!(
3574                        "in-doubt WAL interval {wal_frame_start}..={wal_frame_end} has unexpected committed prefix {valid_frame_count}"
3575                    ),
3576                });
3577            }
3578            // Only after the live WAL shape is classified as the exact
3579            // pre-interval prefix may reconciliation repair torn sidecar bytes
3580            // or remove the matching orphan certificate. Unexpected WAL state
3581            // preserves all durable evidence for diagnosis and retry.
3582            self.reconcile_certificate_sidecar_record(cx, &expected_record, true, sync)
3583                .await?;
3584            self.inner.wal.repair_uncommitted_tail(cx)?;
3585            if sync {
3586                self.inner.wal.sync(cx, SyncFlags::NORMAL)?;
3587                self.vfs.sync_parent_directory(cx, &self.wal_path)?;
3588            }
3589            Ok(ParallelWalCommitReconciliation::NotCommitted)
3590        })
3591    }
3592
3593    fn latest_authorized_parallel_wal_commit_certificate<'a>(
3594        &'a mut self,
3595        cx: &'a Cx,
3596    ) -> WalFuture<'a, Option<ParallelWalCommitCertificate>> {
3597        Box::pin(async move {
3598            self.ensure_current_wal_path(cx).await?;
3599            if let Some(record) = self
3600                .latest_authorized_durable_certificate_record(cx)
3601                .await?
3602            {
3603                return Ok(Some(record.certificate));
3604            }
3605            self.checkpoint_certificate_handoff(cx).await
3606        })
3607    }
3608
3609    fn read_page<'a>(&'a mut self, cx: &'a Cx, page_number: u32) -> WalFuture<'a, Option<Vec<u8>>> {
3610        Box::pin(async move {
3611            self.ensure_current_wal_path(cx).await?;
3612            self.inner.read_page(cx, page_number).await
3613        })
3614    }
3615
3616    fn read_page_pinned<'a>(
3617        &'a self,
3618        cx: &'a Cx,
3619        page_number: u32,
3620    ) -> WalFuture<'a, Option<Vec<u8>>> {
3621        Box::pin(async move { self.inner.read_page_pinned(cx, page_number).await })
3622    }
3623
3624    fn supports_pinned_reads(&self) -> bool {
3625        self.inner.supports_pinned_reads()
3626    }
3627
3628    fn committed_txns_since_page<'a>(
3629        &'a mut self,
3630        cx: &'a Cx,
3631        page_number: u32,
3632    ) -> WalFuture<'a, u64> {
3633        Box::pin(async move {
3634            self.ensure_current_wal_path(cx).await?;
3635            self.inner.committed_txns_since_page(cx, page_number).await
3636        })
3637    }
3638
3639    fn conflicting_pages_since_snapshot<'a>(
3640        &'a mut self,
3641        cx: &'a Cx,
3642        snapshot: TransactionConflictSnapshot,
3643        page_numbers: &'a [u32],
3644        page_baselines: &'a [TransactionConflictPageBaseline],
3645    ) -> WalFuture<'a, Vec<u32>> {
3646        Box::pin(async move {
3647            self.ensure_current_wal_path(cx).await?;
3648            let latest = self.inner.refresh_published_snapshot(cx).await?;
3649            if latest.generation != snapshot.generation {
3650                return Ok(self
3651                    .conflicts_after_generation_change(cx, page_numbers, page_baselines)
3652                    .await);
3653            }
3654            self.inner
3655                .conflicting_pages_since_snapshot(cx, snapshot, page_numbers, page_baselines)
3656                .await
3657        })
3658    }
3659
3660    fn committed_txn_count<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, u64> {
3661        Box::pin(async move {
3662            self.ensure_current_wal_path(cx).await?;
3663            self.inner.committed_txn_count(cx).await
3664        })
3665    }
3666
3667    fn sync(&mut self, cx: &Cx) -> Result<()> {
3668        #[cfg(all(feature = "native", any(unix, windows)))]
3669        if let Some(binding) = &self.namespace_binding {
3670            binding.validate_path_identity()?;
3671        }
3672        self.inner.sync(cx)
3673    }
3674
3675    fn frame_count(&self) -> usize {
3676        self.inner.frame_count()
3677    }
3678
3679    fn checkpoint<'a>(
3680        &'a mut self,
3681        cx: &'a Cx,
3682        mode: CheckpointMode,
3683        writer: &'a mut dyn CheckpointPageWriter,
3684        backfilled_frames: u32,
3685        oldest_reader_frame: Option<u32>,
3686    ) -> WalFuture<'a, CheckpointResult> {
3687        Box::pin(async move {
3688            self.ensure_current_wal_path(cx).await?;
3689            let checkpoint_handoff = self
3690                .latest_authorized_durable_certificate_record(cx)
3691                .await?;
3692            if let Some(record) = checkpoint_handoff.as_ref() {
3693                // Fence the certificate clock before the checkpoint is
3694                // allowed to reset the WAL generation. Replacing the handoff
3695                // is intentionally non-authoritative while the old WAL and
3696                // sidecar remain reconstructible: a crash, cancellation, or
3697                // write failure here aborts the checkpoint without destroying
3698                // the previous generation's source of truth.
3699                self.persist_checkpoint_certificate_handoff(cx, record)
3700                    .await?;
3701            }
3702            let result = self
3703                .inner
3704                .checkpoint(cx, mode, writer, backfilled_frames, oldest_reader_frame)
3705                .await?;
3706            Ok(result)
3707        })
3708    }
3709}
3710
3711/// Adapter wrapping a `&mut dyn CheckpointPageWriter` to implement `CheckpointTarget`.
3712///
3713/// This is used internally by `WalBackendAdapter::checkpoint` to bridge the
3714/// pager's writer to the WAL executor's target trait.
3715struct CheckpointTargetAdapterRef<'a> {
3716    writer: &'a mut dyn CheckpointPageWriter,
3717}
3718
3719impl CheckpointTarget for CheckpointTargetAdapterRef<'_> {
3720    fn write_page<'a>(
3721        &'a mut self,
3722        cx: &'a Cx,
3723        page_no: PageNumber,
3724        data: &'a [u8],
3725    ) -> CheckpointTargetFuture<'a, ()> {
3726        Box::pin(async move { self.writer.write_page(cx, page_no, data).await })
3727    }
3728
3729    fn truncate_db<'a>(&'a mut self, cx: &'a Cx, n_pages: u32) -> CheckpointTargetFuture<'a, ()> {
3730        Box::pin(async move { self.writer.truncate(cx, n_pages).await })
3731    }
3732
3733    fn sync_db<'a>(&'a mut self, cx: &'a Cx) -> CheckpointTargetFuture<'a, ()> {
3734        Box::pin(async move { self.writer.sync(cx).await })
3735    }
3736}
3737
3738// ---------------------------------------------------------------------------
3739// Tests
3740// ---------------------------------------------------------------------------
3741
3742#[cfg(test)]
3743mod tests {
3744    use std::sync::Mutex;
3745
3746    use fsqlite_pager::MockCheckpointPageWriter;
3747    use fsqlite_pager::traits::WalFrameRef;
3748    use fsqlite_types::flags::VfsOpenFlags;
3749    use fsqlite_vfs::MemoryVfs;
3750    use fsqlite_vfs::traits::{Vfs, VfsFile};
3751    use fsqlite_wal::checksum::WalSalts;
3752
3753    use super::*;
3754
3755    const PAGE_SIZE: u32 = 4096;
3756    const CERTIFICATE_PATH: &str = "test.db-wal-cert";
3757    const CHECKPOINT_HANDOFF_PATH: &str = "test.db-wal-cert-head";
3758
3759    #[derive(Clone, Copy, Debug)]
3760    enum CheckpointHandoffWriteFault {
3761        Error,
3762        Pending,
3763    }
3764
3765    #[derive(Clone, Debug, Eq, PartialEq)]
3766    enum CertificateSyncObservation {
3767        Ordinary(PathBuf),
3768        Durable(PathBuf, SyncKind),
3769    }
3770
3771    #[derive(Debug, Default)]
3772    struct CheckpointHandoffFaultState {
3773        next_write: Option<CheckpointHandoffWriteFault>,
3774        fail_next_sync: bool,
3775        /// Fail the next sync on a non-handoff (i.e. WAL) file.
3776        fail_next_wal_sync: bool,
3777        sync_observations: Vec<CertificateSyncObservation>,
3778    }
3779
3780    #[derive(Clone, Debug)]
3781    struct CheckpointHandoffFaultVfs {
3782        inner: MemoryVfs,
3783        faults: Arc<Mutex<CheckpointHandoffFaultState>>,
3784    }
3785
3786    impl CheckpointHandoffFaultVfs {
3787        fn new() -> Self {
3788            Self {
3789                inner: MemoryVfs::new(),
3790                faults: Arc::new(Mutex::new(CheckpointHandoffFaultState::default())),
3791            }
3792        }
3793
3794        fn fail_next_handoff_write(&self) {
3795            self.faults
3796                .lock()
3797                .unwrap_or_else(std::sync::PoisonError::into_inner)
3798                .next_write = Some(CheckpointHandoffWriteFault::Error);
3799        }
3800
3801        fn pend_next_handoff_write(&self) {
3802            self.faults
3803                .lock()
3804                .unwrap_or_else(std::sync::PoisonError::into_inner)
3805                .next_write = Some(CheckpointHandoffWriteFault::Pending);
3806        }
3807
3808        fn fail_next_handoff_sync(&self) {
3809            self.faults
3810                .lock()
3811                .unwrap_or_else(std::sync::PoisonError::into_inner)
3812                .fail_next_sync = true;
3813        }
3814
3815        /// Arm a one-shot sync failure on the WAL file itself.
3816        fn fail_next_wal_sync(&self) {
3817            self.faults
3818                .lock()
3819                .unwrap_or_else(std::sync::PoisonError::into_inner)
3820                .fail_next_wal_sync = true;
3821        }
3822
3823        fn take_sync_observations(&self) -> Vec<CertificateSyncObservation> {
3824            std::mem::take(
3825                &mut self
3826                    .faults
3827                    .lock()
3828                    .unwrap_or_else(std::sync::PoisonError::into_inner)
3829                    .sync_observations,
3830            )
3831        }
3832    }
3833
3834    #[derive(Debug)]
3835    struct CheckpointHandoffFaultFile {
3836        inner: <MemoryVfs as Vfs>::File,
3837        faults: Arc<Mutex<CheckpointHandoffFaultState>>,
3838        path: Option<PathBuf>,
3839        is_checkpoint_handoff: bool,
3840    }
3841
3842    impl Vfs for CheckpointHandoffFaultVfs {
3843        type File = CheckpointHandoffFaultFile;
3844
3845        fn name(&self) -> &'static str {
3846            "checkpoint-handoff-fault"
3847        }
3848
3849        fn open(
3850            &self,
3851            cx: &Cx,
3852            path: Option<&Path>,
3853            flags: VfsOpenFlags,
3854        ) -> Result<(Self::File, VfsOpenFlags)> {
3855            let is_checkpoint_handoff =
3856                path.is_some_and(|candidate| candidate == Path::new(CHECKPOINT_HANDOFF_PATH));
3857            let (inner, actual_flags) = self.inner.open(cx, path, flags)?;
3858            Ok((
3859                CheckpointHandoffFaultFile {
3860                    inner,
3861                    faults: Arc::clone(&self.faults),
3862                    path: path.map(Path::to_path_buf),
3863                    is_checkpoint_handoff,
3864                },
3865                actual_flags,
3866            ))
3867        }
3868
3869        fn delete(&self, cx: &Cx, path: &Path, sync_dir: bool) -> Result<()> {
3870            self.inner.delete(cx, path, sync_dir)
3871        }
3872
3873        fn sync_parent_directory(&self, cx: &Cx, path: &Path) -> Result<()> {
3874            self.inner.sync_parent_directory(cx, path)
3875        }
3876
3877        fn access(&self, cx: &Cx, path: &Path, flags: AccessFlags) -> Result<bool> {
3878            self.inner.access(cx, path, flags)
3879        }
3880
3881        fn path_entry_exists(&self, cx: &Cx, path: &Path) -> Result<bool> {
3882            self.inner.path_entry_exists(cx, path)
3883        }
3884
3885        fn full_pathname(&self, cx: &Cx, path: &Path) -> Result<PathBuf> {
3886            self.inner.full_pathname(cx, path)
3887        }
3888
3889        fn randomness(&self, cx: &Cx, buf: &mut [u8]) {
3890            self.inner.randomness(cx, buf);
3891        }
3892
3893        fn current_time(&self, cx: &Cx) -> f64 {
3894            self.inner.current_time(cx)
3895        }
3896
3897        fn is_memory(&self) -> bool {
3898            true
3899        }
3900    }
3901
3902    impl VfsFile for CheckpointHandoffFaultFile {
3903        fn close(&mut self, cx: &Cx) -> Result<()> {
3904            self.inner.close(cx)
3905        }
3906
3907        fn file_identity(&self) -> Result<Option<fsqlite_vfs::FileIdentity>> {
3908            self.inner.file_identity()
3909        }
3910
3911        fn read<'a>(
3912            &'a self,
3913            cx: &'a Cx,
3914            buf: &'a mut [u8],
3915            offset: u64,
3916        ) -> impl std::future::Future<Output = Result<usize>> + Send + 'a {
3917            self.inner.read(cx, buf, offset)
3918        }
3919
3920        async fn write<'a>(&'a self, cx: &'a Cx, buf: &'a [u8], offset: u64) -> Result<()> {
3921            let fault = if self.is_checkpoint_handoff {
3922                self.faults
3923                    .lock()
3924                    .unwrap_or_else(std::sync::PoisonError::into_inner)
3925                    .next_write
3926                    .take()
3927            } else {
3928                None
3929            };
3930            match fault {
3931                Some(CheckpointHandoffWriteFault::Error) => Err(FrankenError::Io(
3932                    std::io::Error::other("injected checkpoint handoff write failure"),
3933                )),
3934                Some(CheckpointHandoffWriteFault::Pending) => {
3935                    std::future::pending::<Result<()>>().await
3936                }
3937                None => self.inner.write(cx, buf, offset).await,
3938            }
3939        }
3940
3941        fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()> {
3942            self.inner.truncate(cx, size)
3943        }
3944
3945        fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()> {
3946            let mut faults = self
3947                .faults
3948                .lock()
3949                .unwrap_or_else(std::sync::PoisonError::into_inner);
3950            if let Some(path) = self.path.as_ref().filter(|path| {
3951                path.as_path() == Path::new(CERTIFICATE_PATH)
3952                    || path.as_path() == Path::new(CHECKPOINT_HANDOFF_PATH)
3953            }) {
3954                faults
3955                    .sync_observations
3956                    .push(CertificateSyncObservation::Ordinary(path.clone()));
3957            }
3958            let fail = self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_sync);
3959            let fail_wal =
3960                !self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_wal_sync);
3961            drop(faults);
3962            if fail {
3963                Err(FrankenError::Io(std::io::Error::other(
3964                    "injected checkpoint handoff sync failure",
3965                )))
3966            } else if fail_wal {
3967                Err(FrankenError::Io(std::io::Error::other(
3968                    "injected WAL sync failure",
3969                )))
3970            } else {
3971                self.inner.sync(cx, flags)
3972            }
3973        }
3974
3975        fn durable_sync(&mut self, cx: &Cx, kind: SyncKind) -> Result<()> {
3976            let mut faults = self
3977                .faults
3978                .lock()
3979                .unwrap_or_else(std::sync::PoisonError::into_inner);
3980            if let Some(path) = self.path.as_ref().filter(|path| {
3981                path.as_path() == Path::new(CERTIFICATE_PATH)
3982                    || path.as_path() == Path::new(CHECKPOINT_HANDOFF_PATH)
3983            }) {
3984                faults
3985                    .sync_observations
3986                    .push(CertificateSyncObservation::Durable(path.clone(), kind));
3987            }
3988            let fail = self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_sync);
3989            drop(faults);
3990            if fail {
3991                Err(FrankenError::Io(std::io::Error::other(
3992                    "injected checkpoint handoff durable-sync failure",
3993                )))
3994            } else {
3995                self.inner.durable_sync(cx, kind)
3996            }
3997        }
3998
3999        fn file_size(&self, cx: &Cx) -> Result<u64> {
4000            self.inner.file_size(cx)
4001        }
4002
4003        fn lock(&mut self, cx: &Cx, level: fsqlite_types::LockLevel) -> Result<()> {
4004            self.inner.lock(cx, level)
4005        }
4006
4007        fn unlock(&mut self, cx: &Cx, level: fsqlite_types::LockLevel) -> Result<()> {
4008            self.inner.unlock(cx, level)
4009        }
4010
4011        fn lock_external_shared_snapshot(&mut self, cx: &Cx) -> Result<()> {
4012            self.inner.lock_external_shared_snapshot(cx)
4013        }
4014
4015        fn restore_external_shared_snapshot_attempt(&mut self, cx: &Cx) -> Result<()> {
4016            self.inner.restore_external_shared_snapshot_attempt(cx)
4017        }
4018
4019        fn lock_external_maintenance(&mut self, cx: &Cx, wal_mode: bool) -> Result<()> {
4020            self.inner.lock_external_maintenance(cx, wal_mode)
4021        }
4022
4023        fn restore_external_maintenance_attempt(&mut self, cx: &Cx) -> Result<()> {
4024            self.inner.restore_external_maintenance_attempt(cx)
4025        }
4026
4027        fn check_reserved_lock(&self, cx: &Cx) -> Result<bool> {
4028            self.inner.check_reserved_lock(cx)
4029        }
4030
4031        fn sector_size(&self) -> u32 {
4032            self.inner.sector_size()
4033        }
4034
4035        fn device_characteristics(&self) -> u32 {
4036            self.inner.device_characteristics()
4037        }
4038
4039        fn shm_map(
4040            &mut self,
4041            cx: &Cx,
4042            region: u32,
4043            size: u32,
4044            extend: bool,
4045        ) -> Result<fsqlite_vfs::ShmRegion> {
4046            self.inner.shm_map(cx, region, size, extend)
4047        }
4048
4049        fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()> {
4050            self.inner.shm_lock(cx, offset, n, flags)
4051        }
4052
4053        fn shm_barrier(&self) {
4054            self.inner.shm_barrier();
4055        }
4056
4057        fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()> {
4058            self.inner.shm_unmap(cx, delete)
4059        }
4060
4061        fn set_busy_timeout_ms(&mut self, ms: u64) {
4062            self.inner.set_busy_timeout_ms(ms);
4063        }
4064    }
4065
4066    /// Deliberate no-op (frankensqlite#299).
4067    ///
4068    /// This helper previously installed a process-global `TRACE` subscriber via
4069    /// `tracing_subscriber::fmt()...with_test_writer().try_init()`. `try_init()`
4070    /// is process-wide and first-caller-wins, so the first of the 9 callers
4071    /// changed tracing enablement — and libtest output capture — for every
4072    /// unrelated test running afterwards in this binary, making a later failure
4073    /// replay the whole captured global trace stream.
4074    ///
4075    /// `fsqlite-core` already fixed the identical pattern in b262b6a6 for its
4076    /// other helpers; this one was missed. No caller here asserts on emitted
4077    /// trace events, so the body is simply removed and the call sites are kept
4078    /// so the diff stays test-only.
4079    ///
4080    /// See `wal_publication_tracing_helper_installs_no_global_subscriber`.
4081    fn init_wal_publication_test_tracing() {}
4082
4083    /// frankensqlite#299 regression: the WAL publication tracing helper must not
4084    /// install, or otherwise disturb, a process-global subscriber.
4085    ///
4086    /// Only the equality assertion is made, deliberately. Unlike the pager
4087    /// crate, this test binary contains another global-subscriber installation
4088    /// site outside this file, so an absolute `!has_been_set()` assertion would
4089    /// be order-dependent and could fail for reasons unrelated to this helper.
4090    /// Comparing dispatcher state across the call is untaintable and proves the
4091    /// exact property under test: that this helper is inert.
4092    #[test]
4093    fn wal_publication_tracing_helper_installs_no_global_subscriber() {
4094        let before = tracing::dispatcher::has_been_set();
4095        init_wal_publication_test_tracing();
4096
4097        assert_eq!(
4098            before,
4099            tracing::dispatcher::has_been_set(),
4100            "init_wal_publication_test_tracing must not install or alter a global subscriber"
4101        );
4102    }
4103
4104    fn test_cx() -> Cx {
4105        Cx::default()
4106    }
4107
4108    fn test_salts() -> WalSalts {
4109        WalSalts {
4110            salt1: 0xDEAD_BEEF,
4111            salt2: 0xCAFE_BABE,
4112        }
4113    }
4114
4115    fn sample_page(seed: u8) -> Vec<u8> {
4116        let page_size = usize::try_from(PAGE_SIZE).expect("page size fits usize");
4117        let mut page = vec![0u8; page_size];
4118        for (i, byte) in page.iter_mut().enumerate() {
4119            let reduced = u8::try_from(i % 251).expect("modulo fits u8");
4120            *byte = reduced ^ seed;
4121        }
4122        page
4123    }
4124
4125    fn test_frame_payload_digest(
4126        page_number: u32,
4127        page_data: &[u8],
4128        db_size_if_commit: u32,
4129    ) -> [u8; 32] {
4130        let mut digest = ParallelWalFramePayloadDigestBuilder::new();
4131        digest.update(
4132            PageNumber::new(page_number).expect("test page number must be valid"),
4133            db_size_if_commit,
4134            page_data,
4135        );
4136        digest.finalize()
4137    }
4138
4139    fn sample_certificate(
4140        certificate_epoch: u64,
4141        commit_seq: u64,
4142        lane_record_counts: Vec<u32>,
4143    ) -> ParallelWalCommitCertificate {
4144        let lane_count = u16::try_from(lane_record_counts.len()).expect("test lane count fits u16");
4145        let mut certificate = ParallelWalCommitCertificate {
4146            format_version: fsqlite_wal::PARALLEL_WAL_COMMIT_CERTIFICATE_VERSION,
4147            residue: fsqlite_wal::ParallelWalOrderedResidue::CommitCertificateThenPublish,
4148            certificate_epoch,
4149            commit_seq_lo: fsqlite_types::CommitSeq::new(commit_seq),
4150            commit_seq_hi: fsqlite_types::CommitSeq::new(commit_seq),
4151            durable_segment_epoch: certificate_epoch,
4152            lane_count,
4153            lane_record_counts,
4154            db_size_pages: 1,
4155            page_set_size: 1,
4156            wal_frame_payload_digest: [0xA5; 32],
4157            certificate_crc32c: 0,
4158            fallback_active: false,
4159        };
4160        certificate.certificate_crc32c = certificate.computed_crc32c();
4161        certificate
4162    }
4163
4164    fn make_path_refreshing_backend(
4165        vfs: &MemoryVfs,
4166        cx: &Cx,
4167    ) -> PathRefreshingWalBackend<MemoryVfs> {
4168        let wal = WalFile::create(cx, open_wal_file(vfs, cx), PAGE_SIZE, 0, test_salts())
4169            .expect("create WAL");
4170        PathRefreshingWalBackend::new(
4171            vfs.clone(),
4172            std::path::Path::new("test.db"),
4173            std::path::Path::new("test.db-wal"),
4174            PAGE_SIZE,
4175            wal,
4176            true,
4177            #[cfg(all(feature = "native", any(unix, windows)))]
4178            None,
4179        )
4180    }
4181
4182    fn make_authorized_certificate_backend(
4183        vfs: &MemoryVfs,
4184        cx: &Cx,
4185    ) -> (
4186        PathRefreshingWalBackend<MemoryVfs>,
4187        ParallelWalCommitCertificate,
4188    ) {
4189        let mut backend = make_path_refreshing_backend(vfs, cx);
4190        let committed_page = sample_page(0x44);
4191        let mut certificate = sample_certificate(1, 1, vec![1]);
4192        certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &committed_page, 1);
4193        certificate.certificate_crc32c = certificate.computed_crc32c();
4194        backend
4195            .persist_parallel_wal_commit_certificate(cx, &certificate, 1, 1, true)
4196            .expect("persist authorized certificate");
4197        backend
4198            .append_frame(cx, 1, &committed_page, 1)
4199            .expect("append matching commit marker");
4200        backend.sync(cx).expect("sync matching commit marker");
4201        (backend, certificate)
4202    }
4203
4204    struct AuthoritativeWalSnapshot {
4205        generation: WalGenerationIdentity,
4206        frame_count: usize,
4207        wal_bytes: Vec<u8>,
4208        certificate: ParallelWalCommitCertificate,
4209        committed_page: Vec<u8>,
4210    }
4211
4212    fn make_checkpoint_handoff_fault_backend(
4213        vfs: &CheckpointHandoffFaultVfs,
4214        cx: &Cx,
4215    ) -> (
4216        PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4217        ParallelWalCommitCertificate,
4218        Vec<u8>,
4219    ) {
4220        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
4221        let (file, _) = vfs
4222            .open(cx, Some(Path::new("test.db-wal")), flags)
4223            .expect("open fault-injected WAL file");
4224        let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts())
4225            .expect("create fault-injected WAL");
4226        let mut backend = PathRefreshingWalBackend::new(
4227            vfs.clone(),
4228            Path::new("test.db"),
4229            Path::new("test.db-wal"),
4230            PAGE_SIZE,
4231            wal,
4232            true,
4233            #[cfg(all(feature = "native", any(unix, windows)))]
4234            None,
4235        );
4236        let committed_page = sample_page(0x47);
4237        let mut certificate = sample_certificate(1, 1, vec![1]);
4238        certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &committed_page, 1);
4239        certificate.certificate_crc32c = certificate.computed_crc32c();
4240        backend
4241            .persist_parallel_wal_commit_certificate(cx, &certificate, 1, 1, true)
4242            .expect("persist authorized certificate");
4243        backend
4244            .append_frame(cx, 1, &committed_page, 1)
4245            .expect("append matching commit marker");
4246        backend.sync(cx).expect("sync matching commit marker");
4247        (backend, certificate, committed_page)
4248    }
4249
4250    fn read_fault_injected_wal(vfs: &CheckpointHandoffFaultVfs, cx: &Cx) -> Vec<u8> {
4251        let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
4252        let (mut file, _) = vfs
4253            .open(cx, Some(Path::new("test.db-wal")), flags)
4254            .expect("open WAL snapshot");
4255        let len = usize::try_from(file.file_size(cx).expect("read WAL size"))
4256            .expect("WAL size fits usize");
4257        let mut bytes = vec![0_u8; len];
4258        assert_eq!(
4259            file.read(cx, &mut bytes, 0).expect("read WAL snapshot"),
4260            len
4261        );
4262        file.close(cx).expect("close WAL snapshot");
4263        bytes
4264    }
4265
4266    fn capture_authoritative_wal(
4267        backend: &PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4268        vfs: &CheckpointHandoffFaultVfs,
4269        cx: &Cx,
4270        certificate: ParallelWalCommitCertificate,
4271        committed_page: Vec<u8>,
4272    ) -> AuthoritativeWalSnapshot {
4273        AuthoritativeWalSnapshot {
4274            generation: backend.inner.inner().generation_identity(),
4275            frame_count: backend.inner.frame_count(),
4276            wal_bytes: read_fault_injected_wal(vfs, cx),
4277            certificate,
4278            committed_page,
4279        }
4280    }
4281
4282    fn assert_authoritative_wal_unchanged(
4283        backend: &mut PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4284        vfs: &CheckpointHandoffFaultVfs,
4285        cx: &Cx,
4286        before: &AuthoritativeWalSnapshot,
4287    ) {
4288        assert_eq!(
4289            backend.inner.inner().generation_identity(),
4290            before.generation,
4291            "checkpoint handoff failure must not reset the WAL generation"
4292        );
4293        assert_eq!(
4294            backend.inner.frame_count(),
4295            before.frame_count,
4296            "checkpoint handoff failure must not change the visible frame count"
4297        );
4298        assert_eq!(
4299            read_fault_injected_wal(vfs, cx),
4300            before.wal_bytes,
4301            "checkpoint handoff failure must leave the authoritative WAL byte-for-byte unchanged"
4302        );
4303        assert!(
4304            backend
4305                .inner
4306                .inner()
4307                .read_frame_header(cx, 0)
4308                .expect("read original commit frame")
4309                .is_commit(),
4310            "the original generation's commit marker must remain authoritative"
4311        );
4312        assert_eq!(
4313            backend
4314                .latest_authorized_parallel_wal_commit_certificate(cx)
4315                .expect("recover certificate from unchanged WAL generation"),
4316            Some(before.certificate.clone())
4317        );
4318        assert_eq!(
4319            backend
4320                .read_page(cx, 1)
4321                .expect("read committed page from unchanged WAL generation"),
4322            Some(before.committed_page.clone())
4323        );
4324    }
4325
4326    fn read_certificate_sidecar(vfs: &MemoryVfs, cx: &Cx) -> Vec<u8> {
4327        let path = std::path::Path::new("test.db-wal-cert");
4328        let (mut file, _) = vfs
4329            .open(cx, Some(path), VfsOpenFlags::READONLY | VfsOpenFlags::WAL)
4330            .expect("open certificate sidecar");
4331        let len = usize::try_from(file.file_size(cx).expect("read certificate sidecar size"))
4332            .expect("certificate sidecar size fits usize");
4333        let mut bytes = vec![0_u8; len];
4334        assert_eq!(
4335            file.read(cx, &mut bytes, 0)
4336                .expect("read certificate sidecar"),
4337            len
4338        );
4339        file.close(cx).expect("close certificate sidecar");
4340        bytes
4341    }
4342
4343    fn replace_certificate_sidecar(vfs: &MemoryVfs, cx: &Cx, bytes: &[u8]) {
4344        let path = std::path::Path::new("test.db-wal-cert");
4345        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
4346        let (mut file, _) = vfs
4347            .open(cx, Some(path), flags)
4348            .expect("open mutable certificate sidecar");
4349        file.truncate(cx, 0)
4350            .expect("truncate mutable certificate sidecar");
4351        file.write(cx, bytes, 0)
4352            .expect("replace certificate sidecar bytes");
4353        file.close(cx).expect("close mutable certificate sidecar");
4354    }
4355
4356    fn assert_wal_corrupt<T: std::fmt::Debug>(result: Result<T>, scenario: &str) {
4357        assert!(
4358            matches!(&result, Err(FrankenError::WalCorrupt { .. })),
4359            "{scenario} must fail closed with WalCorrupt, got {result:?}"
4360        );
4361    }
4362
4363    fn sqlite_page_one(encoded_page_size: u16) -> Vec<u8> {
4364        let mut page = sample_page(0x11);
4365        page[..16].copy_from_slice(b"SQLite format 3\0");
4366        page[16..18].copy_from_slice(&encoded_page_size.to_be_bytes());
4367        page
4368    }
4369
4370    fn write_main_db_pages(vfs: &MemoryVfs, cx: &Cx, pages: &[Vec<u8>]) {
4371        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::MAIN_DB;
4372        let (mut file, _) = vfs
4373            .open(cx, Some(std::path::Path::new("test.db")), flags)
4374            .expect("open main database");
4375        file.truncate(cx, 0).expect("truncate main database");
4376        for (index, page) in pages.iter().enumerate() {
4377            let offset = u64::try_from(index)
4378                .expect("page index fits u64")
4379                .saturating_mul(u64::from(PAGE_SIZE));
4380            file.write(cx, page, offset).expect("write database page");
4381        }
4382        file.close(cx).expect("close main database");
4383    }
4384
4385    fn replacement_salts() -> WalSalts {
4386        WalSalts {
4387            salt1: 0x1234_5678,
4388            salt2: 0x9ABC_DEF0,
4389        }
4390    }
4391
4392    fn replace_path_visible_wal(vfs: &MemoryVfs, cx: &Cx) {
4393        let wal_path = std::path::Path::new("test.db-wal");
4394        vfs.delete(cx, wal_path, false)
4395            .expect("remove old path-visible WAL");
4396        let file = open_wal_file(vfs, cx);
4397        WalFile::create(cx, file, PAGE_SIZE, 1, replacement_salts())
4398            .expect("create replacement WAL")
4399            .close(cx)
4400            .expect("close replacement WAL");
4401    }
4402
4403    fn append_replacement_wal_page(
4404        vfs: &MemoryVfs,
4405        cx: &Cx,
4406        page_number: u32,
4407        page: &[u8],
4408        db_size_if_commit: u32,
4409    ) {
4410        let file = open_wal_file(vfs, cx);
4411        let wal = WalFile::open(cx, file).expect("open replacement WAL");
4412        let mut adapter = WalBackendAdapter::new(wal);
4413        adapter
4414            .append_frame(cx, page_number, page, db_size_if_commit)
4415            .expect("append replacement WAL page");
4416        adapter.sync(cx).expect("sync replacement WAL page");
4417        adapter
4418            .into_inner()
4419            .expect("sync drained the staged frames")
4420            .close(cx)
4421            .expect("close replacement WAL");
4422    }
4423
4424    fn make_generation_transition_backend(
4425        vfs: &MemoryVfs,
4426        cx: &Cx,
4427    ) -> (
4428        PathRefreshingWalBackend<MemoryVfs>,
4429        TransactionConflictSnapshot,
4430        Vec<u8>,
4431    ) {
4432        let page_one = sqlite_page_one(u16::try_from(PAGE_SIZE).expect("page size fits u16"));
4433        let page_two = sample_page(0x22);
4434        write_main_db_pages(vfs, cx, &[page_one.clone(), page_two.clone()]);
4435
4436        let file = open_wal_file(vfs, cx);
4437        let wal =
4438            WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create original WAL");
4439        let mut backend = PathRefreshingWalBackend::new(
4440            vfs.clone(),
4441            std::path::Path::new("test.db"),
4442            std::path::Path::new("test.db-wal"),
4443            PAGE_SIZE,
4444            wal,
4445            true,
4446            #[cfg(all(feature = "native", any(unix, windows)))]
4447            None,
4448        );
4449        backend
4450            .append_frame(cx, 1, &page_one, 0)
4451            .expect("append original page 1");
4452        backend
4453            .append_frame(cx, 2, &page_two, 2)
4454            .expect("append original commit");
4455        backend
4456            .begin_transaction(cx)
4457            .expect("pin original WAL generation");
4458        let pinned = backend.pinned_read_snapshot().expect("pinned WAL snapshot");
4459        let snapshot = TransactionConflictSnapshot {
4460            generation: pinned.generation,
4461            last_commit_frame: pinned.last_commit_frame,
4462            commit_count: pinned.commit_count,
4463        };
4464        replace_path_visible_wal(vfs, cx);
4465        (backend, snapshot, page_two)
4466    }
4467
4468    #[test]
4469    fn durable_certificate_sidecar_precedes_and_reconstructs_wal_commit() {
4470        let cx = test_cx();
4471        let vfs = MemoryVfs::new();
4472        let committed_page = sample_page(0x44);
4473        let file = open_wal_file(&vfs, &cx);
4474        let wal = WalFile::create(&cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
4475        let mut backend = PathRefreshingWalBackend::new(
4476            vfs.clone(),
4477            std::path::Path::new("test.db"),
4478            std::path::Path::new("test.db-wal"),
4479            PAGE_SIZE,
4480            wal,
4481            true,
4482            #[cfg(all(feature = "native", any(unix, windows)))]
4483            None,
4484        );
4485        let mut certificate = ParallelWalCommitCertificate {
4486            format_version: fsqlite_wal::PARALLEL_WAL_COMMIT_CERTIFICATE_VERSION,
4487            residue: fsqlite_wal::ParallelWalOrderedResidue::CommitCertificateThenPublish,
4488            certificate_epoch: 1,
4489            commit_seq_lo: fsqlite_types::CommitSeq::new(1),
4490            commit_seq_hi: fsqlite_types::CommitSeq::new(1),
4491            durable_segment_epoch: 1,
4492            lane_count: 1,
4493            lane_record_counts: vec![1],
4494            db_size_pages: 1,
4495            page_set_size: 1,
4496            wal_frame_payload_digest: test_frame_payload_digest(1, &committed_page, 1),
4497            certificate_crc32c: 0,
4498            fallback_active: false,
4499        };
4500        certificate.certificate_crc32c = certificate.computed_crc32c();
4501
4502        backend
4503            .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4504            .expect("persist certificate before WAL commit marker");
4505        assert_eq!(
4506            backend.inner.frame_count(),
4507            0,
4508            "certificate persistence must not itself expose a WAL commit marker"
4509        );
4510
4511        let certificate_path = std::path::Path::new("test.db-wal-cert");
4512        let (mut certificate_file, _) = vfs
4513            .open(
4514                &cx,
4515                Some(certificate_path),
4516                VfsOpenFlags::READONLY | VfsOpenFlags::WAL,
4517            )
4518            .expect("open certificate sidecar");
4519        let certificate_len = usize::try_from(
4520            certificate_file
4521                .file_size(&cx)
4522                .expect("certificate sidecar size"),
4523        )
4524        .expect("certificate sidecar size fits usize");
4525        let mut record_bytes = vec![0_u8; certificate_len];
4526        assert_eq!(
4527            certificate_file
4528                .read(&cx, &mut record_bytes, 0)
4529                .expect("read certificate sidecar"),
4530            certificate_len
4531        );
4532        certificate_file
4533            .close(&cx)
4534            .expect("close certificate sidecar");
4535        let reconstructed = ParallelWalDurableCertificateRecord::from_bytes(&record_bytes)
4536            .expect("reconstruct durable certificate record");
4537        assert_eq!(reconstructed.certificate, certificate);
4538        assert_eq!(reconstructed.wal_frame_start, 1);
4539        assert_eq!(reconstructed.wal_frame_end, 1);
4540        assert_eq!(
4541            reconstructed.wal_generation,
4542            backend.inner.inner().generation_identity()
4543        );
4544        assert!(
4545            !reconstructed.authorizes_wal_boundary(
4546                backend.inner.inner().generation_identity(),
4547                0,
4548                0,
4549                test_frame_payload_digest(1, &committed_page, 1),
4550            ),
4551            "orphan certificate must not authorize visibility before the matching commit marker"
4552        );
4553
4554        backend
4555            .append_frame(&cx, 1, &committed_page, 1)
4556            .expect("append matching WAL commit marker");
4557        backend.sync(&cx).expect("sync WAL commit marker");
4558        assert!(
4559            backend
4560                .inner
4561                .inner()
4562                .read_frame_header(&cx, 0)
4563                .expect("read matching WAL commit frame")
4564                .is_commit()
4565        );
4566        assert!(reconstructed.authorizes_wal_boundary(
4567            backend.inner.inner().generation_identity(),
4568            1,
4569            1,
4570            test_frame_payload_digest(1, &committed_page, 1),
4571        ));
4572
4573        let (mut certificate_file, _) = vfs
4574            .open(
4575                &cx,
4576                Some(certificate_path),
4577                VfsOpenFlags::READWRITE | VfsOpenFlags::WAL,
4578            )
4579            .expect("reopen certificate sidecar");
4580        let torn_offset = certificate_file
4581            .file_size(&cx)
4582            .expect("certificate sidecar size before torn tail");
4583        certificate_file
4584            .write(&cx, &[0xA5], torn_offset)
4585            .expect("append torn footer byte");
4586        certificate_file
4587            .close(&cx)
4588            .expect("close sidecar with torn tail");
4589        let recovered = backend
4590            .latest_authorized_parallel_wal_commit_certificate(&cx)
4591            .wait()
4592            .expect("torn certificate tail should recover the prior valid record")
4593            .expect("prior authorized certificate should remain discoverable");
4594        assert_eq!(recovered, certificate);
4595    }
4596
4597    #[test]
4598    fn content_mismatched_wal_interval_cannot_be_authorized_or_repaired() {
4599        let cx = test_cx();
4600        let vfs = MemoryVfs::new();
4601        let certified_page = sample_page(0x61);
4602        let actual_page = sample_page(0x62);
4603        let mut backend = make_path_refreshing_backend(&vfs, &cx);
4604        let mut certificate = sample_certificate(1, 1, vec![1]);
4605        certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &certified_page, 1);
4606        certificate.certificate_crc32c = certificate.computed_crc32c();
4607
4608        backend
4609            .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4610            .expect("persist content-bound certificate");
4611        backend
4612            .append_frame(&cx, 1, &actual_page, 1)
4613            .expect("append differently valued commit frame");
4614        backend.sync(&cx).expect("sync mismatched commit frame");
4615
4616        let sidecar_before = read_certificate_sidecar(&vfs, &cx);
4617        assert!(
4618            backend
4619                .latest_authorized_parallel_wal_commit_certificate(&cx)
4620                .wait()
4621                .expect("content mismatch is a non-authorizing record")
4622                .is_none(),
4623            "matching generation and commit marker must not authorize different frame bytes"
4624        );
4625
4626        assert_wal_corrupt(
4627            backend
4628                .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
4629                .wait(),
4630            "in-doubt content-bound reconciliation mismatch",
4631        );
4632        assert_eq!(
4633            read_certificate_sidecar(&vfs, &cx),
4634            sidecar_before,
4635            "digest mismatch must be diagnosed before sidecar repair"
4636        );
4637        assert_eq!(
4638            backend.inner.frame_count(),
4639            1,
4640            "digest mismatch must preserve the live WAL for diagnosis and retry"
4641        );
4642    }
4643
4644    #[test]
4645    fn absent_commit_marker_repairs_certificate_and_partial_wal_tail() {
4646        let cx = test_cx();
4647        let vfs = MemoryVfs::new();
4648        let mut backend = make_path_refreshing_backend(&vfs, &cx);
4649        let certificate = sample_certificate(1, 1, vec![1]);
4650        backend
4651            .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4652            .expect("persist orphan certificate");
4653
4654        let (mut tail_writer, _) = vfs
4655            .open(
4656                &cx,
4657                Some(std::path::Path::new("test.db-wal")),
4658                VfsOpenFlags::READWRITE | VfsOpenFlags::WAL,
4659            )
4660            .expect("open WAL for partial-tail injection");
4661        let committed_size = tail_writer.file_size(&cx).expect("read committed WAL size");
4662        tail_writer
4663            .write(&cx, &[0xA5; 7], committed_size)
4664            .expect("inject a partial physical frame");
4665        assert!(
4666            tail_writer.file_size(&cx).expect("read extended WAL size") > committed_size,
4667            "fault fixture must extend the physical WAL"
4668        );
4669        tail_writer.close(&cx).expect("close partial-tail injector");
4670
4671        assert_eq!(
4672            backend
4673                .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
4674                .wait()
4675                .expect("missing commit marker must be exactly repairable"),
4676            ParallelWalCommitReconciliation::NotCommitted
4677        );
4678        assert!(
4679            read_certificate_sidecar(&vfs, &cx).is_empty(),
4680            "matching orphan certificate must be removed after NotCommitted proof"
4681        );
4682        let (mut repaired_wal, _) = vfs
4683            .open(
4684                &cx,
4685                Some(std::path::Path::new("test.db-wal")),
4686                VfsOpenFlags::READONLY | VfsOpenFlags::WAL,
4687            )
4688            .expect("open repaired WAL");
4689        assert_eq!(
4690            repaired_wal.file_size(&cx).expect("read repaired WAL size"),
4691            committed_size,
4692            "NotCommitted reconciliation must truncate the physical partial tail"
4693        );
4694        repaired_wal.close(&cx).expect("close repaired WAL");
4695    }
4696
4697    #[test]
4698    fn durable_certificate_recovery_accepts_every_truncated_record_prefix() {
4699        let cx = test_cx();
4700        let vfs = MemoryVfs::new();
4701        let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
4702        let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4703        let orphan = sample_certificate(2, 2, vec![1]);
4704        let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4705            backend.inner.inner().generation_identity(),
4706            2,
4707            2,
4708            orphan,
4709        )
4710        .expect("construct orphan record")
4711        .to_bytes();
4712
4713        for prefix_len in 1..orphan_bytes.len() {
4714            let mut sidecar = authorized_bytes.clone();
4715            sidecar.extend_from_slice(&orphan_bytes[..prefix_len]);
4716            replace_certificate_sidecar(&vfs, &cx, &sidecar);
4717            let recovered_result = backend
4718                .latest_authorized_parallel_wal_commit_certificate(&cx)
4719                .wait();
4720            assert!(
4721                recovered_result.is_ok(),
4722                "truncated certificate prefix of {prefix_len} bytes must recover: {recovered_result:?}"
4723            );
4724            let recovered = recovered_result
4725                .expect("truncated certificate recovery was asserted successful")
4726                .expect("authorized record must remain discoverable");
4727            assert_eq!(recovered, authorized, "failed at prefix {prefix_len}");
4728        }
4729    }
4730
4731    #[test]
4732    fn durable_certificate_append_repairs_the_accepted_torn_suffix() {
4733        let cx = test_cx();
4734        let vfs = MemoryVfs::new();
4735        let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
4736        let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4737        let orphan = sample_certificate(2, 2, vec![1]);
4738        let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4739            backend.inner.inner().generation_identity(),
4740            2,
4741            2,
4742            orphan.clone(),
4743        )
4744        .expect("construct orphan record")
4745        .to_bytes();
4746        for prefix_len in 1..orphan_bytes.len() {
4747            let mut torn_sidecar = authorized_bytes.clone();
4748            torn_sidecar.extend_from_slice(&orphan_bytes[..prefix_len]);
4749            replace_certificate_sidecar(&vfs, &cx, &torn_sidecar);
4750
4751            assert_eq!(
4752                backend
4753                    .latest_authorized_parallel_wal_commit_certificate(&cx)
4754                    .wait()
4755                    .expect("one torn suffix should recover")
4756                    .expect("authorized predecessor remains visible"),
4757                authorized,
4758                "read recovery failed for prefix {prefix_len}"
4759            );
4760
4761            backend
4762                .persist_parallel_wal_commit_certificate(&cx, &orphan, 2, 2, true)
4763                .expect("next append repairs the torn suffix first");
4764            let repaired_sidecar = read_certificate_sidecar(&vfs, &cx);
4765            assert_eq!(
4766                repaired_sidecar.len(),
4767                authorized_bytes.len() + orphan_bytes.len(),
4768                "replacement record did not start at the prior complete boundary for prefix {prefix_len}"
4769            );
4770            assert_eq!(
4771                backend
4772                    .latest_authorized_parallel_wal_commit_certificate(&cx)
4773                    .wait()
4774                    .expect("orphan lookback crosses the repaired boundary")
4775                    .expect("authorized predecessor remains discoverable"),
4776                authorized,
4777                "orphan lookback failed after repairing prefix {prefix_len}"
4778            );
4779        }
4780
4781        let mut corrupt_record = orphan_bytes;
4782        let envelope_crc_offset =
4783            corrupt_record.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE - 4;
4784        corrupt_record[envelope_crc_offset] ^= 0x80;
4785        let mut corrupt_sidecar = authorized_bytes;
4786        corrupt_sidecar.extend_from_slice(&corrupt_record);
4787        replace_certificate_sidecar(&vfs, &cx, &corrupt_sidecar);
4788        assert_wal_corrupt(
4789            backend
4790                .persist_parallel_wal_commit_certificate(&cx, &orphan, 2, 2, true)
4791                .wait(),
4792            "append-time complete record corruption",
4793        );
4794    }
4795
4796    #[test]
4797    fn durable_certificate_recovery_rejects_complete_corruption_and_garbage() {
4798        let cx = test_cx();
4799        let vfs = MemoryVfs::new();
4800        let (mut backend, _) = make_authorized_certificate_backend(&vfs, &cx);
4801        let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4802        let orphan = sample_certificate(2, 2, vec![1]);
4803        let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4804            backend.inner.inner().generation_identity(),
4805            2,
4806            2,
4807            orphan,
4808        )
4809        .expect("construct orphan record")
4810        .to_bytes();
4811
4812        let mut bad_crc = orphan_bytes.clone();
4813        let envelope_crc_offset =
4814            bad_crc.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE - 4;
4815        bad_crc[envelope_crc_offset] ^= 0x80;
4816        let mut sidecar = authorized_bytes.clone();
4817        sidecar.extend_from_slice(&bad_crc);
4818        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4819        assert_wal_corrupt(
4820            backend
4821                .latest_authorized_parallel_wal_commit_certificate(&cx)
4822                .wait(),
4823            "complete record with bad CRC",
4824        );
4825
4826        let mut bad_version = orphan_bytes.clone();
4827        bad_version[8] ^= 0x01;
4828        let mut sidecar = authorized_bytes.clone();
4829        sidecar.extend_from_slice(&bad_version);
4830        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4831        assert_wal_corrupt(
4832            backend
4833                .latest_authorized_parallel_wal_commit_certificate(&cx)
4834                .wait(),
4835            "complete record with bad version",
4836        );
4837
4838        let mut bad_magic = orphan_bytes.clone();
4839        bad_magic[0] ^= 0x01;
4840        let mut sidecar = authorized_bytes.clone();
4841        sidecar.extend_from_slice(&bad_magic);
4842        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4843        assert_wal_corrupt(
4844            backend
4845                .latest_authorized_parallel_wal_commit_certificate(&cx)
4846                .wait(),
4847            "complete record with bad magic",
4848        );
4849
4850        let mut bad_footer = orphan_bytes;
4851        let footer_offset =
4852            bad_footer.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
4853        bad_footer[footer_offset] ^= 0x80;
4854        let mut sidecar = authorized_bytes;
4855        sidecar.extend_from_slice(&bad_footer);
4856        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4857        assert_wal_corrupt(
4858            backend
4859                .latest_authorized_parallel_wal_commit_certificate(&cx)
4860                .wait(),
4861            "complete record with bad footer",
4862        );
4863
4864        let garbage_vfs = MemoryVfs::new();
4865        let mut garbage_backend = make_path_refreshing_backend(&garbage_vfs, &cx);
4866        replace_certificate_sidecar(&garbage_vfs, &cx, &[0xA5; 128]);
4867        assert_wal_corrupt(
4868            garbage_backend
4869                .latest_authorized_parallel_wal_commit_certificate(&cx)
4870                .wait(),
4871            "nonempty garbage sidecar",
4872        );
4873
4874        let mut fake_magic = vec![0_u8; MIN_DURABLE_CERTIFICATE_RECORD_SIZE];
4875        fake_magic[..PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.len()]
4876            .copy_from_slice(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC);
4877        fake_magic[8..10].copy_from_slice(
4878            &fsqlite_wal::PARALLEL_WAL_DURABLE_CERTIFICATE_RECORD_VERSION.to_le_bytes(),
4879        );
4880        let fake_record_len = u32::try_from(fake_magic.len()).expect("fake record length fits u32");
4881        fake_magic[10..14].copy_from_slice(&fake_record_len.to_le_bytes());
4882        let fake_footer_offset =
4883            fake_magic.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
4884        fake_magic[fake_footer_offset..].copy_from_slice(&fake_record_len.to_le_bytes());
4885        replace_certificate_sidecar(&garbage_vfs, &cx, &fake_magic);
4886        assert_wal_corrupt(
4887            garbage_backend
4888                .latest_authorized_parallel_wal_commit_certificate(&cx)
4889                .wait(),
4890            "fake magic and length without a valid envelope",
4891        );
4892    }
4893
4894    #[test]
4895    fn durable_certificate_maximum_size_is_shared_by_writer_and_reader() {
4896        let cx = test_cx();
4897        let vfs = MemoryVfs::new();
4898        let mut backend = make_path_refreshing_backend(&vfs, &cx);
4899        let certificate = sample_certificate(1, 1, vec![1; usize::from(u16::MAX)]);
4900        let record = ParallelWalDurableCertificateRecord::new(
4901            backend.inner.inner().generation_identity(),
4902            1,
4903            1,
4904            certificate.clone(),
4905        )
4906        .expect("construct maximum-size record");
4907        assert_eq!(
4908            record.to_bytes().len(),
4909            PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
4910        );
4911        backend
4912            .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4913            .expect("writer accepts maximum-size record");
4914        assert!(
4915            backend
4916                .latest_authorized_parallel_wal_commit_certificate(&cx)
4917                .wait()
4918                .expect("reader accepts maximum-size record")
4919                .is_none(),
4920            "record remains unauthorized until its WAL commit marker exists"
4921        );
4922    }
4923
4924    #[test]
4925    fn durable_certificate_orphan_lookback_allows_exact_boundary_plus_torn_tail() {
4926        let cx = test_cx();
4927        let vfs = MemoryVfs::new();
4928        let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
4929        let mut sidecar = read_certificate_sidecar(&vfs, &cx);
4930        for orphan_index in 0..MAX_ORPHAN_CERTIFICATE_LOOKBACK {
4931            let epoch = u64::try_from(orphan_index).expect("orphan index fits u64") + 2;
4932            let orphan = sample_certificate(epoch, epoch, vec![1]);
4933            sidecar.extend_from_slice(
4934                &ParallelWalDurableCertificateRecord::new(
4935                    backend.inner.inner().generation_identity(),
4936                    2,
4937                    2,
4938                    orphan,
4939                )
4940                .expect("construct bounded orphan")
4941                .to_bytes(),
4942            );
4943        }
4944        sidecar.push(0xA5);
4945        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4946        assert_eq!(
4947            backend
4948                .latest_authorized_parallel_wal_commit_certificate(&cx)
4949                .wait()
4950                .expect("64 orphans plus one torn suffix remain within bound")
4951                .expect("authorized predecessor is found"),
4952            authorized
4953        );
4954
4955        sidecar.pop();
4956        let overflow_epoch =
4957            u64::try_from(MAX_ORPHAN_CERTIFICATE_LOOKBACK).expect("lookback fits u64") + 2;
4958        let overflow = sample_certificate(overflow_epoch, overflow_epoch, vec![1]);
4959        sidecar.extend_from_slice(
4960            &ParallelWalDurableCertificateRecord::new(
4961                backend.inner.inner().generation_identity(),
4962                2,
4963                2,
4964                overflow,
4965            )
4966            .expect("construct overflow orphan")
4967            .to_bytes(),
4968        );
4969        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4970        assert_wal_corrupt(
4971            backend
4972                .latest_authorized_parallel_wal_commit_certificate(&cx)
4973                .wait(),
4974            "65 unauthorized records",
4975        );
4976    }
4977
4978    #[test]
4979    fn certificate_and_handoff_fences_request_full_durability() {
4980        let cx = test_cx();
4981        let vfs = CheckpointHandoffFaultVfs::new();
4982        let (mut backend, certificate, _) = make_checkpoint_handoff_fault_backend(&vfs, &cx);
4983
4984        assert_eq!(
4985            vfs.take_sync_observations(),
4986            vec![CertificateSyncObservation::Durable(
4987                PathBuf::from(CERTIFICATE_PATH),
4988                SyncKind::FullDurable,
4989            )],
4990            "certificate append must use the strongest durability intent"
4991        );
4992
4993        assert_eq!(
4994            backend
4995                .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
4996                .wait()
4997                .expect("reconcile committed certificate"),
4998            ParallelWalCommitReconciliation::Authorized
4999        );
5000        assert_eq!(
5001            vfs.take_sync_observations(),
5002            vec![CertificateSyncObservation::Durable(
5003                PathBuf::from(CERTIFICATE_PATH),
5004                SyncKind::FullDurable,
5005            )],
5006            "certificate reconciliation must preserve full durability intent"
5007        );
5008
5009        let record = backend
5010            .latest_authorized_durable_certificate_record(&cx)
5011            .wait()
5012            .expect("read authorized certificate record")
5013            .expect("authorized certificate record must exist");
5014        backend
5015            .persist_checkpoint_certificate_handoff(&cx, &record)
5016            .wait()
5017            .expect("persist checkpoint certificate handoff");
5018        assert_eq!(
5019            vfs.take_sync_observations(),
5020            vec![CertificateSyncObservation::Durable(
5021                PathBuf::from(CHECKPOINT_HANDOFF_PATH),
5022                SyncKind::FullDurable,
5023            )],
5024            "checkpoint handoff must use the strongest durability intent"
5025        );
5026    }
5027
5028    #[test]
5029    fn checkpoint_handoff_write_failure_preserves_authoritative_wal_generation() {
5030        let cx = test_cx();
5031        let vfs = CheckpointHandoffFaultVfs::new();
5032        let (mut backend, certificate, committed_page) =
5033            make_checkpoint_handoff_fault_backend(&vfs, &cx);
5034        let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5035        vfs.fail_next_handoff_write();
5036
5037        let mut checkpoint_writer = MockCheckpointPageWriter;
5038        let error = backend
5039            .checkpoint(
5040                &cx,
5041                CheckpointMode::Truncate,
5042                &mut checkpoint_writer,
5043                0,
5044                None,
5045            )
5046            .expect_err("checkpoint must fail before reset when the handoff write fails");
5047        assert!(
5048            error
5049                .to_string()
5050                .contains("injected checkpoint handoff write failure"),
5051            "unexpected handoff write error: {error}"
5052        );
5053        assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5054    }
5055
5056    #[test]
5057    fn checkpoint_handoff_durable_sync_failure_preserves_authoritative_wal_generation() {
5058        let cx = test_cx();
5059        let vfs = CheckpointHandoffFaultVfs::new();
5060        let (mut backend, certificate, committed_page) =
5061            make_checkpoint_handoff_fault_backend(&vfs, &cx);
5062        let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5063        vfs.fail_next_handoff_sync();
5064
5065        let mut checkpoint_writer = MockCheckpointPageWriter;
5066        let error = backend
5067            .checkpoint(
5068                &cx,
5069                CheckpointMode::Truncate,
5070                &mut checkpoint_writer,
5071                0,
5072                None,
5073            )
5074            .expect_err("checkpoint must fail before reset when the handoff sync fails");
5075        assert!(
5076            error
5077                .to_string()
5078                .contains("injected checkpoint handoff durable-sync failure"),
5079            "unexpected handoff durable-sync error: {error}"
5080        );
5081        assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5082    }
5083
5084    #[test]
5085    fn dropping_pending_checkpoint_handoff_write_preserves_authoritative_wal_generation() {
5086        let cx = test_cx();
5087        let vfs = CheckpointHandoffFaultVfs::new();
5088        let (mut backend, certificate, committed_page) =
5089            make_checkpoint_handoff_fault_backend(&vfs, &cx);
5090        let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5091        vfs.pend_next_handoff_write();
5092
5093        let mut checkpoint_writer = MockCheckpointPageWriter;
5094        let reached_pending_handoff = {
5095            let mut checkpoint = backend.checkpoint(
5096                &cx,
5097                CheckpointMode::Truncate,
5098                &mut checkpoint_writer,
5099                0,
5100                None,
5101            );
5102            let mut task_cx = std::task::Context::from_waker(std::task::Waker::noop());
5103            matches!(
5104                std::future::Future::poll(checkpoint.as_mut(), &mut task_cx),
5105                std::task::Poll::Pending
5106            )
5107        };
5108        assert!(
5109            reached_pending_handoff,
5110            "checkpoint should remain pending inside the injected handoff write"
5111        );
5112        assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5113    }
5114
5115    #[test]
5116    fn two_backend_instances_continue_authorized_certificate_clocks() {
5117        let cx = test_cx();
5118        let vfs = MemoryVfs::new();
5119        let wal = WalFile::create(&cx, open_wal_file(&vfs, &cx), PAGE_SIZE, 0, test_salts())
5120            .expect("create shared WAL");
5121        let mut first_backend = PathRefreshingWalBackend::new(
5122            vfs.clone(),
5123            std::path::Path::new("test.db"),
5124            std::path::Path::new("test.db-wal"),
5125            PAGE_SIZE,
5126            wal,
5127            true,
5128            #[cfg(all(feature = "native", any(unix, windows)))]
5129            None,
5130        );
5131        let request =
5132            |batch_id, wal_frame_payload_digest| fsqlite_wal::ParallelWalDurabilityRequest {
5133                trace_id: batch_id,
5134                scenario_id: "two-instance-continuity".to_owned(),
5135                certificate_epoch: 0,
5136                durable_segment_epoch: 0,
5137                batch_size: 1,
5138                batch_ids: vec![batch_id],
5139                lane_record_counts: vec![1],
5140                db_size_pages: 1,
5141                page_set_size: 1,
5142                control_mode: fsqlite_wal::ParallelWalOperatingMode::Auto,
5143                fallback_reason: None,
5144                checkpoint_active: false,
5145                wal_frame_payload_digest,
5146            };
5147
5148        let first_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5149        let first_page = sample_page(0x51);
5150        let first_receipt = first_combiner
5151            .certify_and_publish(
5152                request(1, test_frame_payload_digest(1, &first_page, 1)),
5153                |certificate| {
5154                    first_backend
5155                        .persist_parallel_wal_commit_certificate(&cx, certificate, 1, 1, true)
5156                        .wait()
5157                        .and_then(|()| first_backend.append_frame(&cx, 1, &first_page, 1).wait())
5158                        .and_then(|()| first_backend.sync(&cx))
5159                        .map_err(|error| error.to_string())
5160                },
5161            )
5162            .expect("first backend publishes certificate");
5163
5164        let second_wal =
5165            WalFile::open(&cx, open_wal_file(&vfs, &cx)).expect("second backend opens shared WAL");
5166        let mut second_backend = PathRefreshingWalBackend::new(
5167            vfs.clone(),
5168            std::path::Path::new("test.db"),
5169            std::path::Path::new("test.db-wal"),
5170            PAGE_SIZE,
5171            second_wal,
5172            true,
5173            #[cfg(all(feature = "native", any(unix, windows)))]
5174            None,
5175        );
5176
5177        // Simulate a crash after certificate durability but before its WAL
5178        // commit marker. Bounded tail lookup must step over this well-formed
5179        // orphan and recover the preceding authorized seed.
5180        let orphan_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5181        orphan_combiner
5182            .reconcile_authorized_seed(&first_receipt.certificate)
5183            .expect("seed orphan-producing process");
5184        let orphan_receipt = orphan_combiner
5185            .certify_and_publish(
5186                request(99, test_frame_payload_digest(1, &sample_page(0x52), 1)),
5187                |_| Ok(()),
5188            )
5189            .expect("construct deterministic orphan certificate");
5190        second_backend
5191            .persist_parallel_wal_commit_certificate(&cx, &orphan_receipt.certificate, 2, 2, true)
5192            .expect("persist well-formed orphan certificate tail");
5193        let authorized_seed = second_backend
5194            .latest_authorized_parallel_wal_commit_certificate(&cx)
5195            .expect("second backend performs bounded orphan lookback")
5196            .expect("preceding first certificate remains authorized");
5197        assert_eq!(authorized_seed, first_receipt.certificate);
5198
5199        let second_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5200        second_combiner
5201            .reconcile_authorized_seed(&authorized_seed)
5202            .expect("seed second process-local combiner");
5203        let second_page = sample_page(0x52);
5204        let second_receipt = second_combiner
5205            .certify_and_publish(
5206                request(2, test_frame_payload_digest(1, &second_page, 1)),
5207                |certificate| {
5208                    second_backend
5209                        .persist_parallel_wal_commit_certificate(&cx, certificate, 2, 2, true)
5210                        .wait()
5211                        .and_then(|()| second_backend.append_frame(&cx, 1, &second_page, 1).wait())
5212                        .and_then(|()| second_backend.sync(&cx))
5213                        .map_err(|error| error.to_string())
5214                },
5215            )
5216            .expect("second backend publishes certificate");
5217
5218        assert_eq!(
5219            second_receipt.certificate.commit_seq_lo.get(),
5220            first_receipt.certificate.commit_seq_hi.get() + 1
5221        );
5222        assert_eq!(
5223            second_receipt.certificate.certificate_epoch,
5224            first_receipt.certificate.certificate_epoch + 1
5225        );
5226        assert_eq!(
5227            second_receipt.certificate, orphan_receipt.certificate,
5228            "continuation may reuse an orphan identity but must not overlap any authorized certificate"
5229        );
5230        let latest = second_backend
5231            .latest_authorized_parallel_wal_commit_certificate(&cx)
5232            .expect("read second bounded authorized tail")
5233            .expect("second certificate is authorized");
5234        assert_eq!(latest, second_receipt.certificate);
5235
5236        let generation_before_checkpoint = second_backend.inner.inner().generation_identity();
5237        let mut checkpoint_writer = MockCheckpointPageWriter;
5238        let checkpoint = second_backend
5239            .checkpoint(
5240                &cx,
5241                CheckpointMode::Truncate,
5242                &mut checkpoint_writer,
5243                0,
5244                None,
5245            )
5246            .expect("truncate checkpoint records certificate clock handoff");
5247        assert!(checkpoint.wal_was_reset);
5248        assert_ne!(
5249            second_backend.inner.inner().generation_identity(),
5250            generation_before_checkpoint
5251        );
5252        let checkpoint_seed = second_backend
5253            .latest_authorized_parallel_wal_commit_certificate(&cx)
5254            .expect("read checkpoint certificate clock handoff")
5255            .expect("reset generation retains the last consumed certificate clock");
5256        assert_eq!(checkpoint_seed, second_receipt.certificate);
5257        second_backend
5258            .begin_transaction(&cx)
5259            .expect("pin reset-generation reader snapshot");
5260        let reset_pinned = second_backend
5261            .pinned_read_snapshot()
5262            .expect("reset-generation reader snapshot");
5263        assert_eq!(
5264            reset_pinned.generation,
5265            second_backend.inner.inner().generation_identity(),
5266            "reader snapshot must bind the reset WAL generation"
5267        );
5268        assert_eq!(
5269            reset_pinned.last_commit_frame, None,
5270            "truncate checkpoint leaves no current-generation commit marker"
5271        );
5272        assert_eq!(
5273            second_backend
5274                .pinned_logical_read_snapshot(&cx)
5275                .expect("inspect reset-generation reader horizon"),
5276            None,
5277            "an earlier-generation checkpoint handoff is a clock seed, never reader visibility"
5278        );
5279
5280        let post_checkpoint_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5281        post_checkpoint_combiner
5282            .reconcile_authorized_seed(&checkpoint_seed)
5283            .expect("seed fresh post-checkpoint combiner");
5284        let post_checkpoint_page = sample_page(0x53);
5285        let post_checkpoint_receipt = post_checkpoint_combiner
5286            .certify_and_publish(
5287                request(3, test_frame_payload_digest(1, &post_checkpoint_page, 1)),
5288                |certificate| {
5289                    second_backend
5290                        .persist_parallel_wal_commit_certificate(&cx, certificate, 1, 1, true)
5291                        .wait()
5292                        .and_then(|()| {
5293                            second_backend
5294                                .append_frame(&cx, 1, &post_checkpoint_page, 1)
5295                                .wait()
5296                        })
5297                        .and_then(|()| second_backend.sync(&cx))
5298                        .map_err(|error| error.to_string())
5299                },
5300            )
5301            .expect("publish first certificate in reset WAL generation");
5302        assert_eq!(
5303            post_checkpoint_receipt.certificate.commit_seq_lo.get(),
5304            second_receipt.certificate.commit_seq_hi.get() + 1
5305        );
5306        assert_eq!(
5307            post_checkpoint_receipt.certificate.certificate_epoch,
5308            second_receipt.certificate.certificate_epoch + 1
5309        );
5310        assert_eq!(
5311            second_backend
5312                .latest_authorized_parallel_wal_commit_certificate(&cx)
5313                .expect("read post-checkpoint current-generation certificate")
5314                .expect("post-checkpoint certificate is authorized"),
5315            post_checkpoint_receipt.certificate
5316        );
5317        second_backend
5318            .begin_transaction(&cx)
5319            .expect("pin post-checkpoint reader snapshot");
5320        let pinned = second_backend
5321            .pinned_read_snapshot()
5322            .expect("post-checkpoint reader snapshot");
5323        let logical = second_backend
5324            .pinned_logical_read_snapshot(&cx)
5325            .expect("inspect post-checkpoint reader horizon")
5326            .expect("current-generation certificate exposes a reader horizon");
5327        assert_eq!(logical.generation, pinned.generation);
5328        assert_eq!(logical.last_commit_frame, pinned.last_commit_frame);
5329        assert_eq!(
5330            logical.visible_commit_seq,
5331            post_checkpoint_receipt.certificate.commit_seq_hi
5332        );
5333    }
5334
5335    #[test]
5336    fn pinned_logical_reader_horizon_counts_physical_tail_after_current_certificate() {
5337        let cx = test_cx();
5338        let vfs = MemoryVfs::new();
5339        let (mut backend, certificate) = make_authorized_certificate_backend(&vfs, &cx);
5340
5341        backend
5342            .begin_transaction(&cx)
5343            .expect("pin certificate reader snapshot");
5344        let initial_pinned = backend
5345            .pinned_read_snapshot()
5346            .expect("initial reader snapshot");
5347        let initial_logical = backend
5348            .pinned_logical_read_snapshot(&cx)
5349            .expect("inspect certificate reader horizon")
5350            .expect("current certificate exposes reader horizon");
5351        assert_eq!(initial_logical.generation, initial_pinned.generation);
5352        assert_eq!(
5353            initial_logical.last_commit_frame,
5354            initial_pinned.last_commit_frame
5355        );
5356        assert_eq!(
5357            initial_logical.visible_commit_seq, certificate.commit_seq_hi,
5358            "certificate horizon is exact when no later physical commit exists"
5359        );
5360
5361        let tail_page = sample_page(0x45);
5362        backend
5363            .append_frame(&cx, 2, &tail_page, 2)
5364            .expect("append later ordinary commit marker");
5365        backend
5366            .sync(&cx)
5367            .expect("sync later ordinary commit marker");
5368        backend
5369            .begin_transaction(&cx)
5370            .expect("repin reader after ordinary tail commit");
5371        let pinned = backend
5372            .pinned_read_snapshot()
5373            .expect("reader snapshot includes ordinary tail commit");
5374        let logical = backend
5375            .pinned_logical_read_snapshot(&cx)
5376            .expect("inspect reader horizon with ordinary tail")
5377            .expect("current certificate remains reader-authoritative");
5378        assert_eq!(logical.generation, pinned.generation);
5379        assert_eq!(logical.last_commit_frame, pinned.last_commit_frame);
5380        assert_eq!(
5381            logical.visible_commit_seq.get(),
5382            certificate.commit_seq_hi.get() + 1
5383        );
5384    }
5385
5386    fn open_wal_file(vfs: &MemoryVfs, cx: &Cx) -> <MemoryVfs as Vfs>::File {
5387        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5388        let (file, _) = vfs
5389            .open(cx, Some(std::path::Path::new("test.db-wal")), flags)
5390            .expect("open WAL file");
5391        file
5392    }
5393
5394    fn make_adapter(vfs: &MemoryVfs, cx: &Cx) -> WalBackendAdapter<<MemoryVfs as Vfs>::File> {
5395        let file = open_wal_file(vfs, cx);
5396        let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5397        WalBackendAdapter::new(wal)
5398    }
5399
5400    /// Adapter backed by the fault VFS so WAL `sync` failures can be injected.
5401    fn make_fault_adapter(
5402        vfs: &CheckpointHandoffFaultVfs,
5403        cx: &Cx,
5404    ) -> WalBackendAdapter<<CheckpointHandoffFaultVfs as Vfs>::File> {
5405        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5406        let (file, _) = vfs
5407            .open(cx, Some(std::path::Path::new("test.db-wal")), flags)
5408            .expect("open fault WAL file");
5409        let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create fault WAL");
5410        WalBackendAdapter::new(wal)
5411    }
5412
5413    // -- WalBackendAdapter tests --
5414
5415    #[test]
5416    fn test_adapter_append_and_frame_count() {
5417        let cx = test_cx();
5418        let vfs = MemoryVfs::new();
5419        let mut adapter = make_adapter(&vfs, &cx);
5420
5421        assert_eq!(adapter.frame_count(), 0);
5422
5423        let page = sample_page(0x42);
5424        adapter
5425            .append_frame(&cx, 1, &page, 0)
5426            .expect("append frame");
5427        assert_eq!(adapter.frame_count(), 1);
5428
5429        adapter
5430            .append_frame(&cx, 2, &sample_page(0x43), 2)
5431            .expect("append commit frame");
5432        assert_eq!(adapter.frame_count(), 2);
5433    }
5434
5435    #[test]
5436    fn test_adapter_read_page_found() {
5437        let cx = test_cx();
5438        let vfs = MemoryVfs::new();
5439        let mut adapter = make_adapter(&vfs, &cx);
5440
5441        let page1 = sample_page(0x10);
5442        let page2 = sample_page(0x20);
5443        adapter.append_frame(&cx, 1, &page1, 0).expect("append");
5444        adapter
5445            .append_frame(&cx, 2, &page2, 2)
5446            .expect("append commit");
5447
5448        let result = adapter.read_page(&cx, 1).expect("read page 1");
5449        assert_eq!(result, Some(page1));
5450
5451        let result = adapter.read_page(&cx, 2).expect("read page 2");
5452        assert_eq!(result, Some(page2));
5453    }
5454
5455    #[test]
5456    fn test_adapter_read_page_not_found() {
5457        let cx = test_cx();
5458        let vfs = MemoryVfs::new();
5459        let mut adapter = make_adapter(&vfs, &cx);
5460
5461        adapter
5462            .append_frame(&cx, 1, &sample_page(0x10), 1)
5463            .expect("append");
5464
5465        let result = adapter.read_page(&cx, 99).expect("read missing page");
5466        assert_eq!(result, None);
5467    }
5468
5469    #[test]
5470    fn test_adapter_read_page_returns_latest_version() {
5471        let cx = test_cx();
5472        let vfs = MemoryVfs::new();
5473        let mut adapter = make_adapter(&vfs, &cx);
5474
5475        let old_data = sample_page(0xAA);
5476        let new_data = sample_page(0xBB);
5477
5478        // Write page 5 twice -- the adapter should return the latest.
5479        adapter
5480            .append_frame(&cx, 5, &old_data, 0)
5481            .expect("append old");
5482        adapter
5483            .append_frame(&cx, 5, &new_data, 1)
5484            .expect("append new (commit)");
5485
5486        let result = adapter.read_page(&cx, 5).expect("read page 5");
5487        assert_eq!(
5488            result,
5489            Some(new_data),
5490            "adapter should return the latest WAL version"
5491        );
5492    }
5493
5494    #[test]
5495    fn test_adapter_refreshes_cross_handle_visibility_and_append_position() {
5496        let cx = test_cx();
5497        let vfs = MemoryVfs::new();
5498
5499        let file1 = open_wal_file(&vfs, &cx);
5500        let wal1 = WalFile::create(&cx, file1, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5501        let mut adapter1 = WalBackendAdapter::new(wal1);
5502
5503        let file2 = open_wal_file(&vfs, &cx);
5504        let wal2 = WalFile::open(&cx, file2).expect("open WAL");
5505        let mut adapter2 = WalBackendAdapter::new(wal2);
5506
5507        let page1 = sample_page(0x11);
5508        adapter1
5509            .append_frame(&cx, 1, &page1, 1)
5510            .expect("adapter1 append commit");
5511        adapter1.sync(&cx).expect("adapter1 sync");
5512        adapter2
5513            .begin_transaction(&cx)
5514            .expect("adapter2 begin transaction");
5515        assert_eq!(
5516            adapter2.read_page(&cx, 1).expect("adapter2 read page1"),
5517            Some(page1.clone()),
5518            "adapter2 should observe adapter1 commit at transaction begin"
5519        );
5520
5521        let page2 = sample_page(0x22);
5522        adapter2
5523            .append_frame(&cx, 2, &page2, 2)
5524            .expect("adapter2 append commit");
5525        adapter2.sync(&cx).expect("adapter2 sync");
5526        adapter1
5527            .begin_transaction(&cx)
5528            .expect("adapter1 begin transaction");
5529        assert_eq!(
5530            adapter1.read_page(&cx, 2).expect("adapter1 read page2"),
5531            Some(page2.clone()),
5532            "adapter1 should observe adapter2 commit at transaction begin"
5533        );
5534
5535        // Ensure the second writer appended to frame 1 (not frame 0 overwrite).
5536        assert_eq!(
5537            adapter1.frame_count(),
5538            2,
5539            "shared WAL should contain both commit frames"
5540        );
5541        assert_eq!(
5542            adapter2.frame_count(),
5543            2,
5544            "shared WAL should contain both commit frames"
5545        );
5546    }
5547
5548    #[test]
5549    fn test_path_refresh_rejects_replacement_wal_page_size_mismatch() {
5550        let cx = test_cx();
5551        let vfs = MemoryVfs::new();
5552        let wal_path = std::path::Path::new("test.db-wal");
5553
5554        let file = open_wal_file(&vfs, &cx);
5555        let wal = WalFile::create(&cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5556        let mut backend = PathRefreshingWalBackend::new(
5557            vfs.clone(),
5558            std::path::Path::new("test.db"),
5559            wal_path,
5560            PAGE_SIZE,
5561            wal,
5562            true,
5563            #[cfg(all(feature = "native", any(unix, windows)))]
5564            None,
5565        );
5566
5567        backend
5568            .append_frame(&cx, 1, &sample_page(0x31), 1)
5569            .expect("append through live backend");
5570        backend.sync(&cx).expect("sync live backend");
5571
5572        vfs.delete(&cx, wal_path, false)
5573            .expect("remove path-visible WAL");
5574        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5575        let (replacement_file, _) = vfs
5576            .open(&cx, Some(wal_path), flags)
5577            .expect("open replacement WAL path");
5578        let replacement_page_size = PAGE_SIZE
5579            .checked_mul(2)
5580            .expect("test replacement page size fits u32");
5581        let replacement_wal = WalFile::create(
5582            &cx,
5583            replacement_file,
5584            replacement_page_size,
5585            0,
5586            test_salts(),
5587        )
5588        .expect("create mismatched replacement WAL");
5589        replacement_wal.close(&cx).expect("close replacement WAL");
5590
5591        let err = backend
5592            .begin_transaction(&cx)
5593            .expect_err("path refresh should reject mismatched WAL page size");
5594        assert!(
5595            matches!(
5596                err,
5597                FrankenError::WalCorrupt { ref detail }
5598                    if detail.contains("does not match database page size")
5599                        && detail.contains("during path refresh")
5600            ),
5601            "unexpected error: {err:?}"
5602        );
5603    }
5604
5605    #[test]
5606    fn test_generation_change_allows_identical_full_page_baseline() {
5607        let cx = test_cx();
5608        let vfs = MemoryVfs::new();
5609        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5610        let baseline = TransactionConflictPageBaseline {
5611            page_number: 2,
5612            page_hash: *blake3::hash(&page_two).as_bytes(),
5613        };
5614
5615        let conflicts = backend
5616            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5617            .expect("validate checkpoint-only generation transition");
5618        assert!(
5619            conflicts.is_empty(),
5620            "byte-identical checkpoint-only reset must not create a false conflict"
5621        );
5622    }
5623
5624    #[test]
5625    fn test_generation_change_rejects_changed_candidate_page() {
5626        let cx = test_cx();
5627        let vfs = MemoryVfs::new();
5628        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5629        let changed_page_two = sample_page(0x33);
5630        write_main_db_pages(
5631            &vfs,
5632            &cx,
5633            &[
5634                sqlite_page_one(u16::try_from(PAGE_SIZE).expect("page size fits u16")),
5635                changed_page_two,
5636            ],
5637        );
5638        let baseline = TransactionConflictPageBaseline {
5639            page_number: 2,
5640            page_hash: *blake3::hash(&page_two).as_bytes(),
5641        };
5642
5643        let conflicts = backend
5644            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5645            .expect("validate changed page across generation transition");
5646        assert_eq!(conflicts, vec![2]);
5647    }
5648
5649    #[test]
5650    fn test_generation_change_rejects_changed_candidate_from_replacement_wal() {
5651        let cx = test_cx();
5652        let vfs = MemoryVfs::new();
5653        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5654        append_replacement_wal_page(&vfs, &cx, 2, &sample_page(0x44), 2);
5655        let baseline = TransactionConflictPageBaseline {
5656            page_number: 2,
5657            page_hash: *blake3::hash(&page_two).as_bytes(),
5658        };
5659
5660        let conflicts = backend
5661            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5662            .expect("replacement WAL page must take precedence over identical main page");
5663        assert_eq!(conflicts, vec![2]);
5664    }
5665
5666    #[test]
5667    fn test_generation_change_rejects_missing_baseline() {
5668        let cx = test_cx();
5669        let vfs = MemoryVfs::new();
5670        let (mut backend, snapshot, _) = make_generation_transition_backend(&vfs, &cx);
5671
5672        let conflicts = backend
5673            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[])
5674            .expect("missing baseline must fail closed");
5675        assert_eq!(conflicts, vec![2]);
5676    }
5677
5678    #[test]
5679    fn test_generation_change_rejects_conflicting_duplicate_baselines() {
5680        let cx = test_cx();
5681        let vfs = MemoryVfs::new();
5682        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5683        let baselines = [
5684            TransactionConflictPageBaseline {
5685                page_number: 2,
5686                page_hash: *blake3::hash(&page_two).as_bytes(),
5687            },
5688            TransactionConflictPageBaseline {
5689                page_number: 2,
5690                page_hash: *blake3::hash(&sample_page(0x55)).as_bytes(),
5691            },
5692        ];
5693
5694        let conflicts = backend
5695            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &baselines)
5696            .expect("conflicting duplicate baselines must fail closed");
5697        assert_eq!(conflicts, vec![2]);
5698    }
5699
5700    #[test]
5701    fn test_generation_change_rejects_short_candidate_page() {
5702        let cx = test_cx();
5703        let vfs = MemoryVfs::new();
5704        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5705        write_main_db_pages(
5706            &vfs,
5707            &cx,
5708            &[sqlite_page_one(
5709                u16::try_from(PAGE_SIZE).expect("page size fits u16"),
5710            )],
5711        );
5712        let baseline = TransactionConflictPageBaseline {
5713            page_number: 2,
5714            page_hash: *blake3::hash(&page_two).as_bytes(),
5715        };
5716
5717        let conflicts = backend
5718            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5719            .expect("short page must fail closed");
5720        assert_eq!(conflicts, vec![2]);
5721    }
5722
5723    #[test]
5724    fn test_generation_change_rejects_database_page_size_change() {
5725        let cx = test_cx();
5726        let vfs = MemoryVfs::new();
5727        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5728        write_main_db_pages(&vfs, &cx, &[sqlite_page_one(8192), page_two.clone()]);
5729        let baseline = TransactionConflictPageBaseline {
5730            page_number: 2,
5731            page_hash: *blake3::hash(&page_two).as_bytes(),
5732        };
5733
5734        let conflicts = backend
5735            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5736            .expect("page-size change must fail closed");
5737        assert_eq!(conflicts, vec![2]);
5738    }
5739
5740    #[test]
5741    fn test_generation_change_decodes_64k_database_header_sentinel() {
5742        assert_eq!(
5743            sqlite_database_header_page_size(&sqlite_page_one(1)),
5744            Some(65_536)
5745        );
5746    }
5747
5748    #[test]
5749    fn test_adapter_batch_append_checksum_chain_matches_single_append() {
5750        let cx = test_cx();
5751        let vfs_single = MemoryVfs::new();
5752        let vfs_batch = MemoryVfs::new();
5753
5754        let mut adapter_single = make_adapter(&vfs_single, &cx);
5755        let mut adapter_batch = make_adapter(&vfs_batch, &cx);
5756
5757        let pages: Vec<Vec<u8>> = (0..4u8).map(sample_page).collect();
5758        let commit_sizes = [0_u32, 0, 0, 4];
5759
5760        for (index, page) in pages.iter().enumerate() {
5761            adapter_single
5762                .append_frame(
5763                    &cx,
5764                    u32::try_from(index + 1).expect("page number fits u32"),
5765                    page,
5766                    commit_sizes[index],
5767                )
5768                .expect("single append");
5769        }
5770
5771        let batch_frames: Vec<_> = pages
5772            .iter()
5773            .enumerate()
5774            .map(|(index, page)| WalFrameRef {
5775                page_number: u32::try_from(index + 1).expect("page number fits u32"),
5776                page_data: page,
5777                db_size_if_commit: commit_sizes[index],
5778            })
5779            .collect();
5780        adapter_batch
5781            .append_frames(&cx, &batch_frames)
5782            .expect("batch append");
5783
5784        assert_eq!(
5785            adapter_single.frame_count(),
5786            adapter_batch.frame_count(),
5787            "batch adapter append must preserve frame count"
5788        );
5789        assert_eq!(
5790            adapter_single.wal.running_checksum(),
5791            adapter_batch.wal.running_checksum(),
5792            "batch adapter append must preserve checksum chain"
5793        );
5794
5795        for frame_index in 0..pages.len() {
5796            let (single_header, single_data) = adapter_single
5797                .wal
5798                .read_frame(&cx, frame_index)
5799                .expect("read single frame");
5800            let (batch_header, batch_data) = adapter_batch
5801                .wal
5802                .read_frame(&cx, frame_index)
5803                .expect("read batch frame");
5804            assert_eq!(
5805                single_header, batch_header,
5806                "frame header {frame_index} must match"
5807            );
5808            assert_eq!(
5809                single_data, batch_data,
5810                "frame payload {frame_index} must match"
5811            );
5812        }
5813    }
5814
5815    #[test]
5816    fn test_adapter_prepared_batch_append_checksum_chain_matches_single_append() {
5817        let cx = test_cx();
5818        let vfs_single = MemoryVfs::new();
5819        let vfs_prepared = MemoryVfs::new();
5820
5821        let mut adapter_single = make_adapter(&vfs_single, &cx);
5822        let mut adapter_prepared = make_adapter(&vfs_prepared, &cx);
5823
5824        let pages: Vec<Vec<u8>> = (0..4u8).map(sample_page).collect();
5825        let commit_sizes = [0_u32, 0, 0, 4];
5826
5827        for (index, page) in pages.iter().enumerate() {
5828            adapter_single
5829                .append_frame(
5830                    &cx,
5831                    u32::try_from(index + 1).expect("page number fits u32"),
5832                    page,
5833                    commit_sizes[index],
5834                )
5835                .expect("single append");
5836        }
5837
5838        let batch_frames: Vec<_> = pages
5839            .iter()
5840            .enumerate()
5841            .map(|(index, page)| WalFrameRef {
5842                page_number: u32::try_from(index + 1).expect("page number fits u32"),
5843                page_data: page,
5844                db_size_if_commit: commit_sizes[index],
5845            })
5846            .collect();
5847        let mut prepared = adapter_prepared
5848            .prepare_append_frames(&batch_frames)
5849            .expect("prepare append")
5850            .expect("prepared batch");
5851        adapter_prepared
5852            .append_prepared_frames(&cx, &mut prepared)
5853            .expect("append prepared");
5854
5855        assert_eq!(
5856            adapter_single.frame_count(),
5857            adapter_prepared.frame_count(),
5858            "prepared adapter append must preserve frame count"
5859        );
5860        assert_eq!(
5861            adapter_single.wal.running_checksum(),
5862            adapter_prepared.wal.running_checksum(),
5863            "prepared adapter append must preserve checksum chain"
5864        );
5865
5866        for frame_index in 0..pages.len() {
5867            let (single_header, single_data) = adapter_single
5868                .wal
5869                .read_frame(&cx, frame_index)
5870                .expect("read single frame");
5871            let (prepared_header, prepared_data) = adapter_prepared
5872                .wal
5873                .read_frame(&cx, frame_index)
5874                .expect("read prepared frame");
5875            assert_eq!(
5876                single_header, prepared_header,
5877                "frame header {frame_index} must match"
5878            );
5879            assert_eq!(
5880                single_data, prepared_data,
5881                "frame payload {frame_index} must match"
5882            );
5883        }
5884    }
5885
5886    #[test]
5887    fn test_adapter_pre_finalize_reused_when_append_window_is_stable() {
5888        let cx = test_cx();
5889        let vfs_single = MemoryVfs::new();
5890        let vfs_prepared = MemoryVfs::new();
5891
5892        let mut adapter_single = make_adapter(&vfs_single, &cx);
5893        let mut adapter_prepared = make_adapter(&vfs_prepared, &cx);
5894
5895        let pages: Vec<Vec<u8>> = (0..3u8).map(sample_page).collect();
5896        let commit_sizes = [0_u32, 0, 3];
5897
5898        for (index, page) in pages.iter().enumerate() {
5899            adapter_single
5900                .append_frame(
5901                    &cx,
5902                    u32::try_from(index + 1).expect("page number fits u32"),
5903                    page,
5904                    commit_sizes[index],
5905                )
5906                .expect("single append");
5907        }
5908
5909        let batch_frames: Vec<_> = pages
5910            .iter()
5911            .enumerate()
5912            .map(|(index, page)| WalFrameRef {
5913                page_number: u32::try_from(index + 1).expect("page number fits u32"),
5914                page_data: page,
5915                db_size_if_commit: commit_sizes[index],
5916            })
5917            .collect();
5918        let mut prepared = adapter_prepared
5919            .prepare_append_frames(&batch_frames)
5920            .expect("prepare append")
5921            .expect("prepared batch");
5922        adapter_prepared
5923            .finalize_prepared_frames(&cx, &mut prepared)
5924            .expect("pre-finalize prepared batch");
5925        let finalized_for = prepared.finalized_for.expect("finalization state");
5926        let finalized_running_checksum = prepared
5927            .finalized_running_checksum
5928            .expect("finalized checksum");
5929
5930        adapter_prepared
5931            .append_prepared_frames(&cx, &mut prepared)
5932            .expect("append prepared");
5933
5934        assert_eq!(
5935            prepared.finalized_for,
5936            Some(finalized_for),
5937            "stable append window should reuse the pre-lock finalization state"
5938        );
5939        assert_eq!(
5940            prepared.finalized_running_checksum,
5941            Some(finalized_running_checksum),
5942            "stable append window should reuse the pre-lock finalized checksum"
5943        );
5944        assert_eq!(
5945            adapter_single.wal.running_checksum(),
5946            adapter_prepared.wal.running_checksum(),
5947            "stable reuse path must preserve checksum chain"
5948        );
5949    }
5950
5951    #[test]
5952    fn test_adapter_pre_finalize_reseeds_after_intervening_external_append() {
5953        let cx = test_cx();
5954        let baseline_vfs = MemoryVfs::new();
5955        let shared_vfs = MemoryVfs::new();
5956
5957        let mut baseline = make_adapter(&baseline_vfs, &cx);
5958        let mut prepared_writer = make_adapter(&shared_vfs, &cx);
5959        let intruder_file = open_wal_file(&shared_vfs, &cx);
5960        let intruder_wal = WalFile::open(&cx, intruder_file).expect("open shared WAL");
5961        let mut intruder = WalBackendAdapter::new(intruder_wal);
5962
5963        let pages: Vec<Vec<u8>> = (0..3u8).map(sample_page).collect();
5964        let commit_sizes = [0_u32, 0, 3];
5965        let intruder_page = sample_page(0xEE);
5966
5967        baseline
5968            .append_frame(&cx, 99, &intruder_page, 1)
5969            .expect("baseline intruder append");
5970        for (index, page) in pages.iter().enumerate() {
5971            baseline
5972                .append_frame(
5973                    &cx,
5974                    u32::try_from(index + 1).expect("page number fits u32"),
5975                    page,
5976                    commit_sizes[index],
5977                )
5978                .expect("baseline append");
5979        }
5980
5981        let batch_frames: Vec<_> = pages
5982            .iter()
5983            .enumerate()
5984            .map(|(index, page)| WalFrameRef {
5985                page_number: u32::try_from(index + 1).expect("page number fits u32"),
5986                page_data: page,
5987                db_size_if_commit: commit_sizes[index],
5988            })
5989            .collect();
5990        let mut prepared = prepared_writer
5991            .prepare_append_frames(&batch_frames)
5992            .expect("prepare append")
5993            .expect("prepared batch");
5994        prepared_writer
5995            .finalize_prepared_frames(&cx, &mut prepared)
5996            .expect("pre-finalize prepared batch");
5997        let stale_finalization_state = prepared.finalized_for;
5998
5999        intruder
6000            .append_frame(&cx, 99, &intruder_page, 1)
6001            .expect("intruder append");
6002        intruder.sync(&cx).expect("intruder sync");
6003
6004        prepared_writer
6005            .append_prepared_frames(&cx, &mut prepared)
6006            .expect("append prepared after external growth");
6007
6008        assert_ne!(
6009            prepared.finalized_for, stale_finalization_state,
6010            "intervening external growth should force prepared batch reseeding"
6011        );
6012        assert_eq!(
6013            baseline.wal.running_checksum(),
6014            prepared_writer.wal.running_checksum(),
6015            "reseeding path must preserve checksum chain"
6016        );
6017        assert_eq!(
6018            baseline.frame_count(),
6019            prepared_writer.frame_count(),
6020            "reseeding path must preserve frame count"
6021        );
6022    }
6023
6024    #[test]
6025    fn test_adapter_pins_read_snapshot_until_next_begin() {
6026        init_wal_publication_test_tracing();
6027        let cx = test_cx();
6028        let vfs = MemoryVfs::new();
6029
6030        let file_writer = open_wal_file(&vfs, &cx);
6031        let wal_writer =
6032            WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6033        let mut writer = WalBackendAdapter::new(wal_writer);
6034
6035        let file_reader = open_wal_file(&vfs, &cx);
6036        let wal_reader = WalFile::open(&cx, file_reader).expect("open WAL");
6037        let mut reader = WalBackendAdapter::new(wal_reader);
6038
6039        let v1 = sample_page(0x41);
6040        writer.append_frame(&cx, 3, &v1, 3).expect("append v1");
6041        writer.sync(&cx).expect("sync v1");
6042
6043        reader
6044            .begin_transaction(&cx)
6045            .expect("begin reader snapshot 1");
6046        let pinned_v1 = reader
6047            .pinned_read_snapshot()
6048            .expect("reader pins publication snapshot");
6049        assert_eq!(pinned_v1.last_commit_frame, Some(0));
6050        assert_eq!(pinned_v1.commit_count, 1);
6051        assert_eq!(pinned_v1.latest_frame_entries, 1);
6052        assert!(pinned_v1.lookup_contract_is_authoritative());
6053        assert_eq!(
6054            reader.read_page(&cx, 3).expect("reader sees v1"),
6055            Some(v1.clone())
6056        );
6057
6058        let v2 = sample_page(0x42);
6059        writer.append_frame(&cx, 3, &v2, 3).expect("append v2");
6060        writer.sync(&cx).expect("sync v2");
6061
6062        // Same transaction snapshot must stay stable (no mid-transaction drift).
6063        assert_eq!(
6064            reader
6065                .read_page(&cx, 3)
6066                .expect("reader remains on pinned snapshot"),
6067            Some(v1.clone())
6068        );
6069        assert_eq!(
6070            reader
6071                .pinned_read_snapshot()
6072                .expect("reader keeps the same pinned snapshot"),
6073            pinned_v1,
6074            "pinned publication metadata must stay stable until the next begin"
6075        );
6076
6077        // A new transaction snapshot should pick up the latest commit.
6078        reader
6079            .begin_transaction(&cx)
6080            .expect("begin reader snapshot 2");
6081        let pinned_v2 = reader
6082            .pinned_read_snapshot()
6083            .expect("reader repins publication snapshot");
6084        assert!(pinned_v2.publication_seq > pinned_v1.publication_seq);
6085        assert_eq!(pinned_v2.commit_count, 2);
6086        assert_eq!(pinned_v2.latest_frame_entries, 1);
6087        assert_eq!(reader.read_page(&cx, 3).expect("reader sees v2"), Some(v2));
6088    }
6089
6090    #[test]
6091    fn test_adapter_read_page_hides_uncommitted_frames() {
6092        let cx = test_cx();
6093        let vfs = MemoryVfs::new();
6094        let mut adapter = make_adapter(&vfs, &cx);
6095
6096        let committed = sample_page(0x31);
6097        let uncommitted = sample_page(0x32);
6098
6099        adapter
6100            .append_frame(&cx, 7, &committed, 7)
6101            .expect("append committed frame");
6102        adapter
6103            .append_frame(&cx, 7, &uncommitted, 0)
6104            .expect("append uncommitted frame");
6105
6106        let result = adapter.read_page(&cx, 7).expect("read committed page");
6107        assert_eq!(
6108            result,
6109            Some(committed),
6110            "reader must ignore uncommitted tail frames"
6111        );
6112    }
6113
6114    #[test]
6115    fn test_adapter_read_page_none_when_wal_has_no_commit_frame() {
6116        let cx = test_cx();
6117        let vfs = MemoryVfs::new();
6118        let mut adapter = make_adapter(&vfs, &cx);
6119
6120        adapter
6121            .append_frame(&cx, 3, &sample_page(0x44), 0)
6122            .expect("append uncommitted frame");
6123
6124        let result = adapter.read_page(&cx, 3).expect("read page");
6125        assert_eq!(result, None, "uncommitted WAL frames must stay invisible");
6126    }
6127
6128    #[test]
6129    fn test_adapter_read_page_empty_wal() {
6130        let cx = test_cx();
6131        let vfs = MemoryVfs::new();
6132        let mut adapter = make_adapter(&vfs, &cx);
6133
6134        let result = adapter.read_page(&cx, 1).expect("read from empty WAL");
6135        assert_eq!(result, None);
6136    }
6137
6138    #[test]
6139    fn test_adapter_sync() {
6140        let cx = test_cx();
6141        let vfs = MemoryVfs::new();
6142        let mut adapter = make_adapter(&vfs, &cx);
6143
6144        adapter
6145            .append_frame(&cx, 1, &sample_page(0), 1)
6146            .expect("append");
6147        adapter.sync(&cx).expect("sync should not fail");
6148    }
6149
6150    #[test]
6151    fn test_adapter_into_inner_fails_closed_until_sync() {
6152        let cx = test_cx();
6153        let staged_vfs = MemoryVfs::new();
6154        let mut staged = make_adapter(&staged_vfs, &cx);
6155
6156        staged
6157            .append_frame(&cx, 1, &sample_page(0), 1)
6158            .expect("append");
6159        assert!(
6160            matches!(staged.into_inner(), Err(FrankenError::Busy)),
6161            "an unsynced commit must prevent consuming the adapter"
6162        );
6163
6164        let synced_vfs = MemoryVfs::new();
6165        let mut synced = make_adapter(&synced_vfs, &cx);
6166        synced
6167            .append_frame(&cx, 1, &sample_page(0), 1)
6168            .expect("append");
6169        synced.sync(&cx).expect("sync staged commit");
6170
6171        assert_eq!(synced.inner().frame_count(), 1);
6172
6173        let wal = synced.into_inner().expect("sync drained the staged frames");
6174        assert_eq!(wal.frame_count(), 1);
6175    }
6176
6177    #[test]
6178    fn test_adapter_as_dyn_wal_backend() {
6179        let cx = test_cx();
6180        let vfs = MemoryVfs::new();
6181        let mut adapter = make_adapter(&vfs, &cx);
6182
6183        // Verify it can be used as a trait object.
6184        let backend: &mut dyn WalBackend = &mut adapter;
6185        backend
6186            .append_frame(&cx, 1, &sample_page(0x77), 1)
6187            .expect("append via dyn");
6188        assert_eq!(backend.frame_count(), 1);
6189
6190        let page = backend.read_page(&cx, 1).expect("read via dyn");
6191        assert_eq!(page, Some(sample_page(0x77)));
6192    }
6193
6194    #[test]
6195    fn test_publication_snapshots_are_visible_through_wal_backend_trait() {
6196        init_wal_publication_test_tracing();
6197        let cx = test_cx();
6198        let vfs = MemoryVfs::new();
6199
6200        let file_writer = open_wal_file(&vfs, &cx);
6201        let wal_writer =
6202            WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6203        let mut writer = WalBackendAdapter::new(wal_writer);
6204
6205        writer
6206            .append_frame(&cx, 4, &sample_page(0x84), 4)
6207            .expect("append committed frame");
6208        writer.sync(&cx).expect("sync committed frame");
6209
6210        let file_reader = open_wal_file(&vfs, &cx);
6211        let wal_reader = WalFile::open(&cx, file_reader).expect("open WAL");
6212        let mut reader = WalBackendAdapter::new(wal_reader);
6213        let backend: &mut dyn WalBackend = &mut reader;
6214
6215        let published_before = backend
6216            .published_snapshot()
6217            .expect("trait should expose the adapter publication summary");
6218        assert_eq!(published_before.last_commit_frame, None);
6219        assert_eq!(published_before.commit_count, 0);
6220
6221        let refreshed = backend
6222            .refresh_published_snapshot(&cx)
6223            .expect("refresh through trait should succeed")
6224            .expect("adapter should republish an existing committed prefix");
6225        assert_eq!(refreshed.last_commit_frame, Some(0));
6226        assert_eq!(refreshed.commit_count, 1);
6227        assert_eq!(refreshed.latest_frame_entries, 1);
6228
6229        backend
6230            .begin_transaction(&cx)
6231            .expect("begin_transaction through trait should pin snapshot");
6232        let pinned = backend
6233            .pinned_read_snapshot()
6234            .expect("trait should expose the pinned read snapshot");
6235        assert_eq!(pinned, refreshed);
6236    }
6237
6238    // -- Page index O(1) lookup tests --
6239
6240    #[test]
6241    fn test_page_index_returns_correct_data() {
6242        // Write several pages, verify O(1) index returns the right data.
6243        let cx = test_cx();
6244        let vfs = MemoryVfs::new();
6245        let mut adapter = make_adapter(&vfs, &cx);
6246
6247        let page1 = sample_page(0x01);
6248        let page2 = sample_page(0x02);
6249        let page3 = sample_page(0x03);
6250
6251        adapter.append_frame(&cx, 1, &page1, 0).expect("append");
6252        adapter.append_frame(&cx, 2, &page2, 0).expect("append");
6253        adapter
6254            .append_frame(&cx, 3, &page3, 3)
6255            .expect("append commit");
6256
6257        // All three pages should be readable via the index.
6258        assert_eq!(adapter.read_page(&cx, 1).expect("read"), Some(page1));
6259        assert_eq!(adapter.read_page(&cx, 2).expect("read"), Some(page2));
6260        assert_eq!(adapter.read_page(&cx, 3).expect("read"), Some(page3));
6261
6262        // Non-existent page returns None.
6263        assert_eq!(adapter.read_page(&cx, 99).expect("read"), None);
6264    }
6265
6266    #[test]
6267    fn test_page_index_returns_latest_version() {
6268        // Write the same page twice; the index should point to the newer frame.
6269        let cx = test_cx();
6270        let vfs = MemoryVfs::new();
6271        let mut adapter = make_adapter(&vfs, &cx);
6272
6273        let old_data = sample_page(0xAA);
6274        let new_data = sample_page(0xBB);
6275
6276        adapter
6277            .append_frame(&cx, 5, &old_data, 0)
6278            .expect("append old");
6279        adapter
6280            .append_frame(&cx, 5, &new_data, 1)
6281            .expect("append new (commit)");
6282
6283        assert_eq!(
6284            adapter.read_page(&cx, 5).expect("read"),
6285            Some(new_data),
6286            "page index must return the latest frame for a page"
6287        );
6288    }
6289
6290    #[test]
6291    fn test_page_index_invalidated_on_wal_reset() {
6292        // Simulate a WAL reset with new salts. The index must be rebuilt so
6293        // stale entries from the old generation are not returned.
6294        let cx = test_cx();
6295        let vfs = MemoryVfs::new();
6296        let mut adapter = make_adapter(&vfs, &cx);
6297
6298        let old_data = sample_page(0x11);
6299        adapter
6300            .append_frame(&cx, 1, &old_data, 1)
6301            .expect("append commit");
6302
6303        // Read page 1 to populate the index.
6304        assert_eq!(adapter.read_page(&cx, 1).expect("read old"), Some(old_data));
6305
6306        // Reset WAL with new salts (simulates checkpoint reset).
6307        let new_salts = WalSalts {
6308            salt1: 0xAAAA_BBBB,
6309            salt2: 0xCCCC_DDDD,
6310        };
6311        adapter
6312            .inner_mut()
6313            .expect("no staged batch blocks inner access")
6314            .reset(&cx, 1, new_salts, false)
6315            .expect("WAL reset");
6316
6317        // Write new data for the same page number in the new generation.
6318        let new_data = sample_page(0x22);
6319        adapter
6320            .append_frame(&cx, 1, &new_data, 1)
6321            .expect("append new generation commit");
6322
6323        // The index must have been invalidated; we should get the new data.
6324        let result = adapter.read_page(&cx, 1).expect("read after reset");
6325        assert_eq!(
6326            result,
6327            Some(new_data),
6328            "after WAL reset, page index must return new-generation data, not stale cached data"
6329        );
6330
6331        // A page that existed only in the old generation should be gone.
6332        let old_only = sample_page(0x33);
6333        // (We never wrote page 99 in the new generation.)
6334        assert_eq!(
6335            adapter.read_page(&cx, 99).expect("read non-existent"),
6336            None,
6337            "pages from old WAL generation must not appear after reset"
6338        );
6339        // Suppress unused variable warning.
6340        drop(old_only);
6341    }
6342
6343    #[test]
6344    fn test_page_index_invalidated_on_same_salt_generation_change() {
6345        init_wal_publication_test_tracing();
6346        // Generation identity must include checkpoint_seq. Reusing salts across
6347        // reset must still invalidate the cached page index and avoid ABA bugs.
6348        let cx = test_cx();
6349        let vfs = MemoryVfs::new();
6350        let mut adapter = make_adapter(&vfs, &cx);
6351
6352        let reused_salts = adapter.inner().header().salts;
6353        let old_data = sample_page(0x11);
6354        adapter
6355            .append_frame(&cx, 1, &old_data, 1)
6356            .expect("append commit");
6357        assert_eq!(adapter.read_page(&cx, 1).expect("read old"), Some(old_data));
6358
6359        adapter
6360            .inner_mut()
6361            .expect("no staged batch blocks inner access")
6362            .reset(&cx, 1, reused_salts, false)
6363            .expect("reset with same salts");
6364        let new_data = sample_page(0x22);
6365        adapter
6366            .append_frame(&cx, 2, &new_data, 2)
6367            .expect("append new generation commit");
6368        let refreshed = adapter
6369            .refresh_published_snapshot(&cx)
6370            .expect("refresh published snapshot after same-salt reset");
6371        assert_eq!(refreshed.generation.checkpoint_seq, 1);
6372        assert_eq!(refreshed.generation.salts, reused_salts);
6373        assert_eq!(refreshed.last_commit_frame, Some(0));
6374        assert_eq!(refreshed.commit_count, 1);
6375        assert_eq!(refreshed.latest_frame_entries, 1);
6376
6377        assert_eq!(
6378            adapter.read_page(&cx, 1).expect("old page should be gone"),
6379            None,
6380            "cached index entries from the previous generation must be invalidated"
6381        );
6382        assert_eq!(
6383            adapter.read_page(&cx, 2).expect("read new page"),
6384            Some(new_data),
6385            "adapter must resolve pages from the new generation even when salts are reused"
6386        );
6387    }
6388
6389    #[test]
6390    fn test_refresh_published_snapshot_materializes_existing_committed_prefix() {
6391        init_wal_publication_test_tracing();
6392        let cx = test_cx();
6393        let vfs = MemoryVfs::new();
6394
6395        let file_writer = open_wal_file(&vfs, &cx);
6396        let wal_writer =
6397            WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6398        let mut writer = WalBackendAdapter::new(wal_writer);
6399
6400        let p1 = sample_page(0x71);
6401        let p2 = sample_page(0x72);
6402        writer.append_frame(&cx, 1, &p1, 0).expect("append p1");
6403        writer
6404            .append_frame(&cx, 2, &p2, 2)
6405            .expect("append p2 commit");
6406        writer.sync(&cx).expect("sync writer");
6407
6408        let file_reader = open_wal_file(&vfs, &cx);
6409        let wal_reader = WalFile::open(&cx, file_reader).expect("open reader WAL");
6410        let mut reader = WalBackendAdapter::new(wal_reader);
6411
6412        let before = reader.published_snapshot();
6413        assert_eq!(before.last_commit_frame, None);
6414        assert_eq!(before.commit_count, 0);
6415        assert_eq!(before.latest_frame_entries, 0);
6416
6417        let refreshed = reader
6418            .refresh_published_snapshot(&cx)
6419            .expect("refresh published snapshot");
6420        assert_eq!(refreshed.last_commit_frame, Some(1));
6421        assert_eq!(refreshed.commit_count, 1);
6422        assert_eq!(refreshed.latest_frame_entries, 2);
6423        assert!(refreshed.lookup_contract_is_authoritative());
6424        assert_eq!(reader.read_page(&cx, 1).expect("read p1"), Some(p1));
6425        assert_eq!(reader.read_page(&cx, 2).expect("read p2"), Some(p2));
6426    }
6427
6428    #[test]
6429    fn test_page_index_incremental_extend_after_durable_sync() {
6430        // Verify that the index extends incrementally once each commit crosses
6431        // the durable-sync publication barrier.
6432        let cx = test_cx();
6433        let vfs = MemoryVfs::new();
6434        let mut adapter = make_adapter(&vfs, &cx);
6435
6436        let page1 = sample_page(0x10);
6437        adapter
6438            .append_frame(&cx, 1, &page1, 1)
6439            .expect("append commit 1");
6440        adapter.sync(&cx).expect("durably publish commit 1");
6441
6442        // First read builds the index.
6443        assert_eq!(
6444            adapter.read_page(&cx, 1).expect("read"),
6445            Some(page1.clone())
6446        );
6447
6448        // Append more committed frames.
6449        let page2 = sample_page(0x20);
6450        let page1_v2 = sample_page(0x30);
6451        adapter
6452            .append_frame(&cx, 2, &page2, 0)
6453            .expect("append page 2");
6454        adapter
6455            .append_frame(&cx, 1, &page1_v2, 3)
6456            .expect("append page 1 v2 (commit)");
6457        adapter.sync(&cx).expect("durably publish commit 2");
6458
6459        // Reading should trigger incremental extend, not full rebuild.
6460        assert_eq!(
6461            adapter.read_page(&cx, 1).expect("read page 1 v2"),
6462            Some(page1_v2),
6463            "incremental index extend should pick up the updated page"
6464        );
6465        assert_eq!(adapter.read_page(&cx, 2).expect("read page 2"), Some(page2));
6466    }
6467
6468    /// Frames for a two-page commit batch, the second frame carrying the commit.
6469    fn commit_batch_pages() -> (Vec<u8>, Vec<u8>) {
6470        (sample_page(0x71), sample_page(0x72))
6471    }
6472
6473    /// Assert no commit horizon has been published yet.
6474    fn assert_publication_unchanged(adapter: &WalBackendAdapter<impl VfsFile>, context: &str) {
6475        assert_eq!(
6476            adapter.published_snapshot.last_commit_frame, None,
6477            "{context}: publication must not advance before a successful sync"
6478        );
6479        assert_eq!(
6480            adapter.published_snapshot.commit_count, 0,
6481            "{context}: commit count must not advance before a successful sync"
6482        );
6483        assert!(
6484            adapter.published_snapshot.page_index.is_empty(),
6485            "{context}: no page may be visible before a successful sync"
6486        );
6487    }
6488
6489    #[test]
6490    fn test_append_frame_without_sync_leaves_publication_unchanged() {
6491        let cx = test_cx();
6492        let vfs = MemoryVfs::new();
6493        let mut adapter = make_adapter(&vfs, &cx);
6494
6495        let (p1, p2) = commit_batch_pages();
6496        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6497        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6498
6499        assert_publication_unchanged(&adapter, "append_frame");
6500        assert_eq!(
6501            adapter.pending_publication_commit,
6502            Some(1),
6503            "append_frame must stage the commit horizon for a later sync"
6504        );
6505    }
6506
6507    #[test]
6508    fn test_append_frames_without_sync_leaves_publication_unchanged() {
6509        let cx = test_cx();
6510        let vfs = MemoryVfs::new();
6511        let mut adapter = make_adapter(&vfs, &cx);
6512
6513        let (p1, p2) = commit_batch_pages();
6514        let frames = [
6515            WalFrameRef {
6516                page_number: 1,
6517                page_data: &p1,
6518                db_size_if_commit: 0,
6519            },
6520            WalFrameRef {
6521                page_number: 2,
6522                page_data: &p2,
6523                db_size_if_commit: 2,
6524            },
6525        ];
6526        adapter
6527            .append_frames(&cx, &frames)
6528            .expect("append frames batch");
6529
6530        assert_publication_unchanged(&adapter, "append_frames");
6531        assert_eq!(
6532            adapter.pending_publication_commit,
6533            Some(1),
6534            "append_frames must stage the commit horizon for a later sync"
6535        );
6536    }
6537
6538    #[test]
6539    fn test_append_frames_tracked_without_sync_leaves_publication_unchanged() {
6540        let cx = test_cx();
6541        let vfs = MemoryVfs::new();
6542        let mut adapter = make_adapter(&vfs, &cx);
6543
6544        let (p1, p2) = commit_batch_pages();
6545        let frames = [
6546            WalFrameRef {
6547                page_number: 1,
6548                page_data: &p1,
6549                db_size_if_commit: 0,
6550            },
6551            WalFrameRef {
6552                page_number: 2,
6553                page_data: &p2,
6554                db_size_if_commit: 2,
6555            },
6556        ];
6557        adapter
6558            .append_frames_tracked(&cx, &frames, VfsWriteCompletion::new())
6559            .expect("append tracked frames batch");
6560
6561        assert_publication_unchanged(&adapter, "append_frames_tracked");
6562        assert_eq!(
6563            adapter.pending_publication_commit,
6564            Some(1),
6565            "append_frames_tracked must stage the commit horizon for a later sync"
6566        );
6567    }
6568
6569    #[test]
6570    fn test_append_prepared_frames_without_sync_leaves_publication_unchanged() {
6571        let cx = test_cx();
6572        let vfs = MemoryVfs::new();
6573        let mut adapter = make_adapter(&vfs, &cx);
6574
6575        let (p1, p2) = commit_batch_pages();
6576        let frames = [
6577            WalFrameRef {
6578                page_number: 1,
6579                page_data: &p1,
6580                db_size_if_commit: 0,
6581            },
6582            WalFrameRef {
6583                page_number: 2,
6584                page_data: &p2,
6585                db_size_if_commit: 2,
6586            },
6587        ];
6588        let mut prepared = adapter
6589            .prepare_append_frames(&frames)
6590            .expect("prepare append")
6591            .expect("prepared batch");
6592        adapter
6593            .append_prepared_frames(&cx, &mut prepared)
6594            .expect("append prepared");
6595
6596        assert_publication_unchanged(&adapter, "append_prepared_frames");
6597        assert_eq!(
6598            adapter.pending_publication_commit,
6599            Some(1),
6600            "append_prepared_frames must stage the commit horizon for a later sync"
6601        );
6602    }
6603
6604    #[test]
6605    fn test_append_prepared_frames_tracked_without_sync_leaves_publication_unchanged() {
6606        let cx = test_cx();
6607        let vfs = MemoryVfs::new();
6608        let mut adapter = make_adapter(&vfs, &cx);
6609
6610        let (p1, p2) = commit_batch_pages();
6611        let frames = [
6612            WalFrameRef {
6613                page_number: 1,
6614                page_data: &p1,
6615                db_size_if_commit: 0,
6616            },
6617            WalFrameRef {
6618                page_number: 2,
6619                page_data: &p2,
6620                db_size_if_commit: 2,
6621            },
6622        ];
6623        let mut prepared = adapter
6624            .prepare_append_frames(&frames)
6625            .expect("prepare append")
6626            .expect("prepared batch");
6627        adapter
6628            .append_prepared_frames_tracked(&cx, &mut prepared, VfsWriteCompletion::new())
6629            .expect("append prepared tracked");
6630
6631        assert_publication_unchanged(&adapter, "append_prepared_frames_tracked");
6632        assert_eq!(
6633            adapter.pending_publication_commit,
6634            Some(1),
6635            "append_prepared_frames_tracked must stage the commit horizon for a later sync"
6636        );
6637    }
6638
6639    #[test]
6640    fn test_successful_sync_publishes_staged_commit_horizon() {
6641        let cx = test_cx();
6642        let vfs = MemoryVfs::new();
6643        let mut adapter = make_adapter(&vfs, &cx);
6644
6645        let (p1, p2) = commit_batch_pages();
6646        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6647        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6648        assert_publication_unchanged(&adapter, "before sync");
6649
6650        adapter.sync(&cx).expect("sync must succeed");
6651
6652        assert_eq!(
6653            adapter.published_snapshot.last_commit_frame,
6654            Some(1),
6655            "a successful sync must publish the staged commit horizon"
6656        );
6657        assert_eq!(
6658            adapter.published_snapshot.commit_count, 1,
6659            "a successful sync must publish the staged commit count"
6660        );
6661        assert_eq!(
6662            adapter.published_snapshot.page_index.len(),
6663            2,
6664            "a successful sync must publish every staged page"
6665        );
6666        assert_eq!(
6667            adapter.pending_publication_commit, None,
6668            "a published batch must no longer be staged"
6669        );
6670        assert!(
6671            adapter.pending_publication_frames.is_empty(),
6672            "a published batch must drain its staged frames"
6673        );
6674    }
6675
6676    #[test]
6677    fn test_failed_sync_advances_no_publication_and_retry_publishes() {
6678        let cx = test_cx();
6679        let vfs = CheckpointHandoffFaultVfs::new();
6680        let mut adapter = make_fault_adapter(&vfs, &cx);
6681
6682        let (p1, p2) = commit_batch_pages();
6683        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6684        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6685
6686        vfs.fail_next_wal_sync();
6687        let failure = adapter
6688            .sync(&cx)
6689            .expect_err("injected WAL sync failure must surface");
6690        assert!(
6691            failure.to_string().contains("injected WAL sync failure"),
6692            "sync must report the injected durability failure, got: {failure}"
6693        );
6694
6695        assert_publication_unchanged(&adapter, "after failed sync");
6696        assert_eq!(
6697            adapter.pending_publication_commit,
6698            Some(1),
6699            "a failed sync must preserve the staged horizon for retry"
6700        );
6701        assert!(
6702            !adapter.pending_publication_frames.is_empty(),
6703            "a failed sync must preserve staged frames for retry"
6704        );
6705
6706        // Retry: the same staged batch publishes once durability succeeds.
6707        adapter.sync(&cx).expect("retry sync must succeed");
6708
6709        assert_eq!(
6710            adapter.published_snapshot.last_commit_frame,
6711            Some(1),
6712            "retrying sync must publish the preserved commit horizon"
6713        );
6714        assert_eq!(
6715            adapter.published_snapshot.commit_count, 1,
6716            "retrying sync must publish the preserved commit count"
6717        );
6718        assert_eq!(
6719            adapter.published_snapshot.page_index.len(),
6720            2,
6721            "retrying sync must publish every preserved page"
6722        );
6723        assert_eq!(
6724            adapter.pending_publication_commit, None,
6725            "a retried publication must clear the staged horizon"
6726        );
6727    }
6728
6729    #[test]
6730    fn test_failed_sync_then_append_cannot_drop_or_publish_pending() {
6731        let cx = test_cx();
6732        let vfs = CheckpointHandoffFaultVfs::new();
6733        let mut adapter = make_fault_adapter(&vfs, &cx);
6734
6735        let (p1, p2) = commit_batch_pages();
6736        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6737        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6738
6739        vfs.fail_next_wal_sync();
6740        adapter
6741            .sync(&cx)
6742            .expect_err("injected WAL sync failure must surface");
6743
6744        let staged_after_failure = adapter.pending_publication_commit;
6745        let staged_frames_after_failure = adapter.pending_publication_frames.len();
6746        assert_eq!(
6747            staged_after_failure,
6748            Some(1),
6749            "failed sync must preserve the staged horizon"
6750        );
6751
6752        // A further append must not run the pre-append resynchronization, which
6753        // would discard the preserved batch and republish the unsynced horizon.
6754        let p3 = sample_page(0x73);
6755        adapter
6756            .append_frame(&cx, 3, &p3, 3)
6757            .expect("append after failed sync");
6758
6759        assert_publication_unchanged(&adapter, "append after failed sync");
6760        assert!(
6761            adapter.pending_publication_frames.len() > staged_frames_after_failure,
6762            "append after a failed sync must extend, never discard, the staged batch"
6763        );
6764        assert_eq!(
6765            adapter.pending_publication_commit,
6766            Some(2),
6767            "append after a failed sync must carry the staged horizon forward"
6768        );
6769
6770        // Durability finally succeeds: the whole preserved batch publishes.
6771        adapter.sync(&cx).expect("sync after failed attempt");
6772        assert_eq!(
6773            adapter.published_snapshot.last_commit_frame,
6774            Some(2),
6775            "recovered sync must publish the full preserved horizon"
6776        );
6777        assert_eq!(
6778            adapter.pending_publication_commit, None,
6779            "recovered sync must clear the staged horizon"
6780        );
6781    }
6782
6783    #[test]
6784    fn test_failed_sync_then_begin_transaction_then_append_fails_closed() {
6785        let cx = test_cx();
6786        let vfs = CheckpointHandoffFaultVfs::new();
6787        let mut adapter = make_fault_adapter(&vfs, &cx);
6788
6789        let (p1, p2) = commit_batch_pages();
6790        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6791        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6792
6793        vfs.fail_next_wal_sync();
6794        adapter
6795            .sync(&cx)
6796            .expect_err("injected WAL sync failure must surface");
6797        assert_eq!(
6798            adapter.pending_publication_commit,
6799            Some(1),
6800            "failed sync must preserve the staged horizon"
6801        );
6802
6803        // `begin_transaction` must reject at the earliest illegal transition,
6804        // before refreshing, pinning a read snapshot, or re-arming the
6805        // pre-append guard — and it must be a retryable Busy, not corruption.
6806        let begin_error = adapter
6807            .begin_transaction(&cx)
6808            .expect_err("begin_transaction must fail closed while frames are staged");
6809        assert!(
6810            matches!(begin_error, FrankenError::Busy),
6811            "staged-state rejection must be retryable Busy, not corruption: {begin_error:?}"
6812        );
6813        assert_publication_unchanged(&adapter, "begin_transaction refused after failed sync");
6814        assert_eq!(
6815            adapter.pending_publication_commit,
6816            Some(1),
6817            "a refused begin_transaction must not drop the staged horizon"
6818        );
6819        assert!(
6820            adapter.pinned_read_snapshot().is_none(),
6821            "a refused begin_transaction must not pin a read snapshot"
6822        );
6823
6824        // Defense in depth: the pre-append choke guard still refuses for any
6825        // other path that re-arms `refresh_before_append`.
6826        adapter.refresh_before_append = true;
6827        let p3 = sample_page(0x74);
6828        let append_error = adapter
6829            .append_frame(&cx, 3, &p3, 3)
6830            .expect_err("append must fail closed while frames are staged");
6831        assert!(
6832            matches!(append_error, FrankenError::Busy),
6833            "append rejection must be retryable Busy: {append_error:?}"
6834        );
6835        assert_publication_unchanged(&adapter, "append refused after failed sync");
6836        assert_eq!(
6837            adapter.pending_publication_commit,
6838            Some(1),
6839            "a refused append must leave the staged horizon intact"
6840        );
6841        assert!(
6842            !adapter.pending_publication_frames.is_empty(),
6843            "a refused append must leave the staged frames intact"
6844        );
6845        adapter.refresh_before_append = false;
6846
6847        // The batch is still recoverable: a successful sync publishes it.
6848        adapter.sync(&cx).expect("sync after failed attempt");
6849        assert_eq!(
6850            adapter.published_snapshot.last_commit_frame,
6851            Some(1),
6852            "recovered sync must publish the preserved horizon"
6853        );
6854    }
6855
6856    #[test]
6857    fn test_failed_sync_then_checkpoint_fails_closed_and_preserves_state() {
6858        let cx = test_cx();
6859        let vfs = CheckpointHandoffFaultVfs::new();
6860        let mut adapter = make_fault_adapter(&vfs, &cx);
6861
6862        let (p1, p2) = commit_batch_pages();
6863        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6864        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6865
6866        vfs.fail_next_wal_sync();
6867        adapter
6868            .sync(&cx)
6869            .expect_err("injected WAL sync failure must surface");
6870
6871        let frames_before = adapter.frame_count();
6872        let staged_before = adapter.pending_publication_commit;
6873        let staged_frame_count_before = adapter.pending_publication_frames.len();
6874
6875        // Checkpoint must refuse before touching the WAL: it backfills, may
6876        // reset, and can invalidate the publication plane, all of which would
6877        // destroy the staged batch.
6878        let mut writer = MockCheckpointPageWriter;
6879        let checkpoint_error = adapter
6880            .checkpoint(&cx, CheckpointMode::Passive, &mut writer, 0, None)
6881            .expect_err("checkpoint must fail closed while frames are staged");
6882        assert!(
6883            matches!(checkpoint_error, FrankenError::CheckpointFailed { .. }),
6884            "checkpoint rejection must be CheckpointFailed, not corruption: {checkpoint_error:?}"
6885        );
6886
6887        assert_eq!(
6888            adapter.frame_count(),
6889            frames_before,
6890            "a refused checkpoint must not mutate WAL bytes"
6891        );
6892        assert_publication_unchanged(&adapter, "checkpoint refused");
6893        assert_eq!(
6894            adapter.pending_publication_commit, staged_before,
6895            "a refused checkpoint must preserve the staged horizon"
6896        );
6897        assert_eq!(
6898            adapter.pending_publication_frames.len(),
6899            staged_frame_count_before,
6900            "a refused checkpoint must preserve the staged frames"
6901        );
6902
6903        // Retry: durability succeeds and the preserved batch publishes.
6904        adapter.sync(&cx).expect("retry sync must succeed");
6905        assert_eq!(
6906            adapter.published_snapshot.last_commit_frame,
6907            Some(1),
6908            "retry sync must publish the preserved horizon"
6909        );
6910        assert_eq!(
6911            adapter.pending_publication_commit, None,
6912            "a published batch must no longer be staged"
6913        );
6914    }
6915
6916    #[test]
6917    fn test_midtransaction_sync_preserves_uncommitted_frames_and_allows_continuation() {
6918        let cx = test_cx();
6919        let vfs = MemoryVfs::new();
6920        let mut adapter = make_adapter(&vfs, &cx);
6921
6922        let (p1, p2) = commit_batch_pages();
6923
6924        // Append a non-commit frame, then sync. The frame becomes durable but is
6925        // not committed, so nothing may be published.
6926        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6927        assert_eq!(
6928            adapter.pending_publication_commit, None,
6929            "a non-commit append stages no commit horizon"
6930        );
6931        adapter
6932            .sync(&cx)
6933            .expect("mid-transaction sync must succeed");
6934
6935        assert_publication_unchanged(&adapter, "sync of uncommitted frames");
6936        assert!(
6937            !adapter.pending_publication_frames.is_empty(),
6938            "a mid-transaction sync must preserve durable-but-uncommitted frames"
6939        );
6940
6941        // Continuation must remain possible: the commit marker still lands.
6942        adapter
6943            .append_frame(&cx, 2, &p2, 2)
6944            .expect("commit append after mid-transaction sync must be allowed");
6945        assert_eq!(
6946            adapter.pending_publication_commit,
6947            Some(1),
6948            "the commit append must stage the horizon for the whole batch"
6949        );
6950        assert_publication_unchanged(&adapter, "commit staged but not yet synced");
6951
6952        adapter.sync(&cx).expect("commit sync must succeed");
6953
6954        assert_eq!(
6955            adapter.published_snapshot.last_commit_frame,
6956            Some(1),
6957            "the commit sync must publish the whole batch"
6958        );
6959        assert_eq!(
6960            adapter.published_snapshot.commit_count, 1,
6961            "the batch must publish exactly one commit"
6962        );
6963        assert_eq!(
6964            adapter.published_snapshot.page_index.len(),
6965            2,
6966            "both pages must be published exactly once"
6967        );
6968        assert_eq!(
6969            adapter.published_snapshot.page_index.get(&1),
6970            Some(&0),
6971            "page 1 must map to its frame from before the mid-transaction sync"
6972        );
6973        assert_eq!(
6974            adapter.published_snapshot.page_index.get(&2),
6975            Some(&1),
6976            "page 2 must map to the commit frame"
6977        );
6978        assert!(
6979            !adapter.has_pending_publication(),
6980            "a published batch must leave nothing staged"
6981        );
6982    }
6983
6984    #[test]
6985    fn test_inner_mut_fails_closed_while_batch_is_staged() {
6986        let cx = test_cx();
6987        let vfs = CheckpointHandoffFaultVfs::new();
6988        let mut adapter = make_fault_adapter(&vfs, &cx);
6989
6990        let (p1, p2) = commit_batch_pages();
6991        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6992        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6993
6994        assert!(
6995            adapter.has_pending_publication(),
6996            "an appended-but-unsynced batch must report as pending"
6997        );
6998        // `expect_err` would require `WalFile: Debug`, which the fault-VFS file
6999        // type does not implement, so assert on the pattern directly.
7000        assert!(
7001            matches!(adapter.inner_mut(), Err(FrankenError::Busy)),
7002            "inner_mut must fail closed with retryable Busy while frames are staged"
7003        );
7004        assert_eq!(
7005            adapter.pending_publication_commit,
7006            Some(1),
7007            "a refused inner_mut must preserve the staged horizon"
7008        );
7009
7010        // Once drained, the escape hatch opens again.
7011        adapter.sync(&cx).expect("sync staged batch");
7012        assert!(
7013            !adapter.has_pending_publication(),
7014            "a published batch must clear the pending flag"
7015        );
7016        adapter
7017            .inner_mut()
7018            .expect("inner_mut must succeed once the batch is drained");
7019    }
7020
7021    #[test]
7022    fn test_unpinned_refresh_does_not_expose_staged_horizon_before_sync() {
7023        let cx = test_cx();
7024        let vfs = MemoryVfs::new();
7025        let mut adapter = make_adapter(&vfs, &cx);
7026
7027        let (p1, p2) = commit_batch_pages();
7028        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7029        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7030
7031        // An explicit refresh must not publish frames this handle has staged but
7032        // not yet made durable.
7033        adapter
7034            .refresh_published_snapshot(&cx)
7035            .expect("refresh published snapshot");
7036        assert_publication_unchanged(&adapter, "refresh with staged frames");
7037        assert_eq!(
7038            adapter.pending_publication_commit,
7039            Some(1),
7040            "refresh must leave the staged horizon intact"
7041        );
7042
7043        adapter.sync(&cx).expect("sync staged batch");
7044        assert_eq!(
7045            adapter.published_snapshot.last_commit_frame,
7046            Some(1),
7047            "sync must publish once the staged batch is durable"
7048        );
7049    }
7050
7051    #[test]
7052    fn test_authorized_deferred_commit_publishes_without_claiming_fsync() {
7053        let cx = test_cx();
7054        let vfs = MemoryVfs::new();
7055        let mut adapter = make_adapter(&vfs, &cx);
7056
7057        let (p1, p2) = commit_batch_pages();
7058        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7059        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7060        let fsynced_before = adapter.wal.last_fsynced_frame_count();
7061
7062        adapter
7063            .publish_authorized_deferred_commit(&cx)
7064            .expect("parallel-WAL authorization must publish the deferred commit");
7065
7066        assert_eq!(
7067            adapter.published_snapshot.last_commit_frame,
7068            Some(1),
7069            "the authorized commit marker must become visible"
7070        );
7071        assert_eq!(
7072            adapter.published_snapshot.commit_count, 1,
7073            "the authorized batch must publish exactly one commit"
7074        );
7075        assert!(
7076            !adapter.has_pending_publication(),
7077            "authorization must drain the staged publication horizon"
7078        );
7079        assert_eq!(
7080            adapter.wal.last_fsynced_frame_count(),
7081            fsynced_before,
7082            "deferred authorization must not claim or force an fsync"
7083        );
7084        adapter
7085            .begin_transaction(&cx)
7086            .expect("the next transaction must not see a stale Busy");
7087    }
7088
7089    #[test]
7090    fn test_commit_append_publishes_visibility_snapshot() {
7091        init_wal_publication_test_tracing();
7092        let cx = test_cx();
7093        let vfs = MemoryVfs::new();
7094        let mut adapter = make_adapter(&vfs, &cx);
7095
7096        let p1 = sample_page(0x41);
7097        let p2 = sample_page(0x42);
7098        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7099        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7100        // Publication is deferred to the durability barrier (#187); the commit
7101        // horizon only becomes visible once `sync` persists the frames.
7102        adapter.sync(&cx).expect("sync commit batch");
7103
7104        assert_eq!(
7105            adapter.published_snapshot.last_commit_frame,
7106            Some(1),
7107            "synced commit should publish the visible commit horizon"
7108        );
7109        assert_eq!(
7110            adapter.published_snapshot.commit_count, 1,
7111            "synced commit should track the visible WAL commit count"
7112        );
7113        assert_eq!(
7114            adapter.published_snapshot.page_index.len(),
7115            2,
7116            "published snapshot should track both committed pages"
7117        );
7118        assert_eq!(
7119            adapter.published_snapshot.page_index.get(&2),
7120            Some(&1),
7121            "published snapshot must map each page to its latest committed frame"
7122        );
7123    }
7124
7125    #[test]
7126    fn test_prepared_append_publishes_visibility_snapshot() {
7127        init_wal_publication_test_tracing();
7128        let cx = test_cx();
7129        let vfs = MemoryVfs::new();
7130        let mut adapter = make_adapter(&vfs, &cx);
7131
7132        let p1 = sample_page(0x51);
7133        let p2 = sample_page(0x52);
7134        let frames = [
7135            WalFrameRef {
7136                page_number: 1,
7137                page_data: &p1,
7138                db_size_if_commit: 0,
7139            },
7140            WalFrameRef {
7141                page_number: 2,
7142                page_data: &p2,
7143                db_size_if_commit: 2,
7144            },
7145        ];
7146        let mut prepared = adapter
7147            .prepare_append_frames(&frames)
7148            .expect("prepare append")
7149            .expect("prepared batch");
7150        adapter
7151            .append_prepared_frames(&cx, &mut prepared)
7152            .expect("append prepared");
7153        // Publication is deferred to the durability barrier (#187).
7154        adapter.sync(&cx).expect("sync prepared commit batch");
7155
7156        assert_eq!(
7157            adapter.published_snapshot.last_commit_frame,
7158            Some(1),
7159            "synced prepared commit should publish the visible commit horizon"
7160        );
7161        assert_eq!(
7162            adapter.published_snapshot.commit_count, 1,
7163            "synced prepared commit should track the visible WAL commit count"
7164        );
7165        assert_eq!(
7166            adapter.published_snapshot.page_index.len(),
7167            2,
7168            "synced prepared commit should publish all committed pages"
7169        );
7170        assert_eq!(
7171            adapter.published_snapshot.page_index.get(&2),
7172            Some(&1),
7173            "prepared commit append must map each page to its latest committed frame"
7174        );
7175    }
7176
7177    #[test]
7178    fn test_commit_publication_refreshes_external_prefix_before_local_commit() {
7179        let cx = test_cx();
7180        let vfs = MemoryVfs::new();
7181
7182        let file_writer = open_wal_file(&vfs, &cx);
7183        let wal_writer =
7184            WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
7185        let mut writer = WalBackendAdapter::new(wal_writer);
7186
7187        let file_follower = open_wal_file(&vfs, &cx);
7188        let wal_follower = WalFile::open(&cx, file_follower).expect("open WAL");
7189        let mut follower = WalBackendAdapter::new(wal_follower);
7190
7191        let p1 = sample_page(0x61);
7192        writer
7193            .append_frame(&cx, 1, &p1, 1)
7194            .expect("writer commit 1");
7195        writer.sync(&cx).expect("sync writer commit 1");
7196
7197        let p2 = sample_page(0x62);
7198        writer
7199            .append_frame(&cx, 2, &p2, 2)
7200            .expect("writer commit 2");
7201        writer.sync(&cx).expect("sync writer commit 2");
7202
7203        let p3 = sample_page(0x63);
7204        follower
7205            .append_frame(&cx, 3, &p3, 3)
7206            .expect("follower local commit");
7207
7208        assert_eq!(
7209            follower.published_snapshot.last_commit_frame,
7210            Some(2),
7211            "local commit should publish on top of refreshed external WAL state"
7212        );
7213        assert_eq!(
7214            follower.published_snapshot.commit_count, 3,
7215            "local commit publication should include refreshed external commits"
7216        );
7217        assert_eq!(
7218            follower.published_snapshot.page_index.get(&1),
7219            Some(&0),
7220            "refresh-before-append should preserve earlier committed pages"
7221        );
7222        assert_eq!(
7223            follower.published_snapshot.page_index.get(&2),
7224            Some(&1),
7225            "refresh-before-append should publish externally committed pages"
7226        );
7227        assert_eq!(
7228            follower.published_snapshot.page_index.get(&3),
7229            Some(&2),
7230            "local commit should extend the published WAL visibility map"
7231        );
7232        assert_eq!(follower.read_page(&cx, 1).expect("read p1"), Some(p1));
7233        assert_eq!(follower.read_page(&cx, 2).expect("read p2"), Some(p2));
7234        assert_eq!(follower.read_page(&cx, 3).expect("read p3"), Some(p3));
7235    }
7236
7237    #[test]
7238    fn test_truncate_checkpoint_republishes_empty_generation_snapshot() {
7239        init_wal_publication_test_tracing();
7240        let cx = test_cx();
7241        let vfs = MemoryVfs::new();
7242        let mut adapter = make_adapter(&vfs, &cx);
7243        let mut writer = MockCheckpointPageWriter;
7244
7245        adapter
7246            .append_frame(&cx, 1, &sample_page(0x61), 1)
7247            .expect("append committed frame");
7248        // Publication is deferred to the durability barrier (#187), and
7249        // checkpoint now fails closed while a batch is staged, so the batch must
7250        // be drained before checkpointing.
7251        adapter.sync(&cx).expect("sync committed frame");
7252        let before = adapter.published_snapshot();
7253        assert_eq!(before.last_commit_frame, Some(0));
7254        assert_eq!(before.commit_count, 1);
7255        assert_eq!(before.latest_frame_entries, 1);
7256
7257        let result = adapter
7258            .checkpoint(&cx, CheckpointMode::Truncate, &mut writer, 0, None)
7259            .expect("truncate checkpoint");
7260        assert!(result.completed);
7261        assert!(result.wal_was_reset);
7262
7263        let after = adapter.published_snapshot();
7264        assert_ne!(
7265            before.generation, after.generation,
7266            "truncate checkpoint should publish a new WAL generation"
7267        );
7268        assert_eq!(after.last_commit_frame, None);
7269        assert_eq!(after.commit_count, 0);
7270        assert_eq!(after.latest_frame_entries, 0);
7271        assert!(after.lookup_contract_is_authoritative());
7272    }
7273
7274    // -- Partial index fallback tests --
7275
7276    #[test]
7277    fn test_partial_index_falls_back_to_linear_scan() {
7278        init_wal_publication_test_tracing();
7279        // Verify that when the page index cap is hit, pages that weren't
7280        // indexed are still found via the backwards linear scan fallback.
7281        let cx = test_cx();
7282        let vfs = MemoryVfs::new();
7283        let mut adapter = make_adapter(&vfs, &cx);
7284
7285        // Set a very small cap so we can trigger the partial-index path
7286        // with just a handful of frames.
7287        adapter.set_page_index_cap(2);
7288
7289        // Write 5 distinct pages.  With a cap of 2, only the first 2 unique
7290        // pages will be indexed; pages 3-5 will be dropped from the index.
7291        let p1 = sample_page(0x01);
7292        let p2 = sample_page(0x02);
7293        let p3 = sample_page(0x03);
7294        let p4 = sample_page(0x04);
7295        let p5 = sample_page(0x05);
7296
7297        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7298        adapter.append_frame(&cx, 2, &p2, 0).expect("append p2");
7299        adapter.append_frame(&cx, 3, &p3, 0).expect("append p3");
7300        adapter.append_frame(&cx, 4, &p4, 0).expect("append p4");
7301        adapter
7302            .append_frame(&cx, 5, &p5, 5)
7303            .expect("append p5 (commit)");
7304
7305        // Pages 1 and 2 should be in the index (fast path).
7306        assert_eq!(
7307            adapter.read_page(&cx, 1).expect("read p1"),
7308            Some(p1),
7309            "indexed page should be found via HashMap"
7310        );
7311        assert_eq!(
7312            adapter.read_page(&cx, 2).expect("read p2"),
7313            Some(p2),
7314            "indexed page should be found via HashMap"
7315        );
7316
7317        // Pages 3-5 were NOT indexed, but must still be found via the
7318        // backwards linear scan fallback.
7319        assert_eq!(
7320            adapter.read_page(&cx, 3).expect("read p3"),
7321            Some(p3),
7322            "non-indexed page must be found via linear scan fallback"
7323        );
7324        assert_eq!(
7325            adapter.read_page(&cx, 4).expect("read p4"),
7326            Some(p4),
7327            "non-indexed page must be found via linear scan fallback"
7328        );
7329        assert_eq!(
7330            adapter.read_page(&cx, 5).expect("read p5"),
7331            Some(p5),
7332            "non-indexed page must be found via linear scan fallback"
7333        );
7334
7335        // A page that was never written should still return None.
7336        assert_eq!(
7337            adapter.read_page(&cx, 99).expect("read non-existent"),
7338            None,
7339            "non-existent page must return None even with partial index"
7340        );
7341
7342        // Verify the index was indeed marked partial.
7343        assert!(
7344            adapter.published_snapshot.index_is_partial,
7345            "index_is_partial should be true when cap is exceeded"
7346        );
7347    }
7348
7349    #[test]
7350    fn test_partial_index_returns_latest_version_via_fallback() {
7351        // When the same page appears multiple times and overflows the index,
7352        // the backwards scan must return the LATEST (highest frame index)
7353        // version, not the first one it encounters in a forward scan.
7354        let cx = test_cx();
7355        let vfs = MemoryVfs::new();
7356        let mut adapter = make_adapter(&vfs, &cx);
7357
7358        // Cap at 1 so only page 1 fits in the index.
7359        adapter.set_page_index_cap(1);
7360
7361        let old_p2 = sample_page(0xAA);
7362        let new_p2 = sample_page(0xBB);
7363
7364        // Frame 0: page 1 (indexed)
7365        adapter
7366            .append_frame(&cx, 1, &sample_page(0x01), 0)
7367            .expect("append p1");
7368        // Frame 1: page 2 old version (NOT indexed -- cap exceeded)
7369        adapter
7370            .append_frame(&cx, 2, &old_p2, 0)
7371            .expect("append p2 old");
7372        // Frame 2: page 2 new version (NOT indexed -- cap exceeded, and
7373        // page 2 is not already in the index so it won't be updated)
7374        adapter
7375            .append_frame(&cx, 2, &new_p2, 3)
7376            .expect("append p2 new (commit)");
7377
7378        // The backwards scan from frame 2 should find the newest version first.
7379        assert_eq!(
7380            adapter.read_page(&cx, 2).expect("read p2"),
7381            Some(new_p2),
7382            "backwards scan must return the most recent frame for the page"
7383        );
7384    }
7385
7386    #[test]
7387    fn test_lookup_contract_distinguishes_authoritative_and_fallback_paths() {
7388        init_wal_publication_test_tracing();
7389        let cx = test_cx();
7390        let vfs = MemoryVfs::new();
7391        let mut adapter = make_adapter(&vfs, &cx);
7392        adapter.set_page_index_cap(1);
7393
7394        let p1 = sample_page(0x01);
7395        let p2 = sample_page(0x02);
7396        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7397        adapter
7398            .append_frame(&cx, 2, &p2, 2)
7399            .expect("append p2 commit");
7400
7401        let last_commit = adapter
7402            .inner_mut()
7403            .expect("no staged batch blocks inner access")
7404            .last_commit_frame(&cx)
7405            .expect("last commit")
7406            .expect("commit exists");
7407        adapter
7408            .publish_visible_snapshot(&cx, Some(last_commit), "lookup_contract_test")
7409            .expect("build published snapshot");
7410        let snapshot = adapter.published_snapshot.clone();
7411
7412        assert_eq!(
7413            adapter
7414                .resolve_visible_frame(&cx, &snapshot, 1)
7415                .expect("resolve indexed page"),
7416            WalPageLookupResolution::AuthoritativeHit { frame_index: 0 }
7417        );
7418        assert_eq!(
7419            adapter
7420                .resolve_visible_frame(&cx, &snapshot, 2)
7421                .expect("resolve fallback page"),
7422            WalPageLookupResolution::PartialIndexFallbackHit { frame_index: 1 }
7423        );
7424        assert_eq!(
7425            adapter
7426                .resolve_visible_frame(&cx, &snapshot, 99)
7427                .expect("resolve missing page"),
7428            WalPageLookupResolution::PartialIndexFallbackMiss
7429        );
7430    }
7431
7432    #[test]
7433    fn test_lookup_contract_is_authoritative_by_default() {
7434        let cx = test_cx();
7435        let vfs = MemoryVfs::new();
7436        let mut adapter = make_adapter(&vfs, &cx);
7437
7438        let p1 = sample_page(0x11);
7439        let p2 = sample_page(0x22);
7440        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7441        adapter
7442            .append_frame(&cx, 2, &p2, 2)
7443            .expect("append p2 commit");
7444
7445        let last_commit = adapter
7446            .inner_mut()
7447            .expect("no staged batch blocks inner access")
7448            .last_commit_frame(&cx)
7449            .expect("last commit")
7450            .expect("commit exists");
7451        adapter
7452            .publish_visible_snapshot(&cx, Some(last_commit), "lookup_contract_default")
7453            .expect("build published snapshot");
7454        let snapshot = adapter.published_snapshot.clone();
7455
7456        assert!(
7457            !snapshot.index_is_partial,
7458            "default index should be authoritative"
7459        );
7460        assert_eq!(
7461            adapter
7462                .resolve_visible_frame(&cx, &snapshot, 1)
7463                .expect("resolve page 1"),
7464            WalPageLookupResolution::AuthoritativeHit { frame_index: 0 }
7465        );
7466        assert_eq!(
7467            adapter
7468                .resolve_visible_frame(&cx, &snapshot, 2)
7469                .expect("resolve page 2"),
7470            WalPageLookupResolution::AuthoritativeHit { frame_index: 1 }
7471        );
7472        assert_eq!(
7473            adapter
7474                .resolve_visible_frame(&cx, &snapshot, 99)
7475                .expect("resolve missing page"),
7476            WalPageLookupResolution::AuthoritativeMiss
7477        );
7478    }
7479
7480    #[test]
7481    fn test_committed_txns_since_page_uses_visible_frame_horizon() {
7482        let cx = test_cx();
7483        let vfs = MemoryVfs::new();
7484        let mut adapter = make_adapter(&vfs, &cx);
7485
7486        let p1 = sample_page(0x31);
7487        let p2 = sample_page(0x32);
7488        let p3 = sample_page(0x33);
7489
7490        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7491        adapter.append_frame(&cx, 2, &p2, 2).expect("commit tx1");
7492        adapter.append_frame(&cx, 3, &p3, 0).expect("append p3");
7493        adapter.append_frame(&cx, 2, &p2, 3).expect("commit tx2");
7494
7495        assert_eq!(
7496            adapter
7497                .committed_txns_since_page(&cx, 1)
7498                .expect("count txns since page 1"),
7499            1
7500        );
7501        assert_eq!(
7502            adapter
7503                .committed_txns_since_page(&cx, 2)
7504                .expect("count txns since page 2"),
7505            0
7506        );
7507        assert_eq!(
7508            adapter
7509                .committed_txns_since_page(&cx, 99)
7510                .expect("count txns since missing page"),
7511            2
7512        );
7513        assert_eq!(
7514            adapter
7515                .committed_txn_count(&cx)
7516                .expect("count visible transactions"),
7517            2
7518        );
7519    }
7520
7521    #[test]
7522    fn test_conflicting_pages_since_snapshot_detects_later_wal_writes() {
7523        let cx = test_cx();
7524        let vfs = MemoryVfs::new();
7525        let mut adapter = make_adapter(&vfs, &cx);
7526
7527        let p1 = sample_page(0x41);
7528        let p2_before = sample_page(0x42);
7529        let p2_after = sample_page(0x43);
7530        let p3 = sample_page(0x44);
7531
7532        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7533        adapter
7534            .append_frame(&cx, 2, &p2_before, 2)
7535            .expect("commit tx1");
7536        adapter
7537            .begin_transaction(&cx)
7538            .expect("pin transaction snapshot");
7539        let pinned = adapter
7540            .pinned_read_snapshot()
7541            .expect("transaction should expose pinned WAL snapshot");
7542        let conflict_snapshot = TransactionConflictSnapshot {
7543            generation: pinned.generation,
7544            last_commit_frame: pinned.last_commit_frame,
7545            commit_count: pinned.commit_count,
7546        };
7547
7548        adapter
7549            .append_frame(&cx, 3, &p3, 0)
7550            .expect("append unrelated later page");
7551        adapter
7552            .append_frame(&cx, 2, &p2_after, 3)
7553            .expect("commit later page 2 update");
7554
7555        let conflicts = adapter
7556            .conflicting_pages_since_snapshot(&cx, conflict_snapshot, &[2, 99], &[])
7557            .expect("conflict check should scan later committed frames");
7558        assert_eq!(conflicts, vec![2]);
7559
7560        let unrelated = adapter
7561            .conflicting_pages_since_snapshot(&cx, conflict_snapshot, &[99], &[])
7562            .expect("unrelated page should stay conflict-free");
7563        assert!(unrelated.is_empty());
7564    }
7565
7566    // -- CheckpointTargetAdapterRef tests --
7567
7568    #[test]
7569    fn test_checkpoint_adapter_write_page() {
7570        let cx = test_cx();
7571        let mut writer = MockCheckpointPageWriter;
7572        let mut adapter = CheckpointTargetAdapterRef {
7573            writer: &mut writer,
7574        };
7575
7576        let page_no = PageNumber::new(1).expect("valid page number");
7577        adapter
7578            .write_page(&cx, page_no, &[0u8; 4096])
7579            .expect("write_page");
7580    }
7581
7582    #[test]
7583    fn test_checkpoint_adapter_truncate_db() {
7584        let cx = test_cx();
7585        let mut writer = MockCheckpointPageWriter;
7586        let mut adapter = CheckpointTargetAdapterRef {
7587            writer: &mut writer,
7588        };
7589
7590        adapter.truncate_db(&cx, 10).expect("truncate_db");
7591    }
7592
7593    #[test]
7594    fn test_checkpoint_adapter_sync_db() {
7595        let cx = test_cx();
7596        let mut writer = MockCheckpointPageWriter;
7597        let mut adapter = CheckpointTargetAdapterRef {
7598            writer: &mut writer,
7599        };
7600
7601        adapter.sync_db(&cx).expect("sync_db");
7602    }
7603
7604    #[test]
7605    fn test_checkpoint_adapter_as_dyn_target() {
7606        let cx = test_cx();
7607        let mut writer = MockCheckpointPageWriter;
7608        let mut adapter = CheckpointTargetAdapterRef {
7609            writer: &mut writer,
7610        };
7611
7612        // Verify it can be used as a trait object.
7613        let target: &mut dyn CheckpointTarget = &mut adapter;
7614        let page_no = PageNumber::new(3).expect("valid page number");
7615        target
7616            .write_page(&cx, page_no, &[0u8; 4096])
7617            .expect("write via dyn");
7618        target.truncate_db(&cx, 5).expect("truncate via dyn");
7619        target.sync_db(&cx).expect("sync via dyn");
7620    }
7621}