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
1801            let mut conflicts = HashSet::<u32>::new();
1802
1803            // bd-o81ov: cross-connection EOF double-allocation guard.
1804            //
1805            // Each committing connection has its own pager allocator, so two
1806            // connections can both hand out the same fresh EOF page number from
1807            // a stale committed `db_size`, link that one physical page into
1808            // different B-tree positions, and both commit — leaving the durable
1809            // image referencing a single page from multiple parents ("page N
1810            // referenced multiple times", broken point-seeks, duplicated rows).
1811            // A candidate page beyond this transaction's allocator base
1812            // (`snapshot_db_size`) that already exists within the current durable
1813            // committed size was allocated and committed by a peer first: fail
1814            // closed so the caller retries against the refreshed size and
1815            // re-allocates a non-conflicting page. This runs regardless of the
1816            // horizon short-circuits below, because the aliasing peer commit can
1817            // predate this transaction's own conflict horizon (its allocator
1818            // `db_size` lagged the WAL state its snapshot already observed).
1819            // Existing cross-process first-committer-wins: reject any candidate a
1820            // peer committed after this transaction's WAL conflict horizon.
1821            if !(latest.commit_count <= snapshot.commit_count
1822                && latest.generation == snapshot.generation
1823                && latest.last_commit_frame <= snapshot.last_commit_frame)
1824            {
1825                if latest.generation != snapshot.generation {
1826                    for &page in &candidates {
1827                        conflicts.insert(page);
1828                    }
1829                } else if let Some(latest_last_commit_frame) = latest.last_commit_frame {
1830                    let start_frame = snapshot
1831                        .last_commit_frame
1832                        .map_or(0, |frame| frame.saturating_add(1));
1833                    if start_frame <= latest_last_commit_frame {
1834                        let candidate_set = candidates.iter().copied().collect::<HashSet<_>>();
1835                        for frame_index in start_frame..=latest_last_commit_frame {
1836                            let header = self.wal.read_frame_header(cx, frame_index).await?;
1837                            if candidate_set.contains(&header.page_number) {
1838                                conflicts.insert(header.page_number);
1839                            }
1840                        }
1841                    }
1842                }
1843            }
1844
1845            // A candidate beyond the allocator's begin-time committed size is a
1846            // freshly allocated EOF page. If ANY committed frame for that page
1847            // already exists in this WAL generation, a peer connection
1848            // allocated and committed the same physical page first — committing
1849            // ours would link one page into two B-tree positions ("page N
1850            // referenced multiple times"). The horizon-relative scan above
1851            // cannot catch this: a rebased/refreshed snapshot can sit PAST the
1852            // peer's growth frame, and commit-frame `db_size` headers are not
1853            // monotonic under concurrency (a stale-view pure-update commit
1854            // regresses them), so a size comparison is unreliable. The
1855            // generation-wide page index is the authoritative "was this page
1856            // ever committed" source. A false positive is possible when the
1857            // allocator base lags the publication plane and the candidate is
1858            // an ordinary rewrite of a recently committed page — that fails
1859            // closed as a transient BusySnapshot retry.
1860            if snapshot.snapshot_db_size > 0
1861                && latest.generation == snapshot.generation
1862                && candidates
1863                    .iter()
1864                    .any(|page| *page > snapshot.snapshot_db_size)
1865            {
1866                let published = self.published_snapshot.clone();
1867                for &page in &candidates {
1868                    if page > snapshot.snapshot_db_size
1869                        && !matches!(
1870                            self.resolve_visible_frame(cx, &published, page).await?,
1871                            WalPageLookupResolution::AuthoritativeMiss
1872                                | WalPageLookupResolution::PartialIndexFallbackMiss
1873                        )
1874                    {
1875                        tracing::debug!(
1876                            target: "fsqlite.wal.conflict",
1877                            page,
1878                            allocation_base_db_size = snapshot.snapshot_db_size,
1879                            latest_commit_frame = ?latest.last_commit_frame,
1880                            "fresh EOF allocation aliases a committed page; failing \
1881                             closed with BusySnapshot (bd-o81ov)"
1882                        );
1883                        conflicts.insert(page);
1884                    }
1885                }
1886            }
1887
1888            let mut conflicts = conflicts.into_iter().collect::<Vec<_>>();
1889            conflicts.sort_unstable();
1890            Ok(conflicts)
1891        })
1892    }
1893
1894    fn committed_txn_count<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, u64> {
1895        Box::pin(async move {
1896            let snapshot = if let Some(snapshot) = self.read_snapshot.clone() {
1897                snapshot
1898            } else {
1899                self.publish_latest_committed_snapshot(cx, "committed_txn_count")
1900                    .await?;
1901                self.published_snapshot.clone()
1902            };
1903            Ok(snapshot.commit_count)
1904        })
1905    }
1906
1907    fn sync(&mut self, cx: &Cx) -> Result<()> {
1908        // Durability first. Only once the frames are on stable storage may the
1909        // staged commit horizon become visible to readers.
1910        //
1911        // `refresh_before_append` is deliberately NOT set on the failure paths.
1912        // Setting it would let the next append run
1913        // `synchronize_publication_before_append`, which discards the preserved
1914        // batch and republishes straight from the WAL — reinstating exactly the
1915        // publish-before-fsync hazard this guard exists to prevent. Leaving it
1916        // clear keeps the staged batch intact for a later retry.
1917        self.wal.sync(cx, SyncFlags::NORMAL)?;
1918        self.publish_pending_after_sync(cx)?;
1919        // Re-arm the pre-append resynchronization only when nothing is staged.
1920        //
1921        // Syncing mid-transaction makes the appended frames durable but does not
1922        // commit them: with no commit marker yet, `publish_pending_after_sync`
1923        // correctly publishes nothing and the frames stay staged for the commit
1924        // still to come. Re-arming here would send the next append through
1925        // `synchronize_publication_before_append`, whose fail-closed guard would
1926        // then reject every further append — including the commit marker — and
1927        // strand the transaction permanently.
1928        if !self.has_pending_publication() {
1929            self.refresh_before_append = true;
1930        }
1931        Ok(())
1932    }
1933
1934    fn frame_count(&self) -> usize {
1935        self.wal.frame_count()
1936    }
1937
1938    fn checkpoint<'a>(
1939        &'a mut self,
1940        cx: &'a Cx,
1941        mode: CheckpointMode,
1942        writer: &'a mut dyn CheckpointPageWriter,
1943        backfilled_frames: u32,
1944        oldest_reader_frame: Option<u32>,
1945    ) -> WalFuture<'a, CheckpointResult> {
1946        Box::pin(async move {
1947            // Fail closed BEFORE `wal.refresh` or any writer mutation. Checkpoint
1948            // backfills and may reset the WAL, and its inner paths can call
1949            // `invalidate_publication`, which discards the staged batch. Running
1950            // any of that against frames that were never fsynced would both lose
1951            // the staged horizon and risk backfilling non-durable frames, so the
1952            // batch must be drained by a successful sync first.
1953            if self.has_pending_publication() {
1954                return Err(FrankenError::CheckpointFailed {
1955                    detail: "staged, unpublished frames remain; a successful commit sync must \
1956                             drain them before checkpointing"
1957                        .to_owned(),
1958                });
1959            }
1960            // Refresh so planner state reflects the latest on-disk WAL shape.
1961            self.wal.refresh(cx).await?;
1962            self.refresh_before_append = true;
1963            let total_frames = u32::try_from(self.wal.frame_count()).unwrap_or(u32::MAX);
1964
1965            // Build checkpoint state for the planner.
1966            let state = CheckpointState {
1967                total_frames,
1968                backfilled_frames,
1969                oldest_reader_frame,
1970            };
1971
1972            // Wrap the CheckpointPageWriter in a CheckpointTargetAdapter.
1973            let mut target = CheckpointTargetAdapterRef { writer };
1974
1975            // Execute the checkpoint.
1976            let result =
1977                execute_checkpoint(cx, &mut self.wal, to_wal_mode(mode), state, &mut target)
1978                    .await?;
1979
1980            // Checkpoint-aware FEC lifecycle: once frames are backfilled to the
1981            // database file, their FEC symbols are no longer needed.  Clear
1982            // pending FEC results for the checkpointed range.
1983            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1984            if result.frames_backfilled > 0 {
1985                let drained = self.fec_pending.len();
1986                self.fec_pending.clear();
1987                if drained > 0 {
1988                    debug!(
1989                        drained_groups = drained,
1990                        frames_backfilled = result.frames_backfilled,
1991                        "FEC symbols reclaimed after checkpoint"
1992                    );
1993                }
1994            }
1995
1996            // If the WAL was fully reset, also discard any buffered FEC pages
1997            // and invalidate the page index (salts changed).
1998            #[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
1999            if result.wal_was_reset {
2000                self.fec_discard();
2001            }
2002            if result.wal_was_reset {
2003                self.invalidate_publication();
2004            }
2005
2006            self.publish_latest_committed_snapshot(cx, "checkpoint")
2007                .await?;
2008
2009            Ok(CheckpointResult {
2010                total_frames,
2011                frames_backfilled: result.frames_backfilled,
2012                completed: result.plan.completes_checkpoint(),
2013                wal_was_reset: result.wal_was_reset,
2014                requested_mode: mode,
2015                effective_mode: mode,
2016            })
2017        })
2018    }
2019}
2020
2021const MIN_DURABLE_CERTIFICATE_RECORD_SIZE: usize =
2022    ParallelWalDurableCertificateRecord::MIN_ENCODED_SIZE;
2023const DURABLE_CERTIFICATE_RECORD_HEADER_SIZE: usize = 14;
2024const MAX_ORPHAN_CERTIFICATE_LOOKBACK: usize = 64;
2025
2026fn durable_certificate_declared_len(bytes: &[u8]) -> Option<usize> {
2027    let length_bytes = bytes.get(10..DURABLE_CERTIFICATE_RECORD_HEADER_SIZE)?;
2028    usize::try_from(u32::from_le_bytes([
2029        length_bytes[0],
2030        length_bytes[1],
2031        length_bytes[2],
2032        length_bytes[3],
2033    ]))
2034    .ok()
2035}
2036
2037fn durable_certificate_declares_len(bytes: &[u8], expected: usize) -> bool {
2038    durable_certificate_declared_len(bytes).is_some_and(|actual| actual.cmp(&expected).is_eq())
2039}
2040
2041fn decode_durable_certificate_record(
2042    bytes: &[u8],
2043    location: &str,
2044) -> Result<ParallelWalDurableCertificateRecord> {
2045    ParallelWalDurableCertificateRecord::from_bytes(bytes).map_err(|error| {
2046        FrankenError::WalCorrupt {
2047            detail: format!("parallel WAL certificate {location} is invalid: {error}"),
2048        }
2049    })
2050}
2051
2052fn validate_incomplete_certificate_suffix(bytes: &[u8], anchored: bool) -> Result<()> {
2053    if bytes.is_empty() {
2054        return Ok(());
2055    }
2056    if bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2057        return Err(FrankenError::WalCorrupt {
2058            detail: format!(
2059                "parallel WAL certificate torn suffix exceeds {} bytes",
2060                PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2061            ),
2062        });
2063    }
2064
2065    if bytes.len() < PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.len() {
2066        // A failed append can leave fewer bytes than the magic itself. Once a
2067        // strict record boundary anchors the suffix, those bytes are
2068        // unambiguously one incomplete append (including legacy one-byte
2069        // fault injections that predate the magic prefix).
2070        if anchored || PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.starts_with(bytes) {
2071            return Ok(());
2072        }
2073        return Err(FrankenError::WalCorrupt {
2074            detail: "parallel WAL certificate sidecar starts with non-record garbage".to_owned(),
2075        });
2076    }
2077    if !bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC) {
2078        return Err(FrankenError::WalCorrupt {
2079            detail: "parallel WAL certificate suffix does not start at a record boundary"
2080                .to_owned(),
2081        });
2082    }
2083    if bytes.len() < 10 {
2084        return Ok(());
2085    }
2086    let version = u16::from_le_bytes([bytes[8], bytes[9]]);
2087    if version != fsqlite_wal::PARALLEL_WAL_DURABLE_CERTIFICATE_RECORD_VERSION {
2088        return Err(FrankenError::WalCorrupt {
2089            detail: format!(
2090                "parallel WAL certificate suffix has unsupported record version {version}"
2091            ),
2092        });
2093    }
2094    if bytes.len() < DURABLE_CERTIFICATE_RECORD_HEADER_SIZE {
2095        return Ok(());
2096    }
2097    let declared_len =
2098        durable_certificate_declared_len(bytes).ok_or_else(|| FrankenError::WalCorrupt {
2099            detail: "parallel WAL certificate suffix length exceeds usize".to_owned(),
2100        })?;
2101    if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2102        .contains(&declared_len)
2103    {
2104        return Err(FrankenError::WalCorrupt {
2105            detail: format!(
2106                "parallel WAL certificate suffix declares invalid record length {declared_len}"
2107            ),
2108        });
2109    }
2110    if bytes.len() < declared_len {
2111        return Ok(());
2112    }
2113
2114    // A complete envelope is corruption, not a torn suffix. Strict decoding
2115    // gives a precise CRC/footer/version diagnostic. A valid complete record
2116    // here would mean more than one suffix record escaped the footer walk,
2117    // which is equally outside the one-torn-append recovery contract.
2118    decode_durable_certificate_record(&bytes[..declared_len], "suffix")?;
2119    Err(FrankenError::WalCorrupt {
2120        detail:
2121            "parallel WAL certificate sidecar contains a complete record outside the footer chain"
2122                .to_owned(),
2123    })
2124}
2125
2126fn combine_sidecar_io_results<const N: usize>(
2127    context: &str,
2128    results: [(&str, Result<()>); N],
2129) -> Result<()> {
2130    let failures = results
2131        .into_iter()
2132        .filter_map(|(stage, result)| result.err().map(|error| (stage, error)))
2133        .collect::<Vec<_>>();
2134    if failures.is_empty() {
2135        return Ok(());
2136    }
2137    if failures.len() == 1 {
2138        return failures
2139            .into_iter()
2140            .next()
2141            .map_or(Ok(()), |(_, error)| Err(error));
2142    }
2143    let details = failures
2144        .iter()
2145        .map(|(stage, error)| format!("{stage}={error}"))
2146        .collect::<Vec<_>>()
2147        .join("; ");
2148    Err(FrankenError::internal(format!("{context}: {details}")))
2149}
2150
2151/// WAL backend that can recover when the path-visible `-wal` sidecar is
2152/// removed or replaced while this process still owns an old file descriptor.
2153///
2154/// Real SQLite can checkpoint and unlink/reset `db-wal` when it does not know
2155/// about a live FrankenSQLite handle. `WalFile::refresh` is intentionally
2156/// descriptor-local, so it cannot notice that path-level mutation. This wrapper
2157/// performs a path probe before mutable WAL operations and swaps in a freshly
2158/// opened/created `WalFile` when the path-visible sidecar no longer matches the
2159/// open handle.
2160pub struct PathRefreshingWalBackend<V: Vfs>
2161where
2162    V::File: Send + Sync + 'static,
2163{
2164    vfs: V,
2165    db_path: PathBuf,
2166    wal_path: PathBuf,
2167    page_size: u32,
2168    create_missing: bool,
2169    #[cfg(all(feature = "native", any(unix, windows)))]
2170    namespace_binding: Option<Arc<DatabaseNamespaceBinding>>,
2171    inner: WalBackendAdapter<V::File>,
2172}
2173
2174impl<V> PathRefreshingWalBackend<V>
2175where
2176    V: Vfs + 'static,
2177    V::File: Send + Sync + 'static,
2178{
2179    #[must_use]
2180    pub fn new(
2181        vfs: V,
2182        db_path: impl AsRef<Path>,
2183        wal_path: impl AsRef<Path>,
2184        page_size: u32,
2185        wal: WalFile<V::File>,
2186        create_missing: bool,
2187        #[cfg(all(feature = "native", any(unix, windows)))] namespace_binding: Option<
2188            Arc<DatabaseNamespaceBinding>,
2189        >,
2190    ) -> Self {
2191        Self {
2192            vfs,
2193            db_path: db_path.as_ref().to_path_buf(),
2194            wal_path: wal_path.as_ref().to_path_buf(),
2195            page_size,
2196            create_missing,
2197            #[cfg(all(feature = "native", any(unix, windows)))]
2198            namespace_binding,
2199            inner: WalBackendAdapter::new(wal),
2200        }
2201    }
2202
2203    #[must_use]
2204    pub fn into_inner(self) -> WalBackendAdapter<V::File> {
2205        self.inner
2206    }
2207
2208    /// Swap in a replacement WAL, discarding the previous adapter.
2209    ///
2210    /// Fails closed while the outgoing adapter still holds a staged batch: the
2211    /// replacement would consume away the pending metadata, and the freshly
2212    /// wrapped adapter would republish those frames from the WAL without knowing
2213    /// they were never fsynced (GH #187). A successful sync must drain the batch
2214    /// before a path-visible replacement can proceed.
2215    fn replace_inner(&mut self, cx: &Cx, wal: WalFile<V::File>) -> Result<()> {
2216        if self.inner.has_pending_publication() {
2217            let cleanup_cx = cx.create_child();
2218            let _cleanup_mask = cleanup_cx.masked();
2219            let _ = wal.close(&cleanup_cx);
2220            return Err(FrankenError::Busy);
2221        }
2222        let old = std::mem::replace(&mut self.inner, WalBackendAdapter::new(wal));
2223        let old_wal = old.into_inner()?;
2224        let _ = old_wal.close(cx);
2225        Ok(())
2226    }
2227
2228    async fn create_replacement_wal(&self, cx: &Cx) -> Result<WalFile<V::File>> {
2229        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2230        let (file, _) = self.vfs.open(cx, Some(&self.wal_path), flags)?;
2231        // Random salts (GH #201): the replacement WAL must reject frames
2232        // from the file it replaces.
2233        let wal = WalFile::create(cx, file, self.page_size, 0, WalSalts::generate()).await?;
2234        if let Err(error) = self.vfs.sync_parent_directory(cx, &self.wal_path) {
2235            let cleanup_cx = cx.create_child();
2236            let _cleanup_mask = cleanup_cx.masked();
2237            let _ = wal.close(&cleanup_cx);
2238            return Err(error);
2239        }
2240        Ok(wal)
2241    }
2242
2243    async fn replace_with_created_wal(&mut self, cx: &Cx) -> Result<()> {
2244        let wal = self.create_replacement_wal(cx).await?;
2245        self.replace_inner(cx, wal)
2246    }
2247
2248    async fn open_replacement_wal(&self, cx: &Cx, path_file: V::File) -> Result<WalFile<V::File>> {
2249        let wal = WalFile::open(cx, path_file).await?;
2250        if u32::try_from(wal.page_size()).ok() != Some(self.page_size) {
2251            let actual_page_size = wal.page_size();
2252            let expected_page_size = self.page_size;
2253            let _ = wal.close(cx);
2254            return Err(FrankenError::WalCorrupt {
2255                detail: format!(
2256                    "WAL page size {actual_page_size} does not match database page size {expected_page_size} during path refresh"
2257                ),
2258            });
2259        }
2260        Ok(wal)
2261    }
2262
2263    async fn path_header_matches_current_handle(
2264        &self,
2265        cx: &Cx,
2266        path_file: &V::File,
2267    ) -> Result<bool> {
2268        let mut header_buf = [0_u8; WAL_HEADER_SIZE];
2269        let bytes_read = path_file.read(cx, &mut header_buf, 0).await?;
2270        if bytes_read < WAL_HEADER_SIZE {
2271            return Ok(false);
2272        }
2273
2274        let path_header = WalHeader::from_bytes(&header_buf)?;
2275        if !validate_wal_header_checksum(&header_buf, path_header.big_endian_checksum())? {
2276            return Err(FrankenError::WalCorrupt {
2277                detail: "WAL header checksum mismatch during path refresh".to_owned(),
2278            });
2279        }
2280
2281        let current_header = self.inner.inner().header();
2282        Ok(path_header.magic == current_header.magic
2283            && path_header.format_version == current_header.format_version
2284            && path_header.page_size == current_header.page_size
2285            && path_header.checkpoint_seq == current_header.checkpoint_seq
2286            && path_header.salts == current_header.salts)
2287    }
2288
2289    /// Revalidate conflict candidates across a WAL-generation transition.
2290    ///
2291    /// A stock SQLite reader may checkpoint and replace an otherwise
2292    /// unchanged WAL while a FrankenSQLite transaction is open. Treating the
2293    /// generation change itself as a write conflict produces a false
2294    /// `BusySnapshot`. Conversely, blindly accepting the new generation can
2295    /// overwrite a real external commit that was checkpointed into the main
2296    /// database. The only safe admission proof is therefore page-specific:
2297    /// every candidate must have a transaction-snapshot baseline, and its
2298    /// latest committed full-page image (new WAL first, main DB otherwise)
2299    /// must hash identically.
2300    ///
2301    /// Any missing/ambiguous baseline, unreadable or short main page, invalid
2302    /// database header, page-size change, WAL read error, or close failure
2303    /// fails closed by returning every candidate as conflicting.
2304    async fn conflicts_after_generation_change(
2305        &mut self,
2306        cx: &Cx,
2307        page_numbers: &[u32],
2308        page_baselines: &[TransactionConflictPageBaseline],
2309    ) -> Vec<u32> {
2310        let mut candidates = page_numbers
2311            .iter()
2312            .copied()
2313            .filter(|page| *page != 0)
2314            .collect::<Vec<_>>();
2315        candidates.sort_unstable();
2316        candidates.dedup();
2317        if candidates.is_empty() {
2318            return Vec::new();
2319        }
2320
2321        let mut baselines = HashMap::<u32, [u8; 32]>::new();
2322        let mut ambiguous_baselines = HashSet::<u32>::new();
2323        for baseline in page_baselines {
2324            if baseline.page_number == 0 {
2325                continue;
2326            }
2327            if let Some(previous) = baselines.insert(baseline.page_number, baseline.page_hash)
2328                && previous != baseline.page_hash
2329            {
2330                ambiguous_baselines.insert(baseline.page_number);
2331            }
2332        }
2333
2334        let main_db_flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
2335        let (mut db_file, _) = match self.vfs.open(cx, Some(&self.db_path), main_db_flags) {
2336            Ok(opened) => opened,
2337            Err(_) => return candidates,
2338        };
2339        let page_size = match usize::try_from(self.page_size) {
2340            Ok(page_size) if page_size > 0 => page_size,
2341            _ => {
2342                let _ = db_file.close(cx);
2343                return candidates;
2344            }
2345        };
2346
2347        // Validate the current main-database header before trusting offsets.
2348        // SQLite encodes a 64 KiB page as the u16 value 1.
2349        let mut page_one = vec![0_u8; page_size];
2350        let page_one_read = match db_file.read(cx, &mut page_one, 0).await {
2351            Ok(bytes_read) => bytes_read,
2352            Err(_) => {
2353                let _ = db_file.close(cx);
2354                return candidates;
2355            }
2356        };
2357        let header_page_size =
2358            (page_one_read == page_size).then(|| sqlite_database_header_page_size(&page_one));
2359        if header_page_size.flatten() != Some(self.page_size) {
2360            let _ = db_file.close(cx);
2361            return candidates;
2362        }
2363
2364        let mut conflicts = Vec::new();
2365        for &page_number in &candidates {
2366            let Some(expected_hash) = baselines.get(&page_number).copied() else {
2367                conflicts.push(page_number);
2368                continue;
2369            };
2370            if ambiguous_baselines.contains(&page_number) {
2371                conflicts.push(page_number);
2372                continue;
2373            }
2374
2375            let current_page = match self.inner.read_page(cx, page_number).await {
2376                Ok(Some(page)) if page.len() == page_size => page,
2377                Ok(Some(_)) | Err(_) => {
2378                    conflicts.push(page_number);
2379                    continue;
2380                }
2381                Ok(None) => {
2382                    let mut page = vec![0_u8; page_size];
2383                    let page_offset = u64::from(page_number.saturating_sub(1))
2384                        .saturating_mul(u64::from(self.page_size));
2385                    match db_file.read(cx, &mut page, page_offset).await {
2386                        Ok(bytes_read) if bytes_read == page_size => page,
2387                        Ok(_) | Err(_) => {
2388                            conflicts.push(page_number);
2389                            continue;
2390                        }
2391                    }
2392                }
2393            };
2394            let current_hash = *blake3::hash(&current_page).as_bytes();
2395            if current_hash != expected_hash {
2396                conflicts.push(page_number);
2397            }
2398        }
2399
2400        if db_file.close(cx).is_err() {
2401            return candidates;
2402        }
2403        conflicts.sort_unstable();
2404        conflicts.dedup();
2405        conflicts
2406    }
2407
2408    async fn ensure_current_wal_path(&mut self, cx: &Cx) -> Result<()> {
2409        #[cfg(all(feature = "native", any(unix, windows)))]
2410        if let Some(binding) = &self.namespace_binding {
2411            binding.validate_path_identity()?;
2412        }
2413        if !self.vfs.access(cx, &self.wal_path, AccessFlags::EXISTS)? {
2414            if self.create_missing {
2415                return self.replace_with_created_wal(cx).await;
2416            }
2417            return Ok(());
2418        }
2419
2420        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::WAL;
2421        let (mut path_file, _) = self.vfs.open(cx, Some(&self.wal_path), flags)?;
2422        let path_size = path_file.file_size(cx)?;
2423        if path_size < u64::try_from(WAL_HEADER_SIZE).unwrap_or(32) {
2424            let _ = path_file.close(cx);
2425            if self.create_missing {
2426                return self.replace_with_created_wal(cx).await;
2427            }
2428            return Ok(());
2429        }
2430
2431        let current_size = self.inner.inner().file().file_size(cx).unwrap_or(u64::MAX);
2432        let path_matches_current = if path_size == current_size {
2433            match self
2434                .path_header_matches_current_handle(cx, &path_file)
2435                .await
2436            {
2437                Ok(matches) => matches,
2438                Err(err) => {
2439                    let _ = path_file.close(cx);
2440                    return Err(err);
2441                }
2442            }
2443        } else {
2444            false
2445        };
2446        if !path_matches_current {
2447            let wal = self.open_replacement_wal(cx, path_file).await?;
2448            self.replace_inner(cx, wal)?;
2449        } else {
2450            let _ = path_file.close(cx);
2451        }
2452        Ok(())
2453    }
2454
2455    fn certificate_sidecar_path(&self) -> PathBuf {
2456        let mut path = self.wal_path.as_os_str().to_owned();
2457        path.push("-cert");
2458        PathBuf::from(path)
2459    }
2460
2461    fn certificate_checkpoint_handoff_path(&self) -> PathBuf {
2462        let mut path = self.wal_path.as_os_str().to_owned();
2463        path.push("-cert-head");
2464        PathBuf::from(path)
2465    }
2466
2467    async fn read_certificate_sidecar_exact(
2468        file: &V::File,
2469        cx: &Cx,
2470        offset: u64,
2471        len: usize,
2472        location: &str,
2473    ) -> Result<Vec<u8>> {
2474        let mut bytes = vec![0_u8; len];
2475        let bytes_read = file.read(cx, &mut bytes, offset).await?;
2476        if bytes_read != len {
2477            return Err(FrankenError::WalCorrupt {
2478                detail: format!(
2479                    "parallel WAL certificate {location} at offset {offset} was short-read: got {bytes_read} of {len}"
2480                ),
2481            });
2482        }
2483        Ok(bytes)
2484    }
2485
2486    async fn read_certificate_record_ending_at(
2487        file: &V::File,
2488        cx: &Cx,
2489        record_end: u64,
2490    ) -> Result<(u64, ParallelWalDurableCertificateRecord)> {
2491        let footer_size =
2492            u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE).unwrap_or(4);
2493        let footer_offset = record_end.checked_sub(footer_size).ok_or_else(|| {
2494            FrankenError::WalCorrupt {
2495                detail: format!(
2496                    "parallel WAL certificate record ending at {record_end} has no length footer"
2497                ),
2498            }
2499        })?;
2500        let footer = Self::read_certificate_sidecar_exact(
2501            file,
2502            cx,
2503            footer_offset,
2504            ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
2505            "length footer",
2506        )
2507        .await?;
2508        let record_len = usize::try_from(u32::from_le_bytes([
2509            footer[0], footer[1], footer[2], footer[3],
2510        ]))
2511        .map_err(|_| FrankenError::WalCorrupt {
2512            detail: "parallel WAL certificate footer length exceeds usize".to_owned(),
2513        })?;
2514        if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2515            .contains(&record_len)
2516        {
2517            return Err(FrankenError::WalCorrupt {
2518                detail: format!(
2519                    "parallel WAL certificate footer declares invalid record length {record_len}"
2520                ),
2521            });
2522        }
2523        let record_len_u64 = u64::try_from(record_len).map_err(|_| FrankenError::WalCorrupt {
2524            detail: "parallel WAL certificate record length exceeds u64".to_owned(),
2525        })?;
2526        let record_start =
2527            record_end
2528                .checked_sub(record_len_u64)
2529                .ok_or_else(|| FrankenError::WalCorrupt {
2530                    detail: format!(
2531                        "parallel WAL certificate record length {record_len} exceeds end offset {record_end}"
2532                    ),
2533                })?;
2534        let bytes =
2535            Self::read_certificate_sidecar_exact(file, cx, record_start, record_len, "record")
2536                .await?;
2537        let record = decode_durable_certificate_record(&bytes, "record")?;
2538        Ok((record_start, record))
2539    }
2540
2541    /// Return a safe append boundary, repairing exactly one validated torn
2542    /// suffix in place.
2543    ///
2544    /// The caller must hold the database's external writer or maintenance
2545    /// gate for the whole scan/truncate/append sequence. Keeping this helper
2546    /// private prevents a scan/reopen race from becoming part of the API.
2547    async fn prepare_certificate_sidecar_for_append(file: &mut V::File, cx: &Cx) -> Result<u64> {
2548        let file_size = file.file_size(cx)?;
2549        if file_size == 0 {
2550            return Ok(0);
2551        }
2552
2553        let footer_size =
2554            u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE).unwrap_or(4);
2555        if file_size >= footer_size {
2556            let footer = Self::read_certificate_sidecar_exact(
2557                file,
2558                cx,
2559                file_size - footer_size,
2560                ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
2561                "append-boundary length footer",
2562            )
2563            .await?;
2564            let record_len = usize::try_from(u32::from_le_bytes([
2565                footer[0], footer[1], footer[2], footer[3],
2566            ]))
2567            .unwrap_or(usize::MAX);
2568            let record_len_u64 = u64::try_from(record_len).unwrap_or(u64::MAX);
2569            if (MIN_DURABLE_CERTIFICATE_RECORD_SIZE
2570                ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2571                .contains(&record_len)
2572                && record_len_u64 <= file_size
2573            {
2574                let record_start = file_size - record_len_u64;
2575                let bytes = Self::read_certificate_sidecar_exact(
2576                    file,
2577                    cx,
2578                    record_start,
2579                    record_len,
2580                    "append-boundary record",
2581                )
2582                .await?;
2583                if bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
2584                    || durable_certificate_declares_len(&bytes, record_len)
2585                {
2586                    decode_durable_certificate_record(&bytes, "append-boundary record")?;
2587                    return Ok(file_size);
2588                }
2589            }
2590        }
2591
2592        // The EOF footer was not a complete valid record. Locate at most one
2593        // complete anchor plus one maximum-sized suffix, using exact footer
2594        // boundaries rather than a free-form magic scan.
2595        let recovery_window_size =
2596            PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE.saturating_mul(2);
2597        let recovery_window_size_u64 = u64::try_from(recovery_window_size).unwrap_or(u64::MAX);
2598        let tail_offset = file_size.saturating_sub(recovery_window_size_u64);
2599        let tail_len =
2600            usize::try_from(file_size - tail_offset).map_err(|_| FrankenError::WalCorrupt {
2601                detail: "parallel WAL certificate append-repair window exceeds usize".to_owned(),
2602            })?;
2603        let tail = Self::read_certificate_sidecar_exact(
2604            file,
2605            cx,
2606            tail_offset,
2607            tail_len,
2608            "append-repair window",
2609        )
2610        .await?;
2611        let minimum_candidate_end = tail
2612            .len()
2613            .saturating_sub(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2614            .max(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE);
2615        let mut anchor_end = None;
2616        for candidate_end in (minimum_candidate_end..tail.len()).rev() {
2617            let footer_start =
2618                candidate_end - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
2619            let footer = &tail[footer_start..candidate_end];
2620            let record_len = usize::try_from(u32::from_le_bytes([
2621                footer[0], footer[1], footer[2], footer[3],
2622            ]))
2623            .unwrap_or(usize::MAX);
2624            if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE
2625                ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2626                .contains(&record_len)
2627                || record_len > candidate_end
2628            {
2629                continue;
2630            }
2631            let record_start = candidate_end - record_len;
2632            let record_bytes = &tail[record_start..candidate_end];
2633            if !record_bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
2634                || !durable_certificate_declares_len(record_bytes, record_len)
2635            {
2636                continue;
2637            }
2638            if ParallelWalDurableCertificateRecord::from_bytes(record_bytes).is_ok() {
2639                anchor_end = Some(candidate_end);
2640                break;
2641            }
2642        }
2643
2644        let safe_end = if let Some(anchor_end) = anchor_end {
2645            validate_incomplete_certificate_suffix(&tail[anchor_end..], true)?;
2646            tail_offset
2647                .checked_add(u64::try_from(anchor_end).unwrap_or(u64::MAX))
2648                .ok_or_else(|| FrankenError::WalCorrupt {
2649                    detail: "parallel WAL certificate append-repair boundary overflow".to_owned(),
2650                })?
2651        } else {
2652            if file_size
2653                > u64::try_from(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
2654                    .unwrap_or(u64::MAX)
2655            {
2656                return Err(FrankenError::WalCorrupt {
2657                    detail: format!(
2658                        "parallel WAL certificate sidecar has no valid append boundary within its bounded {}-byte recovery suffix",
2659                        PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2660                    ),
2661                });
2662            }
2663            validate_incomplete_certificate_suffix(&tail, false)?;
2664            0
2665        };
2666
2667        if safe_end < file_size {
2668            file.truncate(cx, safe_end)?;
2669        }
2670        Ok(safe_end)
2671    }
2672
2673    async fn append_durable_certificate_record(
2674        &self,
2675        cx: &Cx,
2676        certificate: &ParallelWalCommitCertificate,
2677        wal_frame_start: u64,
2678        wal_frame_end: u64,
2679        sync: bool,
2680    ) -> Result<()> {
2681        self.append_durable_certificate_record_with_completion(
2682            cx,
2683            certificate,
2684            wal_frame_start,
2685            wal_frame_end,
2686            sync,
2687            None,
2688        )
2689        .await
2690    }
2691
2692    async fn append_durable_certificate_record_with_completion(
2693        &self,
2694        cx: &Cx,
2695        certificate: &ParallelWalCommitCertificate,
2696        wal_frame_start: u64,
2697        wal_frame_end: u64,
2698        sync: bool,
2699        completion: Option<&VfsWriteCompletion>,
2700    ) -> Result<()> {
2701        let mut preflight = WalWriteCompletionPreflight::new(completion);
2702        let expected_frame_start = u64::try_from(self.inner.frame_count())
2703            .unwrap_or(u64::MAX)
2704            .saturating_add(1);
2705        if wal_frame_start != expected_frame_start {
2706            return Err(FrankenError::internal(format!(
2707                "parallel WAL certificate starts at frame {wal_frame_start}, expected {expected_frame_start}"
2708            )));
2709        }
2710        let record = ParallelWalDurableCertificateRecord::new(
2711            self.inner.inner().generation_identity(),
2712            wal_frame_start,
2713            wal_frame_end,
2714            certificate.clone(),
2715        )
2716        .map_err(|error| {
2717            FrankenError::internal(format!(
2718                "could not encode parallel WAL durability certificate: {error}"
2719            ))
2720        })?;
2721        let record_bytes = record.to_bytes();
2722        if record_bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2723            return Err(FrankenError::WalCorrupt {
2724                detail: format!(
2725                    "parallel WAL certificate record is {} bytes; maximum is {}",
2726                    record_bytes.len(),
2727                    PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2728                ),
2729            });
2730        }
2731        let certificate_path = self.certificate_sidecar_path();
2732        let existed = self
2733            .vfs
2734            .access(cx, &certificate_path, AccessFlags::EXISTS)?;
2735        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2736        let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
2737        let append_offset = Self::prepare_certificate_sidecar_for_append(&mut file, cx).await?;
2738        preflight.hand_off();
2739        drop(preflight);
2740        let write_result = if let Some(completion) = completion {
2741            file.write_tracked(cx, &record_bytes, append_offset, completion.clone())
2742                .await
2743        } else {
2744            file.write(cx, &record_bytes, append_offset).await
2745        };
2746        if let Err(write_error) = write_result {
2747            // A VFS may report a failed write after changing a prefix of the
2748            // destination. Restore the append boundary under a masked child
2749            // context so a cooperative cancellation cannot leave a torn tail
2750            // when the failed future itself is allowed to finish.
2751            let cleanup_cx = cx.create_child();
2752            let _cleanup_mask = cleanup_cx.masked();
2753            let cleanup_result = file.truncate(&cleanup_cx, append_offset);
2754            let close_result = file.close(&cleanup_cx);
2755            return combine_sidecar_io_results(
2756                "parallel WAL certificate append cleanup failed",
2757                [
2758                    ("write", Err(write_error)),
2759                    ("truncate", cleanup_result),
2760                    ("close", close_result),
2761                ],
2762            );
2763        }
2764
2765        // Match the WAL's configured synchronous policy exactly. Even when
2766        // `sync` is false, this ordered sidecar write precedes the WAL marker;
2767        // neither write then claims power-loss-stable persistence.
2768        let finalization_cx = cx.create_child();
2769        let _finalization_mask = finalization_cx.masked();
2770        let sync_result = if sync {
2771            file.durable_sync(&finalization_cx, SyncKind::FullDurable)
2772        } else {
2773            Ok(())
2774        };
2775        let directory_sync_result = if sync && !existed && sync_result.is_ok() {
2776            self.vfs
2777                .sync_parent_directory(&finalization_cx, &certificate_path)
2778        } else {
2779            Ok(())
2780        };
2781        let close_result = file.close(&finalization_cx);
2782        combine_sidecar_io_results(
2783            "parallel WAL certificate append finalization failed",
2784            [
2785                ("file_sync", sync_result),
2786                ("directory_sync", directory_sync_result),
2787                ("close", close_result),
2788            ],
2789        )
2790    }
2791
2792    async fn reconcile_certificate_sidecar_record(
2793        &self,
2794        cx: &Cx,
2795        expected: &ParallelWalDurableCertificateRecord,
2796        remove_expected_orphan: bool,
2797        sync: bool,
2798    ) -> Result<bool> {
2799        let certificate_path = self.certificate_sidecar_path();
2800        if !self
2801            .vfs
2802            .access(cx, &certificate_path, AccessFlags::EXISTS)?
2803        {
2804            return Ok(false);
2805        }
2806
2807        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::WAL;
2808        let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
2809        let reconciliation_result = async {
2810            let original_size = file.file_size(cx)?;
2811            let safe_end = Self::prepare_certificate_sidecar_for_append(&mut file, cx).await?;
2812            let latest = if safe_end == 0 {
2813                None
2814            } else {
2815                Some(Self::read_certificate_record_ending_at(&file, cx, safe_end).await?)
2816            };
2817            let latest_is_expected = latest
2818                .as_ref()
2819                .is_some_and(|(_, record)| record == expected);
2820            let sidecar_changed = if remove_expected_orphan
2821                && let Some((record_start, _)) = latest.as_ref()
2822                && latest_is_expected
2823            {
2824                file.truncate(cx, *record_start)?;
2825                true
2826            } else {
2827                safe_end != original_size
2828            };
2829            if sync && (latest_is_expected || sidecar_changed) {
2830                file.durable_sync(cx, SyncKind::FullDurable)?;
2831            }
2832            if sync && latest_is_expected && !remove_expected_orphan {
2833                // The original write may have created the sidecar but been
2834                // dropped before its directory entry was fenced.
2835                self.vfs.sync_parent_directory(cx, &certificate_path)?;
2836            }
2837            Ok(latest_is_expected)
2838        }
2839        .await;
2840
2841        let cleanup_cx = cx.create_child();
2842        let _cleanup_mask = cleanup_cx.masked();
2843        let close_result = file.close(&cleanup_cx);
2844        match (reconciliation_result, close_result) {
2845            (Ok(latest_is_expected), Ok(())) => Ok(latest_is_expected),
2846            (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
2847            (Err(reconciliation_error), Err(close_error)) => Err(FrankenError::internal(format!(
2848                "parallel WAL certificate reconciliation failed and close also failed: reconciliation={reconciliation_error}; close={close_error}"
2849            ))),
2850        }
2851    }
2852
2853    async fn persist_checkpoint_certificate_handoff(
2854        &self,
2855        cx: &Cx,
2856        record: &ParallelWalDurableCertificateRecord,
2857    ) -> Result<()> {
2858        let handoff_path = self.certificate_checkpoint_handoff_path();
2859        let existed = self.vfs.access(cx, &handoff_path, AccessFlags::EXISTS)?;
2860        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
2861        let (mut file, _) = self.vfs.open(cx, Some(&handoff_path), flags)?;
2862        let record_bytes = record.to_bytes();
2863        if record_bytes.len() > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2864            let cleanup_cx = cx.create_child();
2865            let _cleanup_mask = cleanup_cx.masked();
2866            let close_result = file.close(&cleanup_cx);
2867            return combine_sidecar_io_results(
2868                "parallel WAL checkpoint certificate handoff is oversized",
2869                [
2870                    (
2871                        "record_size",
2872                        Err(FrankenError::WalCorrupt {
2873                            detail: format!(
2874                                "parallel WAL checkpoint certificate handoff is {} bytes; maximum is {}",
2875                                record_bytes.len(),
2876                                PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
2877                            ),
2878                        }),
2879                    ),
2880                    ("close", close_result),
2881                ],
2882            );
2883        }
2884        // GH #294: when the handoff sidecar already carries exactly this
2885        // record, rewriting it would only bump its mtime/ctime on every
2886        // close-time checkpoint. The existing bytes were durably synced by
2887        // the write that produced them, so the fence already holds.
2888        if existed {
2889            let file_size = file.file_size(cx)?;
2890            if file_size == u64::try_from(record_bytes.len()).unwrap_or(u64::MAX) {
2891                let mut current = vec![0_u8; record_bytes.len()];
2892                let unchanged = file
2893                    .read(cx, &mut current, 0)
2894                    .await
2895                    .is_ok_and(|bytes_read| {
2896                        bytes_read == record_bytes.len() && current == record_bytes
2897                    });
2898                if unchanged {
2899                    let cleanup_cx = cx.create_child();
2900                    let _cleanup_mask = cleanup_cx.masked();
2901                    return file.close(&cleanup_cx);
2902                }
2903            }
2904        }
2905        // This fence is written before the checkpoint is allowed to reset the
2906        // WAL generation. Once the in-place replacement starts, finish it
2907        // under a cancellation mask; if any stage fails, the checkpoint
2908        // returns before reset and the old WAL remains authoritative.
2909        let mutation_cx = cx.create_child();
2910        let _mutation_mask = mutation_cx.masked();
2911        let truncate_result = file.truncate(&mutation_cx, 0);
2912        let write_result = if truncate_result.is_ok() {
2913            file.write(&mutation_cx, &record_bytes, 0).await
2914        } else {
2915            Ok(())
2916        };
2917        if let Err(write_error) = write_result {
2918            let cleanup_result = file.truncate(&mutation_cx, 0);
2919            let close_result = file.close(&mutation_cx);
2920            return combine_sidecar_io_results(
2921                "parallel WAL checkpoint certificate handoff cleanup failed",
2922                [
2923                    ("truncate_before_write", truncate_result),
2924                    ("write", Err(write_error)),
2925                    ("truncate_after_write", cleanup_result),
2926                    ("close", close_result),
2927                ],
2928            );
2929        }
2930        let sync_result = if truncate_result.is_ok() {
2931            file.durable_sync(&mutation_cx, SyncKind::FullDurable)
2932        } else {
2933            Ok(())
2934        };
2935        let directory_sync_result = if !existed && truncate_result.is_ok() && sync_result.is_ok() {
2936            self.vfs.sync_parent_directory(&mutation_cx, &handoff_path)
2937        } else {
2938            Ok(())
2939        };
2940        let close_result = file.close(&mutation_cx);
2941        combine_sidecar_io_results(
2942            "parallel WAL checkpoint certificate handoff finalization failed",
2943            [
2944                ("truncate", truncate_result),
2945                ("file_sync", sync_result),
2946                ("directory_sync", directory_sync_result),
2947                ("close", close_result),
2948            ],
2949        )
2950    }
2951
2952    async fn checkpoint_certificate_handoff(
2953        &self,
2954        cx: &Cx,
2955    ) -> Result<Option<ParallelWalCommitCertificate>> {
2956        let handoff_path = self.certificate_checkpoint_handoff_path();
2957        if !self.vfs.access(cx, &handoff_path, AccessFlags::EXISTS)? {
2958            return Ok(None);
2959        }
2960        let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
2961        let (mut file, _) = self.vfs.open(cx, Some(&handoff_path), flags)?;
2962        let read_result = async {
2963            let file_size =
2964                usize::try_from(file.file_size(cx)?).map_err(|_| FrankenError::WalCorrupt {
2965                    detail: "parallel WAL checkpoint certificate handoff exceeds usize".to_owned(),
2966                })?;
2967            if file_size == 0 || file_size > PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE {
2968                return Err(FrankenError::WalCorrupt {
2969                    detail: format!(
2970                        "parallel WAL checkpoint certificate handoff has invalid size {file_size}"
2971                    ),
2972                });
2973            }
2974            let mut bytes = vec![0_u8; file_size];
2975            let bytes_read = file.read(cx, &mut bytes, 0).await?;
2976            if bytes_read != bytes.len() {
2977                return Err(FrankenError::WalCorrupt {
2978                    detail: "parallel WAL checkpoint certificate handoff was short-read".to_owned(),
2979                });
2980            }
2981            let record =
2982                ParallelWalDurableCertificateRecord::from_bytes(&bytes).map_err(|error| {
2983                    FrankenError::WalCorrupt {
2984                        detail: format!(
2985                            "parallel WAL checkpoint certificate handoff is invalid: {error}"
2986                        ),
2987                    }
2988                })?;
2989            Ok(Some(record.certificate))
2990        }
2991        .await;
2992        let cleanup_cx = cx.create_child();
2993        let _cleanup_mask = cleanup_cx.masked();
2994        let close_result = file.close(&cleanup_cx);
2995        match (read_result, close_result) {
2996            (Ok(certificate), Ok(())) => Ok(certificate),
2997            (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
2998            (Err(read_error), Err(close_error)) => Err(FrankenError::internal(format!(
2999                "parallel WAL checkpoint handoff read failed and close also failed: read={read_error}; close={close_error}"
3000            ))),
3001        }
3002    }
3003
3004    async fn wal_frame_payload_digest(
3005        &self,
3006        cx: &Cx,
3007        wal_frame_start: u64,
3008        wal_frame_end: u64,
3009    ) -> Result<[u8; 32]> {
3010        if wal_frame_start == 0 || wal_frame_end < wal_frame_start {
3011            return Err(FrankenError::WalCorrupt {
3012                detail: format!(
3013                    "invalid parallel WAL digest interval {wal_frame_start}..={wal_frame_end}"
3014                ),
3015            });
3016        }
3017
3018        let mut digest = ParallelWalFramePayloadDigestBuilder::new();
3019        for frame_number in wal_frame_start..=wal_frame_end {
3020            let frame_index = usize::try_from(frame_number.saturating_sub(1)).map_err(|_| {
3021                FrankenError::WalCorrupt {
3022                    detail: format!(
3023                        "parallel WAL digest frame number {frame_number} exceeds usize"
3024                    ),
3025                }
3026            })?;
3027            let (header, page_data) = self.inner.inner().read_frame(cx, frame_index).await?;
3028            let page_number =
3029                PageNumber::new(header.page_number).ok_or_else(|| FrankenError::WalCorrupt {
3030                    detail: format!(
3031                        "parallel WAL digest frame {frame_number} has invalid page number {}",
3032                        header.page_number
3033                    ),
3034                })?;
3035            digest.update(page_number, header.db_size, &page_data);
3036        }
3037        Ok(digest.finalize())
3038    }
3039
3040    async fn latest_authorized_durable_certificate_record(
3041        &self,
3042        cx: &Cx,
3043    ) -> Result<Option<ParallelWalDurableCertificateRecord>> {
3044        let certificate_path = self.certificate_sidecar_path();
3045        if !self
3046            .vfs
3047            .access(cx, &certificate_path, AccessFlags::EXISTS)?
3048        {
3049            return Ok(None);
3050        }
3051        let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
3052        let (mut file, _) = self.vfs.open(cx, Some(&certificate_path), flags)?;
3053        let read_result = async {
3054            let file_size = file.file_size(cx)?;
3055            if file_size == 0 {
3056                return Ok(None);
3057            }
3058
3059            // Healthy operation is O(1): the final four bytes identify the
3060            // exact newest record, so only its footer and bytes are read.
3061            let footer_size =
3062                u64::try_from(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE)
3063                    .unwrap_or(4);
3064            let mut newest = None;
3065            if file_size >= footer_size {
3066                let footer_offset = file_size - footer_size;
3067                let footer = Self::read_certificate_sidecar_exact(
3068                    &file,
3069                    cx,
3070                    footer_offset,
3071                    ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE,
3072                    "newest length footer",
3073                )
3074                .await?;
3075                let record_len = usize::try_from(u32::from_le_bytes([
3076                    footer[0], footer[1], footer[2], footer[3],
3077                ]))
3078                .map_err(|_| FrankenError::WalCorrupt {
3079                    detail: "parallel WAL certificate newest footer length exceeds usize"
3080                        .to_owned(),
3081                })?;
3082                let record_len_u64 = u64::try_from(record_len).unwrap_or(u64::MAX);
3083                if (MIN_DURABLE_CERTIFICATE_RECORD_SIZE
3084                    ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3085                    .contains(&record_len)
3086                    && record_len_u64 <= file_size
3087                {
3088                    let record_start = file_size - record_len_u64;
3089                    let bytes = Self::read_certificate_sidecar_exact(
3090                        &file,
3091                        cx,
3092                        record_start,
3093                        record_len,
3094                        "newest record",
3095                    )
3096                    .await?;
3097                    // A matching magic or self-declared length makes this a
3098                    // fully-present envelope candidate. Strict decoding is
3099                    // mandatory even when its magic/version/CRC/footer is
3100                    // corrupt; complete corruption is never a torn suffix.
3101                    if bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
3102                        || durable_certificate_declares_len(&bytes, record_len)
3103                    {
3104                        let record =
3105                            decode_durable_certificate_record(&bytes, "newest record")?;
3106                        newest = Some((record_start, record));
3107                    }
3108                }
3109            }
3110
3111            if newest.is_none() {
3112                // Invalid EOF footer means the last append may have torn.
3113                // Search only footer-derived candidates within one maximum
3114                // suffix, retaining enough preceding bytes for one maximum
3115                // anchor record. Magic is only a cheap validation after a
3116                // candidate footer establishes an exact boundary; it is never
3117                // used as a free-form scan key.
3118                let recovery_window_size =
3119                    PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE.saturating_mul(2);
3120                let recovery_window_size_u64 =
3121                    u64::try_from(recovery_window_size).unwrap_or(u64::MAX);
3122                let tail_offset = file_size.saturating_sub(recovery_window_size_u64);
3123                let tail_len =
3124                    usize::try_from(file_size - tail_offset).map_err(|_| {
3125                        FrankenError::WalCorrupt {
3126                            detail: "parallel WAL certificate recovery window exceeds usize"
3127                                .to_owned(),
3128                        }
3129                    })?;
3130                let tail = Self::read_certificate_sidecar_exact(
3131                    &file,
3132                    cx,
3133                    tail_offset,
3134                    tail_len,
3135                    "recovery window",
3136                )
3137                .await?;
3138                let minimum_candidate_end = tail
3139                    .len()
3140                    .saturating_sub(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3141                    .max(ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE);
3142                let mut anchor = None;
3143                for candidate_end in (minimum_candidate_end..tail.len()).rev() {
3144                    let footer_start = candidate_end
3145                        - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
3146                    let footer = &tail[footer_start..candidate_end];
3147                    let record_len = usize::try_from(u32::from_le_bytes([
3148                        footer[0], footer[1], footer[2], footer[3],
3149                    ]))
3150                    .unwrap_or(usize::MAX);
3151                    if !(MIN_DURABLE_CERTIFICATE_RECORD_SIZE
3152                        ..=PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3153                        .contains(&record_len)
3154                        || record_len > candidate_end
3155                    {
3156                        continue;
3157                    }
3158                    let record_start = candidate_end - record_len;
3159                    let record_bytes = &tail[record_start..candidate_end];
3160                    if !record_bytes.starts_with(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC)
3161                        || !durable_certificate_declares_len(record_bytes, record_len)
3162                    {
3163                        continue;
3164                    }
3165                    if let Ok(record) =
3166                        ParallelWalDurableCertificateRecord::from_bytes(record_bytes)
3167                    {
3168                        anchor = Some((record_start, candidate_end, record));
3169                        break;
3170                    }
3171                }
3172
3173                if let Some((record_start, record_end, record)) = anchor {
3174                    validate_incomplete_certificate_suffix(&tail[record_end..], true)?;
3175                    let absolute_start = tail_offset
3176                        .checked_add(u64::try_from(record_start).unwrap_or(u64::MAX))
3177                        .ok_or_else(|| FrankenError::WalCorrupt {
3178                            detail:
3179                                "parallel WAL certificate recovery anchor offset overflow"
3180                                    .to_owned(),
3181                        })?;
3182                    newest = Some((absolute_start, record));
3183                } else {
3184                    if file_size
3185                        > u64::try_from(PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE)
3186                            .unwrap_or(u64::MAX)
3187                    {
3188                        return Err(FrankenError::WalCorrupt {
3189                            detail: format!(
3190                                "parallel WAL certificate sidecar has no valid record within its bounded {}-byte recovery suffix",
3191                                PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
3192                            ),
3193                        });
3194                    }
3195                    validate_incomplete_certificate_suffix(&tail, false)?;
3196                    return Ok(None);
3197                }
3198            }
3199
3200            let valid_frame_count = u64::try_from(self.inner.frame_count()).unwrap_or(u64::MAX);
3201            let wal_generation = self.inner.inner().generation_identity();
3202            let (mut record_start, mut record) = newest.ok_or_else(|| {
3203                FrankenError::WalCorrupt {
3204                    detail: "parallel WAL certificate recovery produced no record".to_owned(),
3205                }
3206            })?;
3207            let mut unauthorized_records = 0_usize;
3208            loop {
3209                // Append order is generation order. Once the newest tail
3210                // belongs to a prior reset generation, no earlier sidecar
3211                // record can authorize the current WAL; checkpoint clock
3212                // continuation comes from the fixed handoff anchor instead.
3213                if record.wal_generation != wal_generation {
3214                    return Ok(None);
3215                }
3216                let frame_index =
3217                    usize::try_from(record.wal_frame_end.saturating_sub(1)).map_err(|_| {
3218                        FrankenError::WalCorrupt {
3219                            detail: "parallel WAL certificate commit-marker index exceeds usize"
3220                                .to_owned(),
3221                        }
3222                    })?;
3223                let commit_marker_frame = if frame_index < self.inner.frame_count()
3224                    && self
3225                        .inner
3226                        .inner()
3227                        .read_frame_header(cx, frame_index)
3228                        .await?
3229                        .is_commit()
3230                {
3231                    record.wal_frame_end
3232                } else {
3233                    0
3234                };
3235                let actual_wal_frame_payload_digest =
3236                    if commit_marker_frame == record.wal_frame_end {
3237                        Some(
3238                            self.wal_frame_payload_digest(
3239                                cx,
3240                                record.wal_frame_start,
3241                                record.wal_frame_end,
3242                            )
3243                            .await?,
3244                        )
3245                    } else {
3246                        None
3247                    };
3248                if actual_wal_frame_payload_digest.is_some_and(|actual_digest| {
3249                    record.authorizes_wal_boundary(
3250                        wal_generation,
3251                        valid_frame_count,
3252                        commit_marker_frame,
3253                        actual_digest,
3254                    )
3255                }) {
3256                    return Ok(Some(record));
3257                }
3258
3259                // A current-generation record whose committed boundary lies
3260                // beyond this reader's frame snapshot is not an orphan:
3261                // concurrent committers keep appending certificates while the
3262                // walk runs, so records newer than the snapshot are expected
3263                // under write load and must not consume the bounded orphan
3264                // budget (bd-e0ghc: three writers racing a concurrent BEGIN
3265                // exhausted the 64-record lookback on a healthy database).
3266                // Genuine orphans — records inside the snapshot that fail the
3267                // commit-marker or digest checks — still count, so real
3268                // sidecar corruption trips the bound exactly as before. The
3269                // walk itself stays terminating either way: record_start is
3270                // strictly decreasing and stops at zero.
3271                if record.wal_frame_end > valid_frame_count {
3272                    tracing::debug!(
3273                        target: "fsqlite::wal::durability_combiner",
3274                        future_certificate_epoch = record.certificate.certificate_epoch,
3275                        future_commit_seq_hi = record.certificate.commit_seq_hi.get(),
3276                        future_wal_frame_end = record.wal_frame_end,
3277                        valid_frame_count,
3278                        "skipped parallel WAL certificate newer than reader frame snapshot"
3279                    );
3280                } else {
3281                    unauthorized_records = unauthorized_records.saturating_add(1);
3282                    if unauthorized_records > MAX_ORPHAN_CERTIFICATE_LOOKBACK {
3283                        return Err(FrankenError::WalCorrupt {
3284                            detail: format!(
3285                                "parallel WAL certificate sidecar exceeded bounded orphan lookback {MAX_ORPHAN_CERTIFICATE_LOOKBACK}"
3286                            ),
3287                        });
3288                    }
3289                    tracing::debug!(
3290                        target: "fsqlite::wal::durability_combiner",
3291                        orphan_certificate_epoch = record.certificate.certificate_epoch,
3292                        orphan_commit_seq_hi = record.certificate.commit_seq_hi.get(),
3293                        orphan_wal_frame_end = record.wal_frame_end,
3294                        lookback = unauthorized_records,
3295                        "ignored unauthorized parallel WAL certificate tail"
3296                    );
3297                }
3298                if record_start == 0 {
3299                    return Ok(None);
3300                }
3301                (record_start, record) =
3302                    Self::read_certificate_record_ending_at(&file, cx, record_start).await?;
3303            }
3304        }
3305        .await;
3306        let cleanup_cx = cx.create_child();
3307        let _cleanup_mask = cleanup_cx.masked();
3308        let close_result = file.close(&cleanup_cx);
3309        match (read_result, close_result) {
3310            (Ok(certificate), Ok(())) => Ok(certificate),
3311            (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error),
3312            (Err(read_error), Err(close_error)) => Err(FrankenError::internal(format!(
3313                "parallel WAL certificate tail read failed and close also failed: read={read_error}; close={close_error}"
3314            ))),
3315        }
3316    }
3317}
3318
3319impl<V> WalBackend for PathRefreshingWalBackend<V>
3320where
3321    V: Vfs + 'static,
3322    V::File: Send + Sync + 'static,
3323{
3324    fn begin_transaction<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
3325        Box::pin(async move {
3326            self.ensure_current_wal_path(cx).await?;
3327            self.inner.begin_transaction(cx).await
3328        })
3329    }
3330
3331    fn published_snapshot(&self) -> Option<WalPublicationSnapshot> {
3332        Some(self.inner.published_snapshot())
3333    }
3334
3335    fn pinned_read_snapshot(&self) -> Option<WalPublicationSnapshot> {
3336        self.inner.pinned_read_snapshot()
3337    }
3338
3339    fn pinned_logical_read_snapshot<'a>(
3340        &'a self,
3341        cx: &'a Cx,
3342    ) -> WalFuture<'a, Option<WalLogicalReadSnapshot>> {
3343        Box::pin(async move {
3344            let Some(pinned) = self.inner.pinned_read_snapshot() else {
3345                return Ok(None);
3346            };
3347            let Some(last_commit_frame) = pinned.last_commit_frame else {
3348                return Ok(None);
3349            };
3350            let Some(record) = self
3351                .latest_authorized_durable_certificate_record(cx)
3352                .await?
3353            else {
3354                return Ok(None);
3355            };
3356            if record.wal_generation != pinned.generation {
3357                return Err(FrankenError::WalCorrupt {
3358                    detail: "current logical WAL certificate generation differs from pinned reader"
3359                        .to_owned(),
3360                });
3361            }
3362            let certificate_commit_frame =
3363                usize::try_from(record.wal_frame_end.checked_sub(1).ok_or_else(|| {
3364                    FrankenError::WalCorrupt {
3365                        detail: "current logical WAL certificate ends at frame zero".to_owned(),
3366                    }
3367                })?)
3368                .map_err(|_| FrankenError::WalCorrupt {
3369                    detail: "current logical WAL certificate frame exceeds usize".to_owned(),
3370                })?;
3371            if certificate_commit_frame > last_commit_frame {
3372                return Err(FrankenError::WalCorrupt {
3373                    detail: "current logical WAL certificate extends past pinned reader horizon"
3374                        .to_owned(),
3375                });
3376            }
3377
3378            let first_tail_frame =
3379                usize::try_from(record.wal_frame_end).map_err(|_| FrankenError::WalCorrupt {
3380                    detail: "logical WAL tail frame exceeds usize".to_owned(),
3381                })?;
3382            let mut tail_commit_count = 0_u64;
3383            if first_tail_frame <= last_commit_frame {
3384                for frame_index in first_tail_frame..=last_commit_frame {
3385                    if self
3386                        .inner
3387                        .inner()
3388                        .read_frame_header(cx, frame_index)
3389                        .await?
3390                        .is_commit()
3391                    {
3392                        tail_commit_count = tail_commit_count.checked_add(1).ok_or_else(|| {
3393                            FrankenError::WalCorrupt {
3394                                detail: "logical WAL tail commit count overflow".to_owned(),
3395                            }
3396                        })?;
3397                    }
3398                }
3399            }
3400            let visible_commit_seq = CommitSeq::new(
3401                record
3402                    .certificate
3403                    .commit_seq_hi
3404                    .get()
3405                    .checked_add(tail_commit_count)
3406                    .ok_or_else(|| FrankenError::WalCorrupt {
3407                        detail: "logical WAL visible commit sequence overflow".to_owned(),
3408                    })?,
3409            );
3410            Ok(Some(WalLogicalReadSnapshot {
3411                generation: pinned.generation,
3412                last_commit_frame: pinned.last_commit_frame,
3413                visible_commit_seq,
3414            }))
3415        })
3416    }
3417
3418    fn refresh_published_snapshot<'a>(
3419        &'a mut self,
3420        cx: &'a Cx,
3421    ) -> WalFuture<'a, Option<WalPublicationSnapshot>> {
3422        Box::pin(async move {
3423            self.ensure_current_wal_path(cx).await?;
3424            self.inner.refresh_published_snapshot(cx).await.map(Some)
3425        })
3426    }
3427
3428    fn publish_authorized_deferred_commit<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()> {
3429        Box::pin(async move { self.inner.publish_authorized_deferred_commit(cx) })
3430    }
3431
3432    fn append_frame<'a>(
3433        &'a mut self,
3434        cx: &'a Cx,
3435        page_number: u32,
3436        page_data: &'a [u8],
3437        db_size_if_commit: u32,
3438    ) -> WalFuture<'a, ()> {
3439        Box::pin(async move {
3440            self.ensure_current_wal_path(cx).await?;
3441            self.inner
3442                .append_frame(cx, page_number, page_data, db_size_if_commit)
3443                .await
3444        })
3445    }
3446
3447    fn append_frames<'a>(
3448        &'a mut self,
3449        cx: &'a Cx,
3450        frames: &'a [WalFrameRef<'a>],
3451    ) -> WalFuture<'a, ()> {
3452        Box::pin(async move {
3453            self.ensure_current_wal_path(cx).await?;
3454            self.inner.append_frames(cx, frames).await
3455        })
3456    }
3457
3458    fn append_frames_tracked<'a>(
3459        &'a mut self,
3460        cx: &'a Cx,
3461        frames: &'a [WalFrameRef<'a>],
3462        completion: VfsWriteCompletion,
3463    ) -> WalFuture<'a, ()> {
3464        Box::pin(async move {
3465            let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3466            self.ensure_current_wal_path(cx).await?;
3467            preflight.hand_off();
3468            drop(preflight);
3469            self.inner
3470                .append_frames_tracked(cx, frames, completion)
3471                .await
3472        })
3473    }
3474
3475    fn prepare_append_frames(
3476        &self,
3477        frames: &[WalFrameRef<'_>],
3478    ) -> Result<Option<PreparedWalFrameBatch>> {
3479        self.inner.prepare_append_frames(frames)
3480    }
3481
3482    fn finalize_prepared_frames(
3483        &self,
3484        cx: &Cx,
3485        prepared: &mut PreparedWalFrameBatch,
3486    ) -> Result<()> {
3487        self.inner.finalize_prepared_frames(cx, prepared)
3488    }
3489
3490    fn append_prepared_frames<'a>(
3491        &'a mut self,
3492        cx: &'a Cx,
3493        prepared: &'a mut PreparedWalFrameBatch,
3494    ) -> WalFuture<'a, ()> {
3495        Box::pin(async move {
3496            self.ensure_current_wal_path(cx).await?;
3497            self.inner.append_prepared_frames(cx, prepared).await
3498        })
3499    }
3500
3501    fn append_prepared_frames_tracked<'a>(
3502        &'a mut self,
3503        cx: &'a Cx,
3504        prepared: &'a mut PreparedWalFrameBatch,
3505        completion: VfsWriteCompletion,
3506    ) -> WalFuture<'a, ()> {
3507        Box::pin(async move {
3508            let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3509            self.ensure_current_wal_path(cx).await?;
3510            preflight.hand_off();
3511            drop(preflight);
3512            self.inner
3513                .append_prepared_frames_tracked(cx, prepared, completion)
3514                .await
3515        })
3516    }
3517
3518    fn persist_parallel_wal_commit_certificate<'a>(
3519        &'a mut self,
3520        cx: &'a Cx,
3521        certificate: &'a ParallelWalCommitCertificate,
3522        wal_frame_start: u64,
3523        wal_frame_end: u64,
3524        sync: bool,
3525    ) -> WalFuture<'a, ()> {
3526        Box::pin(async move {
3527            self.ensure_current_wal_path(cx).await?;
3528            self.append_durable_certificate_record(
3529                cx,
3530                certificate,
3531                wal_frame_start,
3532                wal_frame_end,
3533                sync,
3534            )
3535            .await
3536        })
3537    }
3538
3539    fn persist_parallel_wal_commit_certificate_tracked<'a>(
3540        &'a mut self,
3541        cx: &'a Cx,
3542        certificate: &'a ParallelWalCommitCertificate,
3543        wal_frame_start: u64,
3544        wal_frame_end: u64,
3545        sync: bool,
3546        completion: VfsWriteCompletion,
3547    ) -> WalFuture<'a, ()> {
3548        Box::pin(async move {
3549            let mut preflight = WalWriteCompletionPreflight::new(Some(&completion));
3550            self.ensure_current_wal_path(cx).await?;
3551            preflight.hand_off();
3552            drop(preflight);
3553            self.append_durable_certificate_record_with_completion(
3554                cx,
3555                certificate,
3556                wal_frame_start,
3557                wal_frame_end,
3558                sync,
3559                Some(&completion),
3560            )
3561            .await
3562        })
3563    }
3564
3565    fn reconcile_parallel_wal_commit<'a>(
3566        &'a mut self,
3567        cx: &'a Cx,
3568        certificate: &'a ParallelWalCommitCertificate,
3569        wal_frame_start: u64,
3570        wal_frame_end: u64,
3571        sync: bool,
3572    ) -> WalFuture<'a, ParallelWalCommitReconciliation> {
3573        Box::pin(async move {
3574            self.ensure_current_wal_path(cx).await?;
3575            self.inner.wal.refresh(cx).await?;
3576            let wal_generation = self.inner.wal.generation_identity();
3577            let expected_record = ParallelWalDurableCertificateRecord::new(
3578                wal_generation,
3579                wal_frame_start,
3580                wal_frame_end,
3581                certificate.clone(),
3582            )
3583            .map_err(|error| {
3584                FrankenError::internal(format!(
3585                    "could not reconstruct in-doubt parallel WAL certificate: {error}"
3586                ))
3587            })?;
3588
3589            let valid_frame_count = u64::try_from(self.inner.wal.frame_count()).unwrap_or(u64::MAX);
3590            let target_commit_present = if valid_frame_count < wal_frame_end {
3591                false
3592            } else {
3593                let target_index =
3594                    usize::try_from(wal_frame_end.saturating_sub(1)).map_err(|_| {
3595                        FrankenError::WalCorrupt {
3596                            detail: "in-doubt WAL commit-marker index exceeds usize".to_owned(),
3597                        }
3598                    })?;
3599                self.inner
3600                    .wal
3601                    .read_frame_header(cx, target_index)
3602                    .await?
3603                    .is_commit()
3604            };
3605
3606            if target_commit_present {
3607                if valid_frame_count != wal_frame_end {
3608                    return Err(FrankenError::WalCorrupt {
3609                        detail: format!(
3610                            "in-doubt parallel WAL interval ends at frame {wal_frame_end}, but the retained writer gate observed committed frame count {valid_frame_count}"
3611                        ),
3612                    });
3613                }
3614                let actual_wal_frame_payload_digest = self
3615                    .wal_frame_payload_digest(cx, wal_frame_start, wal_frame_end)
3616                    .await?;
3617                if !expected_record.authorizes_wal_boundary(
3618                    wal_generation,
3619                    valid_frame_count,
3620                    wal_frame_end,
3621                    actual_wal_frame_payload_digest,
3622                ) {
3623                    return Err(FrankenError::WalCorrupt {
3624                        detail: format!(
3625                            "in-doubt parallel WAL interval {wal_frame_start}..={wal_frame_end} does not match its content-bound certificate"
3626                        ),
3627                    });
3628                }
3629                let sidecar_is_exact = self
3630                    .reconcile_certificate_sidecar_record(cx, &expected_record, false, sync)
3631                    .await?;
3632                if !sidecar_is_exact {
3633                    return Err(FrankenError::WalCorrupt {
3634                        detail: format!(
3635                            "parallel WAL commit marker at frame {wal_frame_end} has no exact durable certificate"
3636                        ),
3637                    });
3638                }
3639                if sync {
3640                    self.inner.wal.sync(cx, SyncFlags::NORMAL)?;
3641                    self.vfs.sync_parent_directory(cx, &self.wal_path)?;
3642                }
3643                return Ok(ParallelWalCommitReconciliation::Authorized);
3644            }
3645
3646            let committed_prefix_before =
3647                wal_frame_start
3648                    .checked_sub(1)
3649                    .ok_or_else(|| FrankenError::WalCorrupt {
3650                        detail: "parallel WAL recovery interval starts at frame zero".to_owned(),
3651                    })?;
3652            if valid_frame_count != committed_prefix_before {
3653                return Err(FrankenError::WalCorrupt {
3654                    detail: format!(
3655                        "in-doubt WAL interval {wal_frame_start}..={wal_frame_end} has unexpected committed prefix {valid_frame_count}"
3656                    ),
3657                });
3658            }
3659            // Only after the live WAL shape is classified as the exact
3660            // pre-interval prefix may reconciliation repair torn sidecar bytes
3661            // or remove the matching orphan certificate. Unexpected WAL state
3662            // preserves all durable evidence for diagnosis and retry.
3663            self.reconcile_certificate_sidecar_record(cx, &expected_record, true, sync)
3664                .await?;
3665            self.inner.wal.repair_uncommitted_tail(cx)?;
3666            if sync {
3667                self.inner.wal.sync(cx, SyncFlags::NORMAL)?;
3668                self.vfs.sync_parent_directory(cx, &self.wal_path)?;
3669            }
3670            Ok(ParallelWalCommitReconciliation::NotCommitted)
3671        })
3672    }
3673
3674    fn latest_authorized_parallel_wal_commit_certificate<'a>(
3675        &'a mut self,
3676        cx: &'a Cx,
3677    ) -> WalFuture<'a, Option<ParallelWalCommitCertificate>> {
3678        Box::pin(async move {
3679            self.ensure_current_wal_path(cx).await?;
3680            if let Some(record) = self
3681                .latest_authorized_durable_certificate_record(cx)
3682                .await?
3683            {
3684                return Ok(Some(record.certificate));
3685            }
3686            self.checkpoint_certificate_handoff(cx).await
3687        })
3688    }
3689
3690    fn read_page<'a>(&'a mut self, cx: &'a Cx, page_number: u32) -> WalFuture<'a, Option<Vec<u8>>> {
3691        Box::pin(async move {
3692            self.ensure_current_wal_path(cx).await?;
3693            self.inner.read_page(cx, page_number).await
3694        })
3695    }
3696
3697    fn read_page_pinned<'a>(
3698        &'a self,
3699        cx: &'a Cx,
3700        page_number: u32,
3701    ) -> WalFuture<'a, Option<Vec<u8>>> {
3702        Box::pin(async move { self.inner.read_page_pinned(cx, page_number).await })
3703    }
3704
3705    fn supports_pinned_reads(&self) -> bool {
3706        self.inner.supports_pinned_reads()
3707    }
3708
3709    fn committed_txns_since_page<'a>(
3710        &'a mut self,
3711        cx: &'a Cx,
3712        page_number: u32,
3713    ) -> WalFuture<'a, u64> {
3714        Box::pin(async move {
3715            self.ensure_current_wal_path(cx).await?;
3716            self.inner.committed_txns_since_page(cx, page_number).await
3717        })
3718    }
3719
3720    fn conflicting_pages_since_snapshot<'a>(
3721        &'a mut self,
3722        cx: &'a Cx,
3723        snapshot: TransactionConflictSnapshot,
3724        page_numbers: &'a [u32],
3725        page_baselines: &'a [TransactionConflictPageBaseline],
3726    ) -> WalFuture<'a, Vec<u32>> {
3727        Box::pin(async move {
3728            self.ensure_current_wal_path(cx).await?;
3729            let latest = self.inner.refresh_published_snapshot(cx).await?;
3730            if latest.generation != snapshot.generation {
3731                return Ok(self
3732                    .conflicts_after_generation_change(cx, page_numbers, page_baselines)
3733                    .await);
3734            }
3735            self.inner
3736                .conflicting_pages_since_snapshot(cx, snapshot, page_numbers, page_baselines)
3737                .await
3738        })
3739    }
3740
3741    fn committed_txn_count<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, u64> {
3742        Box::pin(async move {
3743            self.ensure_current_wal_path(cx).await?;
3744            self.inner.committed_txn_count(cx).await
3745        })
3746    }
3747
3748    fn sync(&mut self, cx: &Cx) -> Result<()> {
3749        #[cfg(all(feature = "native", any(unix, windows)))]
3750        if let Some(binding) = &self.namespace_binding {
3751            binding.validate_path_identity()?;
3752        }
3753        self.inner.sync(cx)
3754    }
3755
3756    fn frame_count(&self) -> usize {
3757        self.inner.frame_count()
3758    }
3759
3760    fn checkpoint<'a>(
3761        &'a mut self,
3762        cx: &'a Cx,
3763        mode: CheckpointMode,
3764        writer: &'a mut dyn CheckpointPageWriter,
3765        backfilled_frames: u32,
3766        oldest_reader_frame: Option<u32>,
3767    ) -> WalFuture<'a, CheckpointResult> {
3768        Box::pin(async move {
3769            self.ensure_current_wal_path(cx).await?;
3770            let checkpoint_handoff = self
3771                .latest_authorized_durable_certificate_record(cx)
3772                .await?;
3773            if let Some(record) = checkpoint_handoff.as_ref() {
3774                // Fence the certificate clock before the checkpoint is
3775                // allowed to reset the WAL generation. Replacing the handoff
3776                // is intentionally non-authoritative while the old WAL and
3777                // sidecar remain reconstructible: a crash, cancellation, or
3778                // write failure here aborts the checkpoint without destroying
3779                // the previous generation's source of truth.
3780                self.persist_checkpoint_certificate_handoff(cx, record)
3781                    .await?;
3782            }
3783            let result = self
3784                .inner
3785                .checkpoint(cx, mode, writer, backfilled_frames, oldest_reader_frame)
3786                .await?;
3787            Ok(result)
3788        })
3789    }
3790}
3791
3792/// Adapter wrapping a `&mut dyn CheckpointPageWriter` to implement `CheckpointTarget`.
3793///
3794/// This is used internally by `WalBackendAdapter::checkpoint` to bridge the
3795/// pager's writer to the WAL executor's target trait.
3796struct CheckpointTargetAdapterRef<'a> {
3797    writer: &'a mut dyn CheckpointPageWriter,
3798}
3799
3800impl CheckpointTarget for CheckpointTargetAdapterRef<'_> {
3801    fn write_page<'a>(
3802        &'a mut self,
3803        cx: &'a Cx,
3804        page_no: PageNumber,
3805        data: &'a [u8],
3806    ) -> CheckpointTargetFuture<'a, ()> {
3807        Box::pin(async move { self.writer.write_page(cx, page_no, data).await })
3808    }
3809
3810    fn truncate_db<'a>(&'a mut self, cx: &'a Cx, n_pages: u32) -> CheckpointTargetFuture<'a, ()> {
3811        Box::pin(async move { self.writer.truncate(cx, n_pages).await })
3812    }
3813
3814    fn sync_db<'a>(&'a mut self, cx: &'a Cx) -> CheckpointTargetFuture<'a, ()> {
3815        Box::pin(async move { self.writer.sync(cx).await })
3816    }
3817}
3818
3819// ---------------------------------------------------------------------------
3820// Tests
3821// ---------------------------------------------------------------------------
3822
3823#[cfg(test)]
3824mod tests {
3825    use std::sync::Mutex;
3826
3827    use fsqlite_pager::MockCheckpointPageWriter;
3828    use fsqlite_pager::traits::WalFrameRef;
3829    use fsqlite_types::flags::VfsOpenFlags;
3830    use fsqlite_vfs::MemoryVfs;
3831    use fsqlite_vfs::traits::{Vfs, VfsFile};
3832    use fsqlite_wal::checksum::WalSalts;
3833
3834    use super::*;
3835
3836    const PAGE_SIZE: u32 = 4096;
3837    const CERTIFICATE_PATH: &str = "test.db-wal-cert";
3838    const CHECKPOINT_HANDOFF_PATH: &str = "test.db-wal-cert-head";
3839
3840    #[derive(Clone, Copy, Debug)]
3841    enum CheckpointHandoffWriteFault {
3842        Error,
3843        Pending,
3844    }
3845
3846    #[derive(Clone, Debug, Eq, PartialEq)]
3847    enum CertificateSyncObservation {
3848        Ordinary(PathBuf),
3849        Durable(PathBuf, SyncKind),
3850    }
3851
3852    #[derive(Debug, Default)]
3853    struct CheckpointHandoffFaultState {
3854        next_write: Option<CheckpointHandoffWriteFault>,
3855        fail_next_sync: bool,
3856        /// Fail the next sync on a non-handoff (i.e. WAL) file.
3857        fail_next_wal_sync: bool,
3858        sync_observations: Vec<CertificateSyncObservation>,
3859    }
3860
3861    #[derive(Clone, Debug)]
3862    struct CheckpointHandoffFaultVfs {
3863        inner: MemoryVfs,
3864        faults: Arc<Mutex<CheckpointHandoffFaultState>>,
3865    }
3866
3867    impl CheckpointHandoffFaultVfs {
3868        fn new() -> Self {
3869            Self {
3870                inner: MemoryVfs::new(),
3871                faults: Arc::new(Mutex::new(CheckpointHandoffFaultState::default())),
3872            }
3873        }
3874
3875        fn fail_next_handoff_write(&self) {
3876            self.faults
3877                .lock()
3878                .unwrap_or_else(std::sync::PoisonError::into_inner)
3879                .next_write = Some(CheckpointHandoffWriteFault::Error);
3880        }
3881
3882        fn pend_next_handoff_write(&self) {
3883            self.faults
3884                .lock()
3885                .unwrap_or_else(std::sync::PoisonError::into_inner)
3886                .next_write = Some(CheckpointHandoffWriteFault::Pending);
3887        }
3888
3889        fn fail_next_handoff_sync(&self) {
3890            self.faults
3891                .lock()
3892                .unwrap_or_else(std::sync::PoisonError::into_inner)
3893                .fail_next_sync = true;
3894        }
3895
3896        /// Arm a one-shot sync failure on the WAL file itself.
3897        fn fail_next_wal_sync(&self) {
3898            self.faults
3899                .lock()
3900                .unwrap_or_else(std::sync::PoisonError::into_inner)
3901                .fail_next_wal_sync = true;
3902        }
3903
3904        fn take_sync_observations(&self) -> Vec<CertificateSyncObservation> {
3905            std::mem::take(
3906                &mut self
3907                    .faults
3908                    .lock()
3909                    .unwrap_or_else(std::sync::PoisonError::into_inner)
3910                    .sync_observations,
3911            )
3912        }
3913    }
3914
3915    #[derive(Debug)]
3916    struct CheckpointHandoffFaultFile {
3917        inner: <MemoryVfs as Vfs>::File,
3918        faults: Arc<Mutex<CheckpointHandoffFaultState>>,
3919        path: Option<PathBuf>,
3920        is_checkpoint_handoff: bool,
3921    }
3922
3923    impl Vfs for CheckpointHandoffFaultVfs {
3924        type File = CheckpointHandoffFaultFile;
3925
3926        fn name(&self) -> &'static str {
3927            "checkpoint-handoff-fault"
3928        }
3929
3930        fn open(
3931            &self,
3932            cx: &Cx,
3933            path: Option<&Path>,
3934            flags: VfsOpenFlags,
3935        ) -> Result<(Self::File, VfsOpenFlags)> {
3936            let is_checkpoint_handoff =
3937                path.is_some_and(|candidate| candidate == Path::new(CHECKPOINT_HANDOFF_PATH));
3938            let (inner, actual_flags) = self.inner.open(cx, path, flags)?;
3939            Ok((
3940                CheckpointHandoffFaultFile {
3941                    inner,
3942                    faults: Arc::clone(&self.faults),
3943                    path: path.map(Path::to_path_buf),
3944                    is_checkpoint_handoff,
3945                },
3946                actual_flags,
3947            ))
3948        }
3949
3950        fn delete(&self, cx: &Cx, path: &Path, sync_dir: bool) -> Result<()> {
3951            self.inner.delete(cx, path, sync_dir)
3952        }
3953
3954        fn sync_parent_directory(&self, cx: &Cx, path: &Path) -> Result<()> {
3955            self.inner.sync_parent_directory(cx, path)
3956        }
3957
3958        fn access(&self, cx: &Cx, path: &Path, flags: AccessFlags) -> Result<bool> {
3959            self.inner.access(cx, path, flags)
3960        }
3961
3962        fn path_entry_exists(&self, cx: &Cx, path: &Path) -> Result<bool> {
3963            self.inner.path_entry_exists(cx, path)
3964        }
3965
3966        fn full_pathname(&self, cx: &Cx, path: &Path) -> Result<PathBuf> {
3967            self.inner.full_pathname(cx, path)
3968        }
3969
3970        fn randomness(&self, cx: &Cx, buf: &mut [u8]) {
3971            self.inner.randomness(cx, buf);
3972        }
3973
3974        fn current_time(&self, cx: &Cx) -> f64 {
3975            self.inner.current_time(cx)
3976        }
3977
3978        fn is_memory(&self) -> bool {
3979            true
3980        }
3981    }
3982
3983    impl VfsFile for CheckpointHandoffFaultFile {
3984        fn close(&mut self, cx: &Cx) -> Result<()> {
3985            self.inner.close(cx)
3986        }
3987
3988        fn file_identity(&self) -> Result<Option<fsqlite_vfs::FileIdentity>> {
3989            self.inner.file_identity()
3990        }
3991
3992        fn read<'a>(
3993            &'a self,
3994            cx: &'a Cx,
3995            buf: &'a mut [u8],
3996            offset: u64,
3997        ) -> impl std::future::Future<Output = Result<usize>> + Send + 'a {
3998            self.inner.read(cx, buf, offset)
3999        }
4000
4001        async fn write<'a>(&'a self, cx: &'a Cx, buf: &'a [u8], offset: u64) -> Result<()> {
4002            let fault = if self.is_checkpoint_handoff {
4003                self.faults
4004                    .lock()
4005                    .unwrap_or_else(std::sync::PoisonError::into_inner)
4006                    .next_write
4007                    .take()
4008            } else {
4009                None
4010            };
4011            match fault {
4012                Some(CheckpointHandoffWriteFault::Error) => Err(FrankenError::Io(
4013                    std::io::Error::other("injected checkpoint handoff write failure"),
4014                )),
4015                Some(CheckpointHandoffWriteFault::Pending) => {
4016                    std::future::pending::<Result<()>>().await
4017                }
4018                None => self.inner.write(cx, buf, offset).await,
4019            }
4020        }
4021
4022        fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()> {
4023            self.inner.truncate(cx, size)
4024        }
4025
4026        fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()> {
4027            let mut faults = self
4028                .faults
4029                .lock()
4030                .unwrap_or_else(std::sync::PoisonError::into_inner);
4031            if let Some(path) = self.path.as_ref().filter(|path| {
4032                path.as_path() == Path::new(CERTIFICATE_PATH)
4033                    || path.as_path() == Path::new(CHECKPOINT_HANDOFF_PATH)
4034            }) {
4035                faults
4036                    .sync_observations
4037                    .push(CertificateSyncObservation::Ordinary(path.clone()));
4038            }
4039            let fail = self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_sync);
4040            let fail_wal =
4041                !self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_wal_sync);
4042            drop(faults);
4043            if fail {
4044                Err(FrankenError::Io(std::io::Error::other(
4045                    "injected checkpoint handoff sync failure",
4046                )))
4047            } else if fail_wal {
4048                Err(FrankenError::Io(std::io::Error::other(
4049                    "injected WAL sync failure",
4050                )))
4051            } else {
4052                self.inner.sync(cx, flags)
4053            }
4054        }
4055
4056        fn durable_sync(&mut self, cx: &Cx, kind: SyncKind) -> Result<()> {
4057            let mut faults = self
4058                .faults
4059                .lock()
4060                .unwrap_or_else(std::sync::PoisonError::into_inner);
4061            if let Some(path) = self.path.as_ref().filter(|path| {
4062                path.as_path() == Path::new(CERTIFICATE_PATH)
4063                    || path.as_path() == Path::new(CHECKPOINT_HANDOFF_PATH)
4064            }) {
4065                faults
4066                    .sync_observations
4067                    .push(CertificateSyncObservation::Durable(path.clone(), kind));
4068            }
4069            let fail = self.is_checkpoint_handoff && std::mem::take(&mut faults.fail_next_sync);
4070            drop(faults);
4071            if fail {
4072                Err(FrankenError::Io(std::io::Error::other(
4073                    "injected checkpoint handoff durable-sync failure",
4074                )))
4075            } else {
4076                self.inner.durable_sync(cx, kind)
4077            }
4078        }
4079
4080        fn file_size(&self, cx: &Cx) -> Result<u64> {
4081            self.inner.file_size(cx)
4082        }
4083
4084        fn lock(&mut self, cx: &Cx, level: fsqlite_types::LockLevel) -> Result<()> {
4085            self.inner.lock(cx, level)
4086        }
4087
4088        fn unlock(&mut self, cx: &Cx, level: fsqlite_types::LockLevel) -> Result<()> {
4089            self.inner.unlock(cx, level)
4090        }
4091
4092        fn lock_external_shared_snapshot(&mut self, cx: &Cx) -> Result<()> {
4093            self.inner.lock_external_shared_snapshot(cx)
4094        }
4095
4096        fn restore_external_shared_snapshot_attempt(&mut self, cx: &Cx) -> Result<()> {
4097            self.inner.restore_external_shared_snapshot_attempt(cx)
4098        }
4099
4100        fn lock_external_maintenance(&mut self, cx: &Cx, wal_mode: bool) -> Result<()> {
4101            self.inner.lock_external_maintenance(cx, wal_mode)
4102        }
4103
4104        fn restore_external_maintenance_attempt(&mut self, cx: &Cx) -> Result<()> {
4105            self.inner.restore_external_maintenance_attempt(cx)
4106        }
4107
4108        fn check_reserved_lock(&self, cx: &Cx) -> Result<bool> {
4109            self.inner.check_reserved_lock(cx)
4110        }
4111
4112        fn sector_size(&self) -> u32 {
4113            self.inner.sector_size()
4114        }
4115
4116        fn device_characteristics(&self) -> u32 {
4117            self.inner.device_characteristics()
4118        }
4119
4120        fn shm_map(
4121            &mut self,
4122            cx: &Cx,
4123            region: u32,
4124            size: u32,
4125            extend: bool,
4126        ) -> Result<fsqlite_vfs::ShmRegion> {
4127            self.inner.shm_map(cx, region, size, extend)
4128        }
4129
4130        fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()> {
4131            self.inner.shm_lock(cx, offset, n, flags)
4132        }
4133
4134        fn shm_barrier(&self) {
4135            self.inner.shm_barrier();
4136        }
4137
4138        fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()> {
4139            self.inner.shm_unmap(cx, delete)
4140        }
4141
4142        fn set_busy_timeout_ms(&mut self, ms: u64) {
4143            self.inner.set_busy_timeout_ms(ms);
4144        }
4145    }
4146
4147    /// Deliberate no-op (frankensqlite#299).
4148    ///
4149    /// This helper previously installed a process-global `TRACE` subscriber via
4150    /// `tracing_subscriber::fmt()...with_test_writer().try_init()`. `try_init()`
4151    /// is process-wide and first-caller-wins, so the first of the 9 callers
4152    /// changed tracing enablement — and libtest output capture — for every
4153    /// unrelated test running afterwards in this binary, making a later failure
4154    /// replay the whole captured global trace stream.
4155    ///
4156    /// `fsqlite-core` already fixed the identical pattern in b262b6a6 for its
4157    /// other helpers; this one was missed. No caller here asserts on emitted
4158    /// trace events, so the body is simply removed and the call sites are kept
4159    /// so the diff stays test-only.
4160    ///
4161    /// See `wal_publication_tracing_helper_installs_no_global_subscriber`.
4162    fn init_wal_publication_test_tracing() {}
4163
4164    /// frankensqlite#299 regression: the WAL publication tracing helper must not
4165    /// install, or otherwise disturb, a process-global subscriber.
4166    ///
4167    /// Only the equality assertion is made, deliberately. Unlike the pager
4168    /// crate, this test binary contains another global-subscriber installation
4169    /// site outside this file, so an absolute `!has_been_set()` assertion would
4170    /// be order-dependent and could fail for reasons unrelated to this helper.
4171    /// Comparing dispatcher state across the call is untaintable and proves the
4172    /// exact property under test: that this helper is inert.
4173    #[test]
4174    fn wal_publication_tracing_helper_installs_no_global_subscriber() {
4175        let before = tracing::dispatcher::has_been_set();
4176        init_wal_publication_test_tracing();
4177
4178        assert_eq!(
4179            before,
4180            tracing::dispatcher::has_been_set(),
4181            "init_wal_publication_test_tracing must not install or alter a global subscriber"
4182        );
4183    }
4184
4185    fn test_cx() -> Cx {
4186        Cx::default()
4187    }
4188
4189    fn test_salts() -> WalSalts {
4190        WalSalts {
4191            salt1: 0xDEAD_BEEF,
4192            salt2: 0xCAFE_BABE,
4193        }
4194    }
4195
4196    fn sample_page(seed: u8) -> Vec<u8> {
4197        let page_size = usize::try_from(PAGE_SIZE).expect("page size fits usize");
4198        let mut page = vec![0u8; page_size];
4199        for (i, byte) in page.iter_mut().enumerate() {
4200            let reduced = u8::try_from(i % 251).expect("modulo fits u8");
4201            *byte = reduced ^ seed;
4202        }
4203        page
4204    }
4205
4206    fn test_frame_payload_digest(
4207        page_number: u32,
4208        page_data: &[u8],
4209        db_size_if_commit: u32,
4210    ) -> [u8; 32] {
4211        let mut digest = ParallelWalFramePayloadDigestBuilder::new();
4212        digest.update(
4213            PageNumber::new(page_number).expect("test page number must be valid"),
4214            db_size_if_commit,
4215            page_data,
4216        );
4217        digest.finalize()
4218    }
4219
4220    fn sample_certificate(
4221        certificate_epoch: u64,
4222        commit_seq: u64,
4223        lane_record_counts: Vec<u32>,
4224    ) -> ParallelWalCommitCertificate {
4225        let lane_count = u16::try_from(lane_record_counts.len()).expect("test lane count fits u16");
4226        let mut certificate = ParallelWalCommitCertificate {
4227            format_version: fsqlite_wal::PARALLEL_WAL_COMMIT_CERTIFICATE_VERSION,
4228            residue: fsqlite_wal::ParallelWalOrderedResidue::CommitCertificateThenPublish,
4229            certificate_epoch,
4230            commit_seq_lo: fsqlite_types::CommitSeq::new(commit_seq),
4231            commit_seq_hi: fsqlite_types::CommitSeq::new(commit_seq),
4232            durable_segment_epoch: certificate_epoch,
4233            lane_count,
4234            lane_record_counts,
4235            db_size_pages: 1,
4236            page_set_size: 1,
4237            wal_frame_payload_digest: [0xA5; 32],
4238            certificate_crc32c: 0,
4239            fallback_active: false,
4240        };
4241        certificate.certificate_crc32c = certificate.computed_crc32c();
4242        certificate
4243    }
4244
4245    fn make_path_refreshing_backend(
4246        vfs: &MemoryVfs,
4247        cx: &Cx,
4248    ) -> PathRefreshingWalBackend<MemoryVfs> {
4249        let wal = WalFile::create(cx, open_wal_file(vfs, cx), PAGE_SIZE, 0, test_salts())
4250            .expect("create WAL");
4251        PathRefreshingWalBackend::new(
4252            vfs.clone(),
4253            std::path::Path::new("test.db"),
4254            std::path::Path::new("test.db-wal"),
4255            PAGE_SIZE,
4256            wal,
4257            true,
4258            #[cfg(all(feature = "native", any(unix, windows)))]
4259            None,
4260        )
4261    }
4262
4263    fn make_authorized_certificate_backend(
4264        vfs: &MemoryVfs,
4265        cx: &Cx,
4266    ) -> (
4267        PathRefreshingWalBackend<MemoryVfs>,
4268        ParallelWalCommitCertificate,
4269    ) {
4270        let mut backend = make_path_refreshing_backend(vfs, cx);
4271        let committed_page = sample_page(0x44);
4272        let mut certificate = sample_certificate(1, 1, vec![1]);
4273        certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &committed_page, 1);
4274        certificate.certificate_crc32c = certificate.computed_crc32c();
4275        backend
4276            .persist_parallel_wal_commit_certificate(cx, &certificate, 1, 1, true)
4277            .expect("persist authorized certificate");
4278        backend
4279            .append_frame(cx, 1, &committed_page, 1)
4280            .expect("append matching commit marker");
4281        backend.sync(cx).expect("sync matching commit marker");
4282        (backend, certificate)
4283    }
4284
4285    struct AuthoritativeWalSnapshot {
4286        generation: WalGenerationIdentity,
4287        frame_count: usize,
4288        wal_bytes: Vec<u8>,
4289        certificate: ParallelWalCommitCertificate,
4290        committed_page: Vec<u8>,
4291    }
4292
4293    fn make_checkpoint_handoff_fault_backend(
4294        vfs: &CheckpointHandoffFaultVfs,
4295        cx: &Cx,
4296    ) -> (
4297        PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4298        ParallelWalCommitCertificate,
4299        Vec<u8>,
4300    ) {
4301        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
4302        let (file, _) = vfs
4303            .open(cx, Some(Path::new("test.db-wal")), flags)
4304            .expect("open fault-injected WAL file");
4305        let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts())
4306            .expect("create fault-injected WAL");
4307        let mut backend = PathRefreshingWalBackend::new(
4308            vfs.clone(),
4309            Path::new("test.db"),
4310            Path::new("test.db-wal"),
4311            PAGE_SIZE,
4312            wal,
4313            true,
4314            #[cfg(all(feature = "native", any(unix, windows)))]
4315            None,
4316        );
4317        let committed_page = sample_page(0x47);
4318        let mut certificate = sample_certificate(1, 1, vec![1]);
4319        certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &committed_page, 1);
4320        certificate.certificate_crc32c = certificate.computed_crc32c();
4321        backend
4322            .persist_parallel_wal_commit_certificate(cx, &certificate, 1, 1, true)
4323            .expect("persist authorized certificate");
4324        backend
4325            .append_frame(cx, 1, &committed_page, 1)
4326            .expect("append matching commit marker");
4327        backend.sync(cx).expect("sync matching commit marker");
4328        (backend, certificate, committed_page)
4329    }
4330
4331    fn read_fault_injected_wal(vfs: &CheckpointHandoffFaultVfs, cx: &Cx) -> Vec<u8> {
4332        let flags = VfsOpenFlags::READONLY | VfsOpenFlags::WAL;
4333        let (mut file, _) = vfs
4334            .open(cx, Some(Path::new("test.db-wal")), flags)
4335            .expect("open WAL snapshot");
4336        let len = usize::try_from(file.file_size(cx).expect("read WAL size"))
4337            .expect("WAL size fits usize");
4338        let mut bytes = vec![0_u8; len];
4339        assert_eq!(
4340            file.read(cx, &mut bytes, 0).expect("read WAL snapshot"),
4341            len
4342        );
4343        file.close(cx).expect("close WAL snapshot");
4344        bytes
4345    }
4346
4347    fn capture_authoritative_wal(
4348        backend: &PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4349        vfs: &CheckpointHandoffFaultVfs,
4350        cx: &Cx,
4351        certificate: ParallelWalCommitCertificate,
4352        committed_page: Vec<u8>,
4353    ) -> AuthoritativeWalSnapshot {
4354        AuthoritativeWalSnapshot {
4355            generation: backend.inner.inner().generation_identity(),
4356            frame_count: backend.inner.frame_count(),
4357            wal_bytes: read_fault_injected_wal(vfs, cx),
4358            certificate,
4359            committed_page,
4360        }
4361    }
4362
4363    fn assert_authoritative_wal_unchanged(
4364        backend: &mut PathRefreshingWalBackend<CheckpointHandoffFaultVfs>,
4365        vfs: &CheckpointHandoffFaultVfs,
4366        cx: &Cx,
4367        before: &AuthoritativeWalSnapshot,
4368    ) {
4369        assert_eq!(
4370            backend.inner.inner().generation_identity(),
4371            before.generation,
4372            "checkpoint handoff failure must not reset the WAL generation"
4373        );
4374        assert_eq!(
4375            backend.inner.frame_count(),
4376            before.frame_count,
4377            "checkpoint handoff failure must not change the visible frame count"
4378        );
4379        assert_eq!(
4380            read_fault_injected_wal(vfs, cx),
4381            before.wal_bytes,
4382            "checkpoint handoff failure must leave the authoritative WAL byte-for-byte unchanged"
4383        );
4384        assert!(
4385            backend
4386                .inner
4387                .inner()
4388                .read_frame_header(cx, 0)
4389                .expect("read original commit frame")
4390                .is_commit(),
4391            "the original generation's commit marker must remain authoritative"
4392        );
4393        assert_eq!(
4394            backend
4395                .latest_authorized_parallel_wal_commit_certificate(cx)
4396                .expect("recover certificate from unchanged WAL generation"),
4397            Some(before.certificate.clone())
4398        );
4399        assert_eq!(
4400            backend
4401                .read_page(cx, 1)
4402                .expect("read committed page from unchanged WAL generation"),
4403            Some(before.committed_page.clone())
4404        );
4405    }
4406
4407    fn read_certificate_sidecar(vfs: &MemoryVfs, cx: &Cx) -> Vec<u8> {
4408        let path = std::path::Path::new("test.db-wal-cert");
4409        let (mut file, _) = vfs
4410            .open(cx, Some(path), VfsOpenFlags::READONLY | VfsOpenFlags::WAL)
4411            .expect("open certificate sidecar");
4412        let len = usize::try_from(file.file_size(cx).expect("read certificate sidecar size"))
4413            .expect("certificate sidecar size fits usize");
4414        let mut bytes = vec![0_u8; len];
4415        assert_eq!(
4416            file.read(cx, &mut bytes, 0)
4417                .expect("read certificate sidecar"),
4418            len
4419        );
4420        file.close(cx).expect("close certificate sidecar");
4421        bytes
4422    }
4423
4424    fn replace_certificate_sidecar(vfs: &MemoryVfs, cx: &Cx, bytes: &[u8]) {
4425        let path = std::path::Path::new("test.db-wal-cert");
4426        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
4427        let (mut file, _) = vfs
4428            .open(cx, Some(path), flags)
4429            .expect("open mutable certificate sidecar");
4430        file.truncate(cx, 0)
4431            .expect("truncate mutable certificate sidecar");
4432        file.write(cx, bytes, 0)
4433            .expect("replace certificate sidecar bytes");
4434        file.close(cx).expect("close mutable certificate sidecar");
4435    }
4436
4437    fn assert_wal_corrupt<T: std::fmt::Debug>(result: Result<T>, scenario: &str) {
4438        assert!(
4439            matches!(&result, Err(FrankenError::WalCorrupt { .. })),
4440            "{scenario} must fail closed with WalCorrupt, got {result:?}"
4441        );
4442    }
4443
4444    fn sqlite_page_one(encoded_page_size: u16) -> Vec<u8> {
4445        let mut page = sample_page(0x11);
4446        page[..16].copy_from_slice(b"SQLite format 3\0");
4447        page[16..18].copy_from_slice(&encoded_page_size.to_be_bytes());
4448        page
4449    }
4450
4451    fn write_main_db_pages(vfs: &MemoryVfs, cx: &Cx, pages: &[Vec<u8>]) {
4452        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::MAIN_DB;
4453        let (mut file, _) = vfs
4454            .open(cx, Some(std::path::Path::new("test.db")), flags)
4455            .expect("open main database");
4456        file.truncate(cx, 0).expect("truncate main database");
4457        for (index, page) in pages.iter().enumerate() {
4458            let offset = u64::try_from(index)
4459                .expect("page index fits u64")
4460                .saturating_mul(u64::from(PAGE_SIZE));
4461            file.write(cx, page, offset).expect("write database page");
4462        }
4463        file.close(cx).expect("close main database");
4464    }
4465
4466    fn replacement_salts() -> WalSalts {
4467        WalSalts {
4468            salt1: 0x1234_5678,
4469            salt2: 0x9ABC_DEF0,
4470        }
4471    }
4472
4473    fn replace_path_visible_wal(vfs: &MemoryVfs, cx: &Cx) {
4474        let wal_path = std::path::Path::new("test.db-wal");
4475        vfs.delete(cx, wal_path, false)
4476            .expect("remove old path-visible WAL");
4477        let file = open_wal_file(vfs, cx);
4478        WalFile::create(cx, file, PAGE_SIZE, 1, replacement_salts())
4479            .expect("create replacement WAL")
4480            .close(cx)
4481            .expect("close replacement WAL");
4482    }
4483
4484    fn append_replacement_wal_page(
4485        vfs: &MemoryVfs,
4486        cx: &Cx,
4487        page_number: u32,
4488        page: &[u8],
4489        db_size_if_commit: u32,
4490    ) {
4491        let file = open_wal_file(vfs, cx);
4492        let wal = WalFile::open(cx, file).expect("open replacement WAL");
4493        let mut adapter = WalBackendAdapter::new(wal);
4494        adapter
4495            .append_frame(cx, page_number, page, db_size_if_commit)
4496            .expect("append replacement WAL page");
4497        adapter.sync(cx).expect("sync replacement WAL page");
4498        adapter
4499            .into_inner()
4500            .expect("sync drained the staged frames")
4501            .close(cx)
4502            .expect("close replacement WAL");
4503    }
4504
4505    fn make_generation_transition_backend(
4506        vfs: &MemoryVfs,
4507        cx: &Cx,
4508    ) -> (
4509        PathRefreshingWalBackend<MemoryVfs>,
4510        TransactionConflictSnapshot,
4511        Vec<u8>,
4512    ) {
4513        let page_one = sqlite_page_one(u16::try_from(PAGE_SIZE).expect("page size fits u16"));
4514        let page_two = sample_page(0x22);
4515        write_main_db_pages(vfs, cx, &[page_one.clone(), page_two.clone()]);
4516
4517        let file = open_wal_file(vfs, cx);
4518        let wal =
4519            WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create original WAL");
4520        let mut backend = PathRefreshingWalBackend::new(
4521            vfs.clone(),
4522            std::path::Path::new("test.db"),
4523            std::path::Path::new("test.db-wal"),
4524            PAGE_SIZE,
4525            wal,
4526            true,
4527            #[cfg(all(feature = "native", any(unix, windows)))]
4528            None,
4529        );
4530        backend
4531            .append_frame(cx, 1, &page_one, 0)
4532            .expect("append original page 1");
4533        backend
4534            .append_frame(cx, 2, &page_two, 2)
4535            .expect("append original commit");
4536        // Durable-certificate contract: staged frames are unpublished until
4537        // sync; pin the read snapshot AFTER publication so the fixture pins
4538        // the original generation's committed horizon as intended.
4539        backend.sync(cx).expect("publish original commit");
4540        backend
4541            .begin_transaction(cx)
4542            .expect("pin original WAL generation");
4543        let pinned = backend.pinned_read_snapshot().expect("pinned WAL snapshot");
4544        let snapshot = TransactionConflictSnapshot {
4545            generation: pinned.generation,
4546            last_commit_frame: pinned.last_commit_frame,
4547            commit_count: pinned.commit_count,
4548            snapshot_db_size: 0,
4549        };
4550        replace_path_visible_wal(vfs, cx);
4551        (backend, snapshot, page_two)
4552    }
4553
4554    #[test]
4555    fn durable_certificate_sidecar_precedes_and_reconstructs_wal_commit() {
4556        let cx = test_cx();
4557        let vfs = MemoryVfs::new();
4558        let committed_page = sample_page(0x44);
4559        let file = open_wal_file(&vfs, &cx);
4560        let wal = WalFile::create(&cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
4561        let mut backend = PathRefreshingWalBackend::new(
4562            vfs.clone(),
4563            std::path::Path::new("test.db"),
4564            std::path::Path::new("test.db-wal"),
4565            PAGE_SIZE,
4566            wal,
4567            true,
4568            #[cfg(all(feature = "native", any(unix, windows)))]
4569            None,
4570        );
4571        let mut certificate = ParallelWalCommitCertificate {
4572            format_version: fsqlite_wal::PARALLEL_WAL_COMMIT_CERTIFICATE_VERSION,
4573            residue: fsqlite_wal::ParallelWalOrderedResidue::CommitCertificateThenPublish,
4574            certificate_epoch: 1,
4575            commit_seq_lo: fsqlite_types::CommitSeq::new(1),
4576            commit_seq_hi: fsqlite_types::CommitSeq::new(1),
4577            durable_segment_epoch: 1,
4578            lane_count: 1,
4579            lane_record_counts: vec![1],
4580            db_size_pages: 1,
4581            page_set_size: 1,
4582            wal_frame_payload_digest: test_frame_payload_digest(1, &committed_page, 1),
4583            certificate_crc32c: 0,
4584            fallback_active: false,
4585        };
4586        certificate.certificate_crc32c = certificate.computed_crc32c();
4587
4588        backend
4589            .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4590            .expect("persist certificate before WAL commit marker");
4591        assert_eq!(
4592            backend.inner.frame_count(),
4593            0,
4594            "certificate persistence must not itself expose a WAL commit marker"
4595        );
4596
4597        let certificate_path = std::path::Path::new("test.db-wal-cert");
4598        let (mut certificate_file, _) = vfs
4599            .open(
4600                &cx,
4601                Some(certificate_path),
4602                VfsOpenFlags::READONLY | VfsOpenFlags::WAL,
4603            )
4604            .expect("open certificate sidecar");
4605        let certificate_len = usize::try_from(
4606            certificate_file
4607                .file_size(&cx)
4608                .expect("certificate sidecar size"),
4609        )
4610        .expect("certificate sidecar size fits usize");
4611        let mut record_bytes = vec![0_u8; certificate_len];
4612        assert_eq!(
4613            certificate_file
4614                .read(&cx, &mut record_bytes, 0)
4615                .expect("read certificate sidecar"),
4616            certificate_len
4617        );
4618        certificate_file
4619            .close(&cx)
4620            .expect("close certificate sidecar");
4621        let reconstructed = ParallelWalDurableCertificateRecord::from_bytes(&record_bytes)
4622            .expect("reconstruct durable certificate record");
4623        assert_eq!(reconstructed.certificate, certificate);
4624        assert_eq!(reconstructed.wal_frame_start, 1);
4625        assert_eq!(reconstructed.wal_frame_end, 1);
4626        assert_eq!(
4627            reconstructed.wal_generation,
4628            backend.inner.inner().generation_identity()
4629        );
4630        assert!(
4631            !reconstructed.authorizes_wal_boundary(
4632                backend.inner.inner().generation_identity(),
4633                0,
4634                0,
4635                test_frame_payload_digest(1, &committed_page, 1),
4636            ),
4637            "orphan certificate must not authorize visibility before the matching commit marker"
4638        );
4639
4640        backend
4641            .append_frame(&cx, 1, &committed_page, 1)
4642            .expect("append matching WAL commit marker");
4643        backend.sync(&cx).expect("sync WAL commit marker");
4644        assert!(
4645            backend
4646                .inner
4647                .inner()
4648                .read_frame_header(&cx, 0)
4649                .expect("read matching WAL commit frame")
4650                .is_commit()
4651        );
4652        assert!(reconstructed.authorizes_wal_boundary(
4653            backend.inner.inner().generation_identity(),
4654            1,
4655            1,
4656            test_frame_payload_digest(1, &committed_page, 1),
4657        ));
4658
4659        let (mut certificate_file, _) = vfs
4660            .open(
4661                &cx,
4662                Some(certificate_path),
4663                VfsOpenFlags::READWRITE | VfsOpenFlags::WAL,
4664            )
4665            .expect("reopen certificate sidecar");
4666        let torn_offset = certificate_file
4667            .file_size(&cx)
4668            .expect("certificate sidecar size before torn tail");
4669        certificate_file
4670            .write(&cx, &[0xA5], torn_offset)
4671            .expect("append torn footer byte");
4672        certificate_file
4673            .close(&cx)
4674            .expect("close sidecar with torn tail");
4675        let recovered = backend
4676            .latest_authorized_parallel_wal_commit_certificate(&cx)
4677            .wait()
4678            .expect("torn certificate tail should recover the prior valid record")
4679            .expect("prior authorized certificate should remain discoverable");
4680        assert_eq!(recovered, certificate);
4681    }
4682
4683    #[test]
4684    fn content_mismatched_wal_interval_cannot_be_authorized_or_repaired() {
4685        let cx = test_cx();
4686        let vfs = MemoryVfs::new();
4687        let certified_page = sample_page(0x61);
4688        let actual_page = sample_page(0x62);
4689        let mut backend = make_path_refreshing_backend(&vfs, &cx);
4690        let mut certificate = sample_certificate(1, 1, vec![1]);
4691        certificate.wal_frame_payload_digest = test_frame_payload_digest(1, &certified_page, 1);
4692        certificate.certificate_crc32c = certificate.computed_crc32c();
4693
4694        backend
4695            .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4696            .expect("persist content-bound certificate");
4697        backend
4698            .append_frame(&cx, 1, &actual_page, 1)
4699            .expect("append differently valued commit frame");
4700        backend.sync(&cx).expect("sync mismatched commit frame");
4701
4702        let sidecar_before = read_certificate_sidecar(&vfs, &cx);
4703        assert!(
4704            backend
4705                .latest_authorized_parallel_wal_commit_certificate(&cx)
4706                .wait()
4707                .expect("content mismatch is a non-authorizing record")
4708                .is_none(),
4709            "matching generation and commit marker must not authorize different frame bytes"
4710        );
4711
4712        assert_wal_corrupt(
4713            backend
4714                .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
4715                .wait(),
4716            "in-doubt content-bound reconciliation mismatch",
4717        );
4718        assert_eq!(
4719            read_certificate_sidecar(&vfs, &cx),
4720            sidecar_before,
4721            "digest mismatch must be diagnosed before sidecar repair"
4722        );
4723        assert_eq!(
4724            backend.inner.frame_count(),
4725            1,
4726            "digest mismatch must preserve the live WAL for diagnosis and retry"
4727        );
4728    }
4729
4730    #[test]
4731    fn absent_commit_marker_repairs_certificate_and_partial_wal_tail() {
4732        let cx = test_cx();
4733        let vfs = MemoryVfs::new();
4734        let mut backend = make_path_refreshing_backend(&vfs, &cx);
4735        let certificate = sample_certificate(1, 1, vec![1]);
4736        backend
4737            .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4738            .expect("persist orphan certificate");
4739
4740        let (mut tail_writer, _) = vfs
4741            .open(
4742                &cx,
4743                Some(std::path::Path::new("test.db-wal")),
4744                VfsOpenFlags::READWRITE | VfsOpenFlags::WAL,
4745            )
4746            .expect("open WAL for partial-tail injection");
4747        let committed_size = tail_writer.file_size(&cx).expect("read committed WAL size");
4748        tail_writer
4749            .write(&cx, &[0xA5; 7], committed_size)
4750            .expect("inject a partial physical frame");
4751        assert!(
4752            tail_writer.file_size(&cx).expect("read extended WAL size") > committed_size,
4753            "fault fixture must extend the physical WAL"
4754        );
4755        tail_writer.close(&cx).expect("close partial-tail injector");
4756
4757        assert_eq!(
4758            backend
4759                .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
4760                .wait()
4761                .expect("missing commit marker must be exactly repairable"),
4762            ParallelWalCommitReconciliation::NotCommitted
4763        );
4764        assert!(
4765            read_certificate_sidecar(&vfs, &cx).is_empty(),
4766            "matching orphan certificate must be removed after NotCommitted proof"
4767        );
4768        let (mut repaired_wal, _) = vfs
4769            .open(
4770                &cx,
4771                Some(std::path::Path::new("test.db-wal")),
4772                VfsOpenFlags::READONLY | VfsOpenFlags::WAL,
4773            )
4774            .expect("open repaired WAL");
4775        assert_eq!(
4776            repaired_wal.file_size(&cx).expect("read repaired WAL size"),
4777            committed_size,
4778            "NotCommitted reconciliation must truncate the physical partial tail"
4779        );
4780        repaired_wal.close(&cx).expect("close repaired WAL");
4781    }
4782
4783    #[test]
4784    fn durable_certificate_recovery_accepts_every_truncated_record_prefix() {
4785        let cx = test_cx();
4786        let vfs = MemoryVfs::new();
4787        let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
4788        let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4789        let orphan = sample_certificate(2, 2, vec![1]);
4790        let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4791            backend.inner.inner().generation_identity(),
4792            2,
4793            2,
4794            orphan,
4795        )
4796        .expect("construct orphan record")
4797        .to_bytes();
4798
4799        for prefix_len in 1..orphan_bytes.len() {
4800            let mut sidecar = authorized_bytes.clone();
4801            sidecar.extend_from_slice(&orphan_bytes[..prefix_len]);
4802            replace_certificate_sidecar(&vfs, &cx, &sidecar);
4803            let recovered_result = backend
4804                .latest_authorized_parallel_wal_commit_certificate(&cx)
4805                .wait();
4806            assert!(
4807                recovered_result.is_ok(),
4808                "truncated certificate prefix of {prefix_len} bytes must recover: {recovered_result:?}"
4809            );
4810            let recovered = recovered_result
4811                .expect("truncated certificate recovery was asserted successful")
4812                .expect("authorized record must remain discoverable");
4813            assert_eq!(recovered, authorized, "failed at prefix {prefix_len}");
4814        }
4815    }
4816
4817    #[test]
4818    fn durable_certificate_append_repairs_the_accepted_torn_suffix() {
4819        let cx = test_cx();
4820        let vfs = MemoryVfs::new();
4821        let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
4822        let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4823        let orphan = sample_certificate(2, 2, vec![1]);
4824        let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4825            backend.inner.inner().generation_identity(),
4826            2,
4827            2,
4828            orphan.clone(),
4829        )
4830        .expect("construct orphan record")
4831        .to_bytes();
4832        for prefix_len in 1..orphan_bytes.len() {
4833            let mut torn_sidecar = authorized_bytes.clone();
4834            torn_sidecar.extend_from_slice(&orphan_bytes[..prefix_len]);
4835            replace_certificate_sidecar(&vfs, &cx, &torn_sidecar);
4836
4837            assert_eq!(
4838                backend
4839                    .latest_authorized_parallel_wal_commit_certificate(&cx)
4840                    .wait()
4841                    .expect("one torn suffix should recover")
4842                    .expect("authorized predecessor remains visible"),
4843                authorized,
4844                "read recovery failed for prefix {prefix_len}"
4845            );
4846
4847            backend
4848                .persist_parallel_wal_commit_certificate(&cx, &orphan, 2, 2, true)
4849                .expect("next append repairs the torn suffix first");
4850            let repaired_sidecar = read_certificate_sidecar(&vfs, &cx);
4851            assert_eq!(
4852                repaired_sidecar.len(),
4853                authorized_bytes.len() + orphan_bytes.len(),
4854                "replacement record did not start at the prior complete boundary for prefix {prefix_len}"
4855            );
4856            assert_eq!(
4857                backend
4858                    .latest_authorized_parallel_wal_commit_certificate(&cx)
4859                    .wait()
4860                    .expect("orphan lookback crosses the repaired boundary")
4861                    .expect("authorized predecessor remains discoverable"),
4862                authorized,
4863                "orphan lookback failed after repairing prefix {prefix_len}"
4864            );
4865        }
4866
4867        let mut corrupt_record = orphan_bytes;
4868        let envelope_crc_offset =
4869            corrupt_record.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE - 4;
4870        corrupt_record[envelope_crc_offset] ^= 0x80;
4871        let mut corrupt_sidecar = authorized_bytes;
4872        corrupt_sidecar.extend_from_slice(&corrupt_record);
4873        replace_certificate_sidecar(&vfs, &cx, &corrupt_sidecar);
4874        assert_wal_corrupt(
4875            backend
4876                .persist_parallel_wal_commit_certificate(&cx, &orphan, 2, 2, true)
4877                .wait(),
4878            "append-time complete record corruption",
4879        );
4880    }
4881
4882    #[test]
4883    fn durable_certificate_recovery_rejects_complete_corruption_and_garbage() {
4884        let cx = test_cx();
4885        let vfs = MemoryVfs::new();
4886        let (mut backend, _) = make_authorized_certificate_backend(&vfs, &cx);
4887        let authorized_bytes = read_certificate_sidecar(&vfs, &cx);
4888        let orphan = sample_certificate(2, 2, vec![1]);
4889        let orphan_bytes = ParallelWalDurableCertificateRecord::new(
4890            backend.inner.inner().generation_identity(),
4891            2,
4892            2,
4893            orphan,
4894        )
4895        .expect("construct orphan record")
4896        .to_bytes();
4897
4898        let mut bad_crc = orphan_bytes.clone();
4899        let envelope_crc_offset =
4900            bad_crc.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE - 4;
4901        bad_crc[envelope_crc_offset] ^= 0x80;
4902        let mut sidecar = authorized_bytes.clone();
4903        sidecar.extend_from_slice(&bad_crc);
4904        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4905        assert_wal_corrupt(
4906            backend
4907                .latest_authorized_parallel_wal_commit_certificate(&cx)
4908                .wait(),
4909            "complete record with bad CRC",
4910        );
4911
4912        let mut bad_version = orphan_bytes.clone();
4913        bad_version[8] ^= 0x01;
4914        let mut sidecar = authorized_bytes.clone();
4915        sidecar.extend_from_slice(&bad_version);
4916        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4917        assert_wal_corrupt(
4918            backend
4919                .latest_authorized_parallel_wal_commit_certificate(&cx)
4920                .wait(),
4921            "complete record with bad version",
4922        );
4923
4924        let mut bad_magic = orphan_bytes.clone();
4925        bad_magic[0] ^= 0x01;
4926        let mut sidecar = authorized_bytes.clone();
4927        sidecar.extend_from_slice(&bad_magic);
4928        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4929        assert_wal_corrupt(
4930            backend
4931                .latest_authorized_parallel_wal_commit_certificate(&cx)
4932                .wait(),
4933            "complete record with bad magic",
4934        );
4935
4936        let mut bad_footer = orphan_bytes;
4937        let footer_offset =
4938            bad_footer.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
4939        bad_footer[footer_offset] ^= 0x80;
4940        let mut sidecar = authorized_bytes;
4941        sidecar.extend_from_slice(&bad_footer);
4942        replace_certificate_sidecar(&vfs, &cx, &sidecar);
4943        assert_wal_corrupt(
4944            backend
4945                .latest_authorized_parallel_wal_commit_certificate(&cx)
4946                .wait(),
4947            "complete record with bad footer",
4948        );
4949
4950        let garbage_vfs = MemoryVfs::new();
4951        let mut garbage_backend = make_path_refreshing_backend(&garbage_vfs, &cx);
4952        replace_certificate_sidecar(&garbage_vfs, &cx, &[0xA5; 128]);
4953        assert_wal_corrupt(
4954            garbage_backend
4955                .latest_authorized_parallel_wal_commit_certificate(&cx)
4956                .wait(),
4957            "nonempty garbage sidecar",
4958        );
4959
4960        let mut fake_magic = vec![0_u8; MIN_DURABLE_CERTIFICATE_RECORD_SIZE];
4961        fake_magic[..PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC.len()]
4962            .copy_from_slice(&PARALLEL_WAL_DURABLE_CERTIFICATE_MAGIC);
4963        fake_magic[8..10].copy_from_slice(
4964            &fsqlite_wal::PARALLEL_WAL_DURABLE_CERTIFICATE_RECORD_VERSION.to_le_bytes(),
4965        );
4966        let fake_record_len = u32::try_from(fake_magic.len()).expect("fake record length fits u32");
4967        fake_magic[10..14].copy_from_slice(&fake_record_len.to_le_bytes());
4968        let fake_footer_offset =
4969            fake_magic.len() - ParallelWalDurableCertificateRecord::LENGTH_FOOTER_SIZE;
4970        fake_magic[fake_footer_offset..].copy_from_slice(&fake_record_len.to_le_bytes());
4971        replace_certificate_sidecar(&garbage_vfs, &cx, &fake_magic);
4972        assert_wal_corrupt(
4973            garbage_backend
4974                .latest_authorized_parallel_wal_commit_certificate(&cx)
4975                .wait(),
4976            "fake magic and length without a valid envelope",
4977        );
4978    }
4979
4980    #[test]
4981    fn durable_certificate_maximum_size_is_shared_by_writer_and_reader() {
4982        let cx = test_cx();
4983        let vfs = MemoryVfs::new();
4984        let mut backend = make_path_refreshing_backend(&vfs, &cx);
4985        let certificate = sample_certificate(1, 1, vec![1; usize::from(u16::MAX)]);
4986        let record = ParallelWalDurableCertificateRecord::new(
4987            backend.inner.inner().generation_identity(),
4988            1,
4989            1,
4990            certificate.clone(),
4991        )
4992        .expect("construct maximum-size record");
4993        assert_eq!(
4994            record.to_bytes().len(),
4995            PARALLEL_WAL_MAX_DURABLE_CERTIFICATE_RECORD_SIZE
4996        );
4997        backend
4998            .persist_parallel_wal_commit_certificate(&cx, &certificate, 1, 1, true)
4999            .expect("writer accepts maximum-size record");
5000        assert!(
5001            backend
5002                .latest_authorized_parallel_wal_commit_certificate(&cx)
5003                .wait()
5004                .expect("reader accepts maximum-size record")
5005                .is_none(),
5006            "record remains unauthorized until its WAL commit marker exists"
5007        );
5008    }
5009
5010    #[test]
5011    fn durable_certificate_orphan_lookback_allows_exact_boundary_plus_torn_tail() {
5012        let cx = test_cx();
5013        let vfs = MemoryVfs::new();
5014        let (mut backend, authorized) = make_authorized_certificate_backend(&vfs, &cx);
5015        let mut sidecar = read_certificate_sidecar(&vfs, &cx);
5016        // bd-e0ghc contract: only IN-SNAPSHOT invalid records consume the
5017        // bounded orphan budget; records whose frame boundary lies beyond the
5018        // published frame count are futures under concurrent load and are
5019        // skipped budget-free. Exercise the budget with in-snapshot orphans
5020        // (boundary 1,1 — within the published horizon — but content that
5021        // fails authorization against the real commit marker).
5022        for orphan_index in 0..MAX_ORPHAN_CERTIFICATE_LOOKBACK {
5023            let epoch = u64::try_from(orphan_index).expect("orphan index fits u64") + 2;
5024            let orphan = sample_certificate(epoch, epoch, vec![1]);
5025            sidecar.extend_from_slice(
5026                &ParallelWalDurableCertificateRecord::new(
5027                    backend.inner.inner().generation_identity(),
5028                    1,
5029                    1,
5030                    orphan,
5031                )
5032                .expect("construct bounded orphan")
5033                .to_bytes(),
5034            );
5035        }
5036        sidecar.push(0xA5);
5037        replace_certificate_sidecar(&vfs, &cx, &sidecar);
5038        assert_eq!(
5039            backend
5040                .latest_authorized_parallel_wal_commit_certificate(&cx)
5041                .wait()
5042                .expect("64 orphans plus one torn suffix remain within bound")
5043                .expect("authorized predecessor is found"),
5044            authorized
5045        );
5046
5047        sidecar.pop();
5048        let overflow_epoch =
5049            u64::try_from(MAX_ORPHAN_CERTIFICATE_LOOKBACK).expect("lookback fits u64") + 2;
5050        let overflow = sample_certificate(overflow_epoch, overflow_epoch, vec![1]);
5051        sidecar.extend_from_slice(
5052            &ParallelWalDurableCertificateRecord::new(
5053                backend.inner.inner().generation_identity(),
5054                1,
5055                1,
5056                overflow,
5057            )
5058            .expect("construct overflow orphan")
5059            .to_bytes(),
5060        );
5061        replace_certificate_sidecar(&vfs, &cx, &sidecar);
5062        assert_wal_corrupt(
5063            backend
5064                .latest_authorized_parallel_wal_commit_certificate(&cx)
5065                .wait(),
5066            "65 unauthorized records",
5067        );
5068
5069        // Contract-positive twin: FUTURE-boundary records (beyond the
5070        // published frame count) are budget-exempt — 65 of them plus the
5071        // torn tail must still resolve the authorized predecessor.
5072        let mut future_sidecar = read_certificate_sidecar(&vfs, &cx);
5073        future_sidecar.truncate(
5074            future_sidecar.len()
5075                - (MAX_ORPHAN_CERTIFICATE_LOOKBACK + 1)
5076                    * ParallelWalDurableCertificateRecord::new(
5077                        backend.inner.inner().generation_identity(),
5078                        1,
5079                        1,
5080                        sample_certificate(2, 2, vec![1]),
5081                    )
5082                    .expect("sizing record")
5083                    .to_bytes()
5084                    .len(),
5085        );
5086        for future_index in 0..=MAX_ORPHAN_CERTIFICATE_LOOKBACK {
5087            let epoch = u64::try_from(future_index).expect("future index fits u64") + 2;
5088            let future = sample_certificate(epoch, epoch, vec![1]);
5089            future_sidecar.extend_from_slice(
5090                &ParallelWalDurableCertificateRecord::new(
5091                    backend.inner.inner().generation_identity(),
5092                    2,
5093                    2,
5094                    future,
5095                )
5096                .expect("construct future record")
5097                .to_bytes(),
5098            );
5099        }
5100        future_sidecar.push(0xA5);
5101        replace_certificate_sidecar(&vfs, &cx, &future_sidecar);
5102        assert_eq!(
5103            backend
5104                .latest_authorized_parallel_wal_commit_certificate(&cx)
5105                .wait()
5106                .expect("future-boundary records are budget-exempt")
5107                .expect("authorized predecessor is found beneath futures"),
5108            authorized
5109        );
5110    }
5111
5112    #[test]
5113    fn certificate_and_handoff_fences_request_full_durability() {
5114        let cx = test_cx();
5115        let vfs = CheckpointHandoffFaultVfs::new();
5116        let (mut backend, certificate, _) = make_checkpoint_handoff_fault_backend(&vfs, &cx);
5117
5118        assert_eq!(
5119            vfs.take_sync_observations(),
5120            vec![CertificateSyncObservation::Durable(
5121                PathBuf::from(CERTIFICATE_PATH),
5122                SyncKind::FullDurable,
5123            )],
5124            "certificate append must use the strongest durability intent"
5125        );
5126
5127        assert_eq!(
5128            backend
5129                .reconcile_parallel_wal_commit(&cx, &certificate, 1, 1, true)
5130                .wait()
5131                .expect("reconcile committed certificate"),
5132            ParallelWalCommitReconciliation::Authorized
5133        );
5134        assert_eq!(
5135            vfs.take_sync_observations(),
5136            vec![CertificateSyncObservation::Durable(
5137                PathBuf::from(CERTIFICATE_PATH),
5138                SyncKind::FullDurable,
5139            )],
5140            "certificate reconciliation must preserve full durability intent"
5141        );
5142
5143        let record = backend
5144            .latest_authorized_durable_certificate_record(&cx)
5145            .wait()
5146            .expect("read authorized certificate record")
5147            .expect("authorized certificate record must exist");
5148        backend
5149            .persist_checkpoint_certificate_handoff(&cx, &record)
5150            .wait()
5151            .expect("persist checkpoint certificate handoff");
5152        assert_eq!(
5153            vfs.take_sync_observations(),
5154            vec![CertificateSyncObservation::Durable(
5155                PathBuf::from(CHECKPOINT_HANDOFF_PATH),
5156                SyncKind::FullDurable,
5157            )],
5158            "checkpoint handoff must use the strongest durability intent"
5159        );
5160    }
5161
5162    #[test]
5163    fn checkpoint_handoff_write_failure_preserves_authoritative_wal_generation() {
5164        let cx = test_cx();
5165        let vfs = CheckpointHandoffFaultVfs::new();
5166        let (mut backend, certificate, committed_page) =
5167            make_checkpoint_handoff_fault_backend(&vfs, &cx);
5168        let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5169        vfs.fail_next_handoff_write();
5170
5171        let mut checkpoint_writer = MockCheckpointPageWriter;
5172        let error = backend
5173            .checkpoint(
5174                &cx,
5175                CheckpointMode::Truncate,
5176                &mut checkpoint_writer,
5177                0,
5178                None,
5179            )
5180            .expect_err("checkpoint must fail before reset when the handoff write fails");
5181        assert!(
5182            error
5183                .to_string()
5184                .contains("injected checkpoint handoff write failure"),
5185            "unexpected handoff write error: {error}"
5186        );
5187        assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5188    }
5189
5190    #[test]
5191    fn checkpoint_handoff_durable_sync_failure_preserves_authoritative_wal_generation() {
5192        let cx = test_cx();
5193        let vfs = CheckpointHandoffFaultVfs::new();
5194        let (mut backend, certificate, committed_page) =
5195            make_checkpoint_handoff_fault_backend(&vfs, &cx);
5196        let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5197        vfs.fail_next_handoff_sync();
5198
5199        let mut checkpoint_writer = MockCheckpointPageWriter;
5200        let error = backend
5201            .checkpoint(
5202                &cx,
5203                CheckpointMode::Truncate,
5204                &mut checkpoint_writer,
5205                0,
5206                None,
5207            )
5208            .expect_err("checkpoint must fail before reset when the handoff sync fails");
5209        assert!(
5210            error
5211                .to_string()
5212                .contains("injected checkpoint handoff durable-sync failure"),
5213            "unexpected handoff durable-sync error: {error}"
5214        );
5215        assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5216    }
5217
5218    #[test]
5219    fn dropping_pending_checkpoint_handoff_write_preserves_authoritative_wal_generation() {
5220        let cx = test_cx();
5221        let vfs = CheckpointHandoffFaultVfs::new();
5222        let (mut backend, certificate, committed_page) =
5223            make_checkpoint_handoff_fault_backend(&vfs, &cx);
5224        let before = capture_authoritative_wal(&backend, &vfs, &cx, certificate, committed_page);
5225        vfs.pend_next_handoff_write();
5226
5227        let mut checkpoint_writer = MockCheckpointPageWriter;
5228        let reached_pending_handoff = {
5229            let mut checkpoint = backend.checkpoint(
5230                &cx,
5231                CheckpointMode::Truncate,
5232                &mut checkpoint_writer,
5233                0,
5234                None,
5235            );
5236            let mut task_cx = std::task::Context::from_waker(std::task::Waker::noop());
5237            matches!(
5238                std::future::Future::poll(checkpoint.as_mut(), &mut task_cx),
5239                std::task::Poll::Pending
5240            )
5241        };
5242        assert!(
5243            reached_pending_handoff,
5244            "checkpoint should remain pending inside the injected handoff write"
5245        );
5246        assert_authoritative_wal_unchanged(&mut backend, &vfs, &cx, &before);
5247    }
5248
5249    #[test]
5250    fn two_backend_instances_continue_authorized_certificate_clocks() {
5251        let cx = test_cx();
5252        let vfs = MemoryVfs::new();
5253        let wal = WalFile::create(&cx, open_wal_file(&vfs, &cx), PAGE_SIZE, 0, test_salts())
5254            .expect("create shared WAL");
5255        let mut first_backend = PathRefreshingWalBackend::new(
5256            vfs.clone(),
5257            std::path::Path::new("test.db"),
5258            std::path::Path::new("test.db-wal"),
5259            PAGE_SIZE,
5260            wal,
5261            true,
5262            #[cfg(all(feature = "native", any(unix, windows)))]
5263            None,
5264        );
5265        let request =
5266            |batch_id, wal_frame_payload_digest| fsqlite_wal::ParallelWalDurabilityRequest {
5267                trace_id: batch_id,
5268                scenario_id: "two-instance-continuity".to_owned(),
5269                certificate_epoch: 0,
5270                durable_segment_epoch: 0,
5271                batch_size: 1,
5272                batch_ids: vec![batch_id],
5273                lane_record_counts: vec![1],
5274                db_size_pages: 1,
5275                page_set_size: 1,
5276                control_mode: fsqlite_wal::ParallelWalOperatingMode::Auto,
5277                fallback_reason: None,
5278                checkpoint_active: false,
5279                wal_frame_payload_digest,
5280            };
5281
5282        let first_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5283        let first_page = sample_page(0x51);
5284        let first_receipt = first_combiner
5285            .certify_and_publish(
5286                request(1, test_frame_payload_digest(1, &first_page, 1)),
5287                |certificate| {
5288                    first_backend
5289                        .persist_parallel_wal_commit_certificate(&cx, certificate, 1, 1, true)
5290                        .wait()
5291                        .and_then(|()| first_backend.append_frame(&cx, 1, &first_page, 1).wait())
5292                        .and_then(|()| first_backend.sync(&cx))
5293                        .map_err(|error| error.to_string())
5294                },
5295            )
5296            .expect("first backend publishes certificate");
5297
5298        let second_wal =
5299            WalFile::open(&cx, open_wal_file(&vfs, &cx)).expect("second backend opens shared WAL");
5300        let mut second_backend = PathRefreshingWalBackend::new(
5301            vfs.clone(),
5302            std::path::Path::new("test.db"),
5303            std::path::Path::new("test.db-wal"),
5304            PAGE_SIZE,
5305            second_wal,
5306            true,
5307            #[cfg(all(feature = "native", any(unix, windows)))]
5308            None,
5309        );
5310
5311        // Simulate a crash after certificate durability but before its WAL
5312        // commit marker. Bounded tail lookup must step over this well-formed
5313        // orphan and recover the preceding authorized seed.
5314        let orphan_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5315        orphan_combiner
5316            .reconcile_authorized_seed(&first_receipt.certificate)
5317            .expect("seed orphan-producing process");
5318        let orphan_receipt = orphan_combiner
5319            .certify_and_publish(
5320                request(99, test_frame_payload_digest(1, &sample_page(0x52), 1)),
5321                |_| Ok(()),
5322            )
5323            .expect("construct deterministic orphan certificate");
5324        second_backend
5325            .persist_parallel_wal_commit_certificate(&cx, &orphan_receipt.certificate, 2, 2, true)
5326            .expect("persist well-formed orphan certificate tail");
5327        let authorized_seed = second_backend
5328            .latest_authorized_parallel_wal_commit_certificate(&cx)
5329            .expect("second backend performs bounded orphan lookback")
5330            .expect("preceding first certificate remains authorized");
5331        assert_eq!(authorized_seed, first_receipt.certificate);
5332
5333        let second_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5334        second_combiner
5335            .reconcile_authorized_seed(&authorized_seed)
5336            .expect("seed second process-local combiner");
5337        let second_page = sample_page(0x52);
5338        let second_receipt = second_combiner
5339            .certify_and_publish(
5340                request(2, test_frame_payload_digest(1, &second_page, 1)),
5341                |certificate| {
5342                    second_backend
5343                        .persist_parallel_wal_commit_certificate(&cx, certificate, 2, 2, true)
5344                        .wait()
5345                        .and_then(|()| second_backend.append_frame(&cx, 1, &second_page, 1).wait())
5346                        .and_then(|()| second_backend.sync(&cx))
5347                        .map_err(|error| error.to_string())
5348                },
5349            )
5350            .expect("second backend publishes certificate");
5351
5352        assert_eq!(
5353            second_receipt.certificate.commit_seq_lo.get(),
5354            first_receipt.certificate.commit_seq_hi.get() + 1
5355        );
5356        assert_eq!(
5357            second_receipt.certificate.certificate_epoch,
5358            first_receipt.certificate.certificate_epoch + 1
5359        );
5360        assert_eq!(
5361            second_receipt.certificate, orphan_receipt.certificate,
5362            "continuation may reuse an orphan identity but must not overlap any authorized certificate"
5363        );
5364        let latest = second_backend
5365            .latest_authorized_parallel_wal_commit_certificate(&cx)
5366            .expect("read second bounded authorized tail")
5367            .expect("second certificate is authorized");
5368        assert_eq!(latest, second_receipt.certificate);
5369
5370        let generation_before_checkpoint = second_backend.inner.inner().generation_identity();
5371        let mut checkpoint_writer = MockCheckpointPageWriter;
5372        let checkpoint = second_backend
5373            .checkpoint(
5374                &cx,
5375                CheckpointMode::Truncate,
5376                &mut checkpoint_writer,
5377                0,
5378                None,
5379            )
5380            .expect("truncate checkpoint records certificate clock handoff");
5381        assert!(checkpoint.wal_was_reset);
5382        assert_ne!(
5383            second_backend.inner.inner().generation_identity(),
5384            generation_before_checkpoint
5385        );
5386        let checkpoint_seed = second_backend
5387            .latest_authorized_parallel_wal_commit_certificate(&cx)
5388            .expect("read checkpoint certificate clock handoff")
5389            .expect("reset generation retains the last consumed certificate clock");
5390        assert_eq!(checkpoint_seed, second_receipt.certificate);
5391        second_backend
5392            .begin_transaction(&cx)
5393            .expect("pin reset-generation reader snapshot");
5394        let reset_pinned = second_backend
5395            .pinned_read_snapshot()
5396            .expect("reset-generation reader snapshot");
5397        assert_eq!(
5398            reset_pinned.generation,
5399            second_backend.inner.inner().generation_identity(),
5400            "reader snapshot must bind the reset WAL generation"
5401        );
5402        assert_eq!(
5403            reset_pinned.last_commit_frame, None,
5404            "truncate checkpoint leaves no current-generation commit marker"
5405        );
5406        assert_eq!(
5407            second_backend
5408                .pinned_logical_read_snapshot(&cx)
5409                .expect("inspect reset-generation reader horizon"),
5410            None,
5411            "an earlier-generation checkpoint handoff is a clock seed, never reader visibility"
5412        );
5413
5414        let post_checkpoint_combiner = fsqlite_wal::ParallelWalDurabilityCombiner::default();
5415        post_checkpoint_combiner
5416            .reconcile_authorized_seed(&checkpoint_seed)
5417            .expect("seed fresh post-checkpoint combiner");
5418        let post_checkpoint_page = sample_page(0x53);
5419        let post_checkpoint_receipt = post_checkpoint_combiner
5420            .certify_and_publish(
5421                request(3, test_frame_payload_digest(1, &post_checkpoint_page, 1)),
5422                |certificate| {
5423                    second_backend
5424                        .persist_parallel_wal_commit_certificate(&cx, certificate, 1, 1, true)
5425                        .wait()
5426                        .and_then(|()| {
5427                            second_backend
5428                                .append_frame(&cx, 1, &post_checkpoint_page, 1)
5429                                .wait()
5430                        })
5431                        .and_then(|()| second_backend.sync(&cx))
5432                        .map_err(|error| error.to_string())
5433                },
5434            )
5435            .expect("publish first certificate in reset WAL generation");
5436        assert_eq!(
5437            post_checkpoint_receipt.certificate.commit_seq_lo.get(),
5438            second_receipt.certificate.commit_seq_hi.get() + 1
5439        );
5440        assert_eq!(
5441            post_checkpoint_receipt.certificate.certificate_epoch,
5442            second_receipt.certificate.certificate_epoch + 1
5443        );
5444        assert_eq!(
5445            second_backend
5446                .latest_authorized_parallel_wal_commit_certificate(&cx)
5447                .expect("read post-checkpoint current-generation certificate")
5448                .expect("post-checkpoint certificate is authorized"),
5449            post_checkpoint_receipt.certificate
5450        );
5451        second_backend
5452            .begin_transaction(&cx)
5453            .expect("pin post-checkpoint reader snapshot");
5454        let pinned = second_backend
5455            .pinned_read_snapshot()
5456            .expect("post-checkpoint reader snapshot");
5457        let logical = second_backend
5458            .pinned_logical_read_snapshot(&cx)
5459            .expect("inspect post-checkpoint reader horizon")
5460            .expect("current-generation certificate exposes a reader horizon");
5461        assert_eq!(logical.generation, pinned.generation);
5462        assert_eq!(logical.last_commit_frame, pinned.last_commit_frame);
5463        assert_eq!(
5464            logical.visible_commit_seq,
5465            post_checkpoint_receipt.certificate.commit_seq_hi
5466        );
5467    }
5468
5469    #[test]
5470    fn pinned_logical_reader_horizon_counts_physical_tail_after_current_certificate() {
5471        let cx = test_cx();
5472        let vfs = MemoryVfs::new();
5473        let (mut backend, certificate) = make_authorized_certificate_backend(&vfs, &cx);
5474
5475        backend
5476            .begin_transaction(&cx)
5477            .expect("pin certificate reader snapshot");
5478        let initial_pinned = backend
5479            .pinned_read_snapshot()
5480            .expect("initial reader snapshot");
5481        let initial_logical = backend
5482            .pinned_logical_read_snapshot(&cx)
5483            .expect("inspect certificate reader horizon")
5484            .expect("current certificate exposes reader horizon");
5485        assert_eq!(initial_logical.generation, initial_pinned.generation);
5486        assert_eq!(
5487            initial_logical.last_commit_frame,
5488            initial_pinned.last_commit_frame
5489        );
5490        assert_eq!(
5491            initial_logical.visible_commit_seq, certificate.commit_seq_hi,
5492            "certificate horizon is exact when no later physical commit exists"
5493        );
5494
5495        let tail_page = sample_page(0x45);
5496        backend
5497            .append_frame(&cx, 2, &tail_page, 2)
5498            .expect("append later ordinary commit marker");
5499        backend
5500            .sync(&cx)
5501            .expect("sync later ordinary commit marker");
5502        backend
5503            .begin_transaction(&cx)
5504            .expect("repin reader after ordinary tail commit");
5505        let pinned = backend
5506            .pinned_read_snapshot()
5507            .expect("reader snapshot includes ordinary tail commit");
5508        let logical = backend
5509            .pinned_logical_read_snapshot(&cx)
5510            .expect("inspect reader horizon with ordinary tail")
5511            .expect("current certificate remains reader-authoritative");
5512        assert_eq!(logical.generation, pinned.generation);
5513        assert_eq!(logical.last_commit_frame, pinned.last_commit_frame);
5514        assert_eq!(
5515            logical.visible_commit_seq.get(),
5516            certificate.commit_seq_hi.get() + 1
5517        );
5518    }
5519
5520    fn open_wal_file(vfs: &MemoryVfs, cx: &Cx) -> <MemoryVfs as Vfs>::File {
5521        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5522        let (file, _) = vfs
5523            .open(cx, Some(std::path::Path::new("test.db-wal")), flags)
5524            .expect("open WAL file");
5525        file
5526    }
5527
5528    fn make_adapter(vfs: &MemoryVfs, cx: &Cx) -> WalBackendAdapter<<MemoryVfs as Vfs>::File> {
5529        let file = open_wal_file(vfs, cx);
5530        let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5531        WalBackendAdapter::new(wal)
5532    }
5533
5534    /// Adapter backed by the fault VFS so WAL `sync` failures can be injected.
5535    fn make_fault_adapter(
5536        vfs: &CheckpointHandoffFaultVfs,
5537        cx: &Cx,
5538    ) -> WalBackendAdapter<<CheckpointHandoffFaultVfs as Vfs>::File> {
5539        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5540        let (file, _) = vfs
5541            .open(cx, Some(std::path::Path::new("test.db-wal")), flags)
5542            .expect("open fault WAL file");
5543        let wal = WalFile::create(cx, file, PAGE_SIZE, 0, test_salts()).expect("create fault WAL");
5544        WalBackendAdapter::new(wal)
5545    }
5546
5547    // -- WalBackendAdapter tests --
5548
5549    #[test]
5550    fn test_adapter_append_and_frame_count() {
5551        let cx = test_cx();
5552        let vfs = MemoryVfs::new();
5553        let mut adapter = make_adapter(&vfs, &cx);
5554
5555        assert_eq!(adapter.frame_count(), 0);
5556
5557        let page = sample_page(0x42);
5558        adapter
5559            .append_frame(&cx, 1, &page, 0)
5560            .expect("append frame");
5561        assert_eq!(adapter.frame_count(), 1);
5562
5563        adapter
5564            .append_frame(&cx, 2, &sample_page(0x43), 2)
5565            .expect("append commit frame");
5566        assert_eq!(adapter.frame_count(), 2);
5567    }
5568
5569    #[test]
5570    fn test_adapter_read_page_found() {
5571        let cx = test_cx();
5572        let vfs = MemoryVfs::new();
5573        let mut adapter = make_adapter(&vfs, &cx);
5574
5575        let page1 = sample_page(0x10);
5576        let page2 = sample_page(0x20);
5577        adapter.append_frame(&cx, 1, &page1, 0).expect("append");
5578        adapter
5579            .append_frame(&cx, 2, &page2, 2)
5580            .expect("append commit");
5581
5582        // Durable-certificate contract: staged frames are unpublished until
5583        // sync; the backend read path serves only the published horizon.
5584        assert_eq!(
5585            adapter.read_page(&cx, 1).expect("read staged page 1"),
5586            None,
5587            "staged frames must stay invisible before publication"
5588        );
5589        adapter.sync(&cx).expect("publish staged frames");
5590
5591        let result = adapter.read_page(&cx, 1).expect("read page 1");
5592        assert_eq!(result, Some(page1));
5593
5594        let result = adapter.read_page(&cx, 2).expect("read page 2");
5595        assert_eq!(result, Some(page2));
5596    }
5597
5598    #[test]
5599    fn test_adapter_read_page_not_found() {
5600        let cx = test_cx();
5601        let vfs = MemoryVfs::new();
5602        let mut adapter = make_adapter(&vfs, &cx);
5603
5604        adapter
5605            .append_frame(&cx, 1, &sample_page(0x10), 1)
5606            .expect("append");
5607
5608        let result = adapter.read_page(&cx, 99).expect("read missing page");
5609        assert_eq!(result, None);
5610    }
5611
5612    #[test]
5613    fn test_adapter_read_page_returns_latest_version() {
5614        let cx = test_cx();
5615        let vfs = MemoryVfs::new();
5616        let mut adapter = make_adapter(&vfs, &cx);
5617
5618        let old_data = sample_page(0xAA);
5619        let new_data = sample_page(0xBB);
5620
5621        // Write page 5 twice -- the adapter should return the latest.
5622        adapter
5623            .append_frame(&cx, 5, &old_data, 0)
5624            .expect("append old");
5625        adapter
5626            .append_frame(&cx, 5, &new_data, 1)
5627            .expect("append new (commit)");
5628
5629        // Durable-certificate contract: publication (sync) gates visibility.
5630        adapter.sync(&cx).expect("publish staged frames");
5631
5632        let result = adapter.read_page(&cx, 5).expect("read page 5");
5633        assert_eq!(
5634            result,
5635            Some(new_data),
5636            "adapter should return the latest WAL version"
5637        );
5638    }
5639
5640    #[test]
5641    fn test_adapter_refreshes_cross_handle_visibility_and_append_position() {
5642        let cx = test_cx();
5643        let vfs = MemoryVfs::new();
5644
5645        let file1 = open_wal_file(&vfs, &cx);
5646        let wal1 = WalFile::create(&cx, file1, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5647        let mut adapter1 = WalBackendAdapter::new(wal1);
5648
5649        let file2 = open_wal_file(&vfs, &cx);
5650        let wal2 = WalFile::open(&cx, file2).expect("open WAL");
5651        let mut adapter2 = WalBackendAdapter::new(wal2);
5652
5653        let page1 = sample_page(0x11);
5654        adapter1
5655            .append_frame(&cx, 1, &page1, 1)
5656            .expect("adapter1 append commit");
5657        adapter1.sync(&cx).expect("adapter1 sync");
5658        adapter2
5659            .begin_transaction(&cx)
5660            .expect("adapter2 begin transaction");
5661        assert_eq!(
5662            adapter2.read_page(&cx, 1).expect("adapter2 read page1"),
5663            Some(page1.clone()),
5664            "adapter2 should observe adapter1 commit at transaction begin"
5665        );
5666
5667        let page2 = sample_page(0x22);
5668        adapter2
5669            .append_frame(&cx, 2, &page2, 2)
5670            .expect("adapter2 append commit");
5671        adapter2.sync(&cx).expect("adapter2 sync");
5672        adapter1
5673            .begin_transaction(&cx)
5674            .expect("adapter1 begin transaction");
5675        assert_eq!(
5676            adapter1.read_page(&cx, 2).expect("adapter1 read page2"),
5677            Some(page2.clone()),
5678            "adapter1 should observe adapter2 commit at transaction begin"
5679        );
5680
5681        // Ensure the second writer appended to frame 1 (not frame 0 overwrite).
5682        assert_eq!(
5683            adapter1.frame_count(),
5684            2,
5685            "shared WAL should contain both commit frames"
5686        );
5687        assert_eq!(
5688            adapter2.frame_count(),
5689            2,
5690            "shared WAL should contain both commit frames"
5691        );
5692    }
5693
5694    #[test]
5695    fn test_path_refresh_rejects_replacement_wal_page_size_mismatch() {
5696        let cx = test_cx();
5697        let vfs = MemoryVfs::new();
5698        let wal_path = std::path::Path::new("test.db-wal");
5699
5700        let file = open_wal_file(&vfs, &cx);
5701        let wal = WalFile::create(&cx, file, PAGE_SIZE, 0, test_salts()).expect("create WAL");
5702        let mut backend = PathRefreshingWalBackend::new(
5703            vfs.clone(),
5704            std::path::Path::new("test.db"),
5705            wal_path,
5706            PAGE_SIZE,
5707            wal,
5708            true,
5709            #[cfg(all(feature = "native", any(unix, windows)))]
5710            None,
5711        );
5712
5713        backend
5714            .append_frame(&cx, 1, &sample_page(0x31), 1)
5715            .expect("append through live backend");
5716        backend.sync(&cx).expect("sync live backend");
5717
5718        vfs.delete(&cx, wal_path, false)
5719            .expect("remove path-visible WAL");
5720        let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
5721        let (replacement_file, _) = vfs
5722            .open(&cx, Some(wal_path), flags)
5723            .expect("open replacement WAL path");
5724        let replacement_page_size = PAGE_SIZE
5725            .checked_mul(2)
5726            .expect("test replacement page size fits u32");
5727        let replacement_wal = WalFile::create(
5728            &cx,
5729            replacement_file,
5730            replacement_page_size,
5731            0,
5732            test_salts(),
5733        )
5734        .expect("create mismatched replacement WAL");
5735        replacement_wal.close(&cx).expect("close replacement WAL");
5736
5737        let err = backend
5738            .begin_transaction(&cx)
5739            .expect_err("path refresh should reject mismatched WAL page size");
5740        assert!(
5741            matches!(
5742                err,
5743                FrankenError::WalCorrupt { ref detail }
5744                    if detail.contains("does not match database page size")
5745                        && detail.contains("during path refresh")
5746            ),
5747            "unexpected error: {err:?}"
5748        );
5749    }
5750
5751    #[test]
5752    fn test_generation_change_allows_identical_full_page_baseline() {
5753        let cx = test_cx();
5754        let vfs = MemoryVfs::new();
5755        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5756        let baseline = TransactionConflictPageBaseline {
5757            page_number: 2,
5758            page_hash: *blake3::hash(&page_two).as_bytes(),
5759        };
5760
5761        let conflicts = backend
5762            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5763            .expect("validate checkpoint-only generation transition");
5764        assert!(
5765            conflicts.is_empty(),
5766            "byte-identical checkpoint-only reset must not create a false conflict"
5767        );
5768    }
5769
5770    #[test]
5771    fn test_generation_change_rejects_changed_candidate_page() {
5772        let cx = test_cx();
5773        let vfs = MemoryVfs::new();
5774        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5775        let changed_page_two = sample_page(0x33);
5776        write_main_db_pages(
5777            &vfs,
5778            &cx,
5779            &[
5780                sqlite_page_one(u16::try_from(PAGE_SIZE).expect("page size fits u16")),
5781                changed_page_two,
5782            ],
5783        );
5784        let baseline = TransactionConflictPageBaseline {
5785            page_number: 2,
5786            page_hash: *blake3::hash(&page_two).as_bytes(),
5787        };
5788
5789        let conflicts = backend
5790            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5791            .expect("validate changed page across generation transition");
5792        assert_eq!(conflicts, vec![2]);
5793    }
5794
5795    #[test]
5796    fn test_generation_change_rejects_changed_candidate_from_replacement_wal() {
5797        let cx = test_cx();
5798        let vfs = MemoryVfs::new();
5799        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5800        append_replacement_wal_page(&vfs, &cx, 2, &sample_page(0x44), 2);
5801        let baseline = TransactionConflictPageBaseline {
5802            page_number: 2,
5803            page_hash: *blake3::hash(&page_two).as_bytes(),
5804        };
5805
5806        let conflicts = backend
5807            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5808            .expect("replacement WAL page must take precedence over identical main page");
5809        assert_eq!(conflicts, vec![2]);
5810    }
5811
5812    #[test]
5813    fn test_generation_change_rejects_missing_baseline() {
5814        let cx = test_cx();
5815        let vfs = MemoryVfs::new();
5816        let (mut backend, snapshot, _) = make_generation_transition_backend(&vfs, &cx);
5817
5818        let conflicts = backend
5819            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[])
5820            .expect("missing baseline must fail closed");
5821        assert_eq!(conflicts, vec![2]);
5822    }
5823
5824    #[test]
5825    fn test_generation_change_rejects_conflicting_duplicate_baselines() {
5826        let cx = test_cx();
5827        let vfs = MemoryVfs::new();
5828        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5829        let baselines = [
5830            TransactionConflictPageBaseline {
5831                page_number: 2,
5832                page_hash: *blake3::hash(&page_two).as_bytes(),
5833            },
5834            TransactionConflictPageBaseline {
5835                page_number: 2,
5836                page_hash: *blake3::hash(&sample_page(0x55)).as_bytes(),
5837            },
5838        ];
5839
5840        let conflicts = backend
5841            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &baselines)
5842            .expect("conflicting duplicate baselines must fail closed");
5843        assert_eq!(conflicts, vec![2]);
5844    }
5845
5846    #[test]
5847    fn test_generation_change_rejects_short_candidate_page() {
5848        let cx = test_cx();
5849        let vfs = MemoryVfs::new();
5850        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5851        write_main_db_pages(
5852            &vfs,
5853            &cx,
5854            &[sqlite_page_one(
5855                u16::try_from(PAGE_SIZE).expect("page size fits u16"),
5856            )],
5857        );
5858        let baseline = TransactionConflictPageBaseline {
5859            page_number: 2,
5860            page_hash: *blake3::hash(&page_two).as_bytes(),
5861        };
5862
5863        let conflicts = backend
5864            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5865            .expect("short page must fail closed");
5866        assert_eq!(conflicts, vec![2]);
5867    }
5868
5869    #[test]
5870    fn test_generation_change_rejects_database_page_size_change() {
5871        let cx = test_cx();
5872        let vfs = MemoryVfs::new();
5873        let (mut backend, snapshot, page_two) = make_generation_transition_backend(&vfs, &cx);
5874        write_main_db_pages(&vfs, &cx, &[sqlite_page_one(8192), page_two.clone()]);
5875        let baseline = TransactionConflictPageBaseline {
5876            page_number: 2,
5877            page_hash: *blake3::hash(&page_two).as_bytes(),
5878        };
5879
5880        let conflicts = backend
5881            .conflicting_pages_since_snapshot(&cx, snapshot, &[2], &[baseline])
5882            .expect("page-size change must fail closed");
5883        assert_eq!(conflicts, vec![2]);
5884    }
5885
5886    #[test]
5887    fn test_generation_change_decodes_64k_database_header_sentinel() {
5888        assert_eq!(
5889            sqlite_database_header_page_size(&sqlite_page_one(1)),
5890            Some(65_536)
5891        );
5892    }
5893
5894    #[test]
5895    fn test_adapter_batch_append_checksum_chain_matches_single_append() {
5896        let cx = test_cx();
5897        let vfs_single = MemoryVfs::new();
5898        let vfs_batch = MemoryVfs::new();
5899
5900        let mut adapter_single = make_adapter(&vfs_single, &cx);
5901        let mut adapter_batch = make_adapter(&vfs_batch, &cx);
5902
5903        let pages: Vec<Vec<u8>> = (0..4u8).map(sample_page).collect();
5904        let commit_sizes = [0_u32, 0, 0, 4];
5905
5906        for (index, page) in pages.iter().enumerate() {
5907            adapter_single
5908                .append_frame(
5909                    &cx,
5910                    u32::try_from(index + 1).expect("page number fits u32"),
5911                    page,
5912                    commit_sizes[index],
5913                )
5914                .expect("single append");
5915        }
5916
5917        let batch_frames: Vec<_> = pages
5918            .iter()
5919            .enumerate()
5920            .map(|(index, page)| WalFrameRef {
5921                page_number: u32::try_from(index + 1).expect("page number fits u32"),
5922                page_data: page,
5923                db_size_if_commit: commit_sizes[index],
5924            })
5925            .collect();
5926        adapter_batch
5927            .append_frames(&cx, &batch_frames)
5928            .expect("batch append");
5929
5930        assert_eq!(
5931            adapter_single.frame_count(),
5932            adapter_batch.frame_count(),
5933            "batch adapter append must preserve frame count"
5934        );
5935        assert_eq!(
5936            adapter_single.wal.running_checksum(),
5937            adapter_batch.wal.running_checksum(),
5938            "batch adapter append must preserve checksum chain"
5939        );
5940
5941        for frame_index in 0..pages.len() {
5942            let (single_header, single_data) = adapter_single
5943                .wal
5944                .read_frame(&cx, frame_index)
5945                .expect("read single frame");
5946            let (batch_header, batch_data) = adapter_batch
5947                .wal
5948                .read_frame(&cx, frame_index)
5949                .expect("read batch frame");
5950            assert_eq!(
5951                single_header, batch_header,
5952                "frame header {frame_index} must match"
5953            );
5954            assert_eq!(
5955                single_data, batch_data,
5956                "frame payload {frame_index} must match"
5957            );
5958        }
5959    }
5960
5961    #[test]
5962    fn test_adapter_prepared_batch_append_checksum_chain_matches_single_append() {
5963        let cx = test_cx();
5964        let vfs_single = MemoryVfs::new();
5965        let vfs_prepared = MemoryVfs::new();
5966
5967        let mut adapter_single = make_adapter(&vfs_single, &cx);
5968        let mut adapter_prepared = make_adapter(&vfs_prepared, &cx);
5969
5970        let pages: Vec<Vec<u8>> = (0..4u8).map(sample_page).collect();
5971        let commit_sizes = [0_u32, 0, 0, 4];
5972
5973        for (index, page) in pages.iter().enumerate() {
5974            adapter_single
5975                .append_frame(
5976                    &cx,
5977                    u32::try_from(index + 1).expect("page number fits u32"),
5978                    page,
5979                    commit_sizes[index],
5980                )
5981                .expect("single append");
5982        }
5983
5984        let batch_frames: Vec<_> = pages
5985            .iter()
5986            .enumerate()
5987            .map(|(index, page)| WalFrameRef {
5988                page_number: u32::try_from(index + 1).expect("page number fits u32"),
5989                page_data: page,
5990                db_size_if_commit: commit_sizes[index],
5991            })
5992            .collect();
5993        let mut prepared = adapter_prepared
5994            .prepare_append_frames(&batch_frames)
5995            .expect("prepare append")
5996            .expect("prepared batch");
5997        adapter_prepared
5998            .append_prepared_frames(&cx, &mut prepared)
5999            .expect("append prepared");
6000
6001        assert_eq!(
6002            adapter_single.frame_count(),
6003            adapter_prepared.frame_count(),
6004            "prepared adapter append must preserve frame count"
6005        );
6006        assert_eq!(
6007            adapter_single.wal.running_checksum(),
6008            adapter_prepared.wal.running_checksum(),
6009            "prepared adapter append must preserve checksum chain"
6010        );
6011
6012        for frame_index in 0..pages.len() {
6013            let (single_header, single_data) = adapter_single
6014                .wal
6015                .read_frame(&cx, frame_index)
6016                .expect("read single frame");
6017            let (prepared_header, prepared_data) = adapter_prepared
6018                .wal
6019                .read_frame(&cx, frame_index)
6020                .expect("read prepared frame");
6021            assert_eq!(
6022                single_header, prepared_header,
6023                "frame header {frame_index} must match"
6024            );
6025            assert_eq!(
6026                single_data, prepared_data,
6027                "frame payload {frame_index} must match"
6028            );
6029        }
6030    }
6031
6032    #[test]
6033    fn test_adapter_pre_finalize_reused_when_append_window_is_stable() {
6034        let cx = test_cx();
6035        let vfs_single = MemoryVfs::new();
6036        let vfs_prepared = MemoryVfs::new();
6037
6038        let mut adapter_single = make_adapter(&vfs_single, &cx);
6039        let mut adapter_prepared = make_adapter(&vfs_prepared, &cx);
6040
6041        let pages: Vec<Vec<u8>> = (0..3u8).map(sample_page).collect();
6042        let commit_sizes = [0_u32, 0, 3];
6043
6044        for (index, page) in pages.iter().enumerate() {
6045            adapter_single
6046                .append_frame(
6047                    &cx,
6048                    u32::try_from(index + 1).expect("page number fits u32"),
6049                    page,
6050                    commit_sizes[index],
6051                )
6052                .expect("single append");
6053        }
6054
6055        let batch_frames: Vec<_> = pages
6056            .iter()
6057            .enumerate()
6058            .map(|(index, page)| WalFrameRef {
6059                page_number: u32::try_from(index + 1).expect("page number fits u32"),
6060                page_data: page,
6061                db_size_if_commit: commit_sizes[index],
6062            })
6063            .collect();
6064        let mut prepared = adapter_prepared
6065            .prepare_append_frames(&batch_frames)
6066            .expect("prepare append")
6067            .expect("prepared batch");
6068        adapter_prepared
6069            .finalize_prepared_frames(&cx, &mut prepared)
6070            .expect("pre-finalize prepared batch");
6071        let finalized_for = prepared.finalized_for.expect("finalization state");
6072        let finalized_running_checksum = prepared
6073            .finalized_running_checksum
6074            .expect("finalized checksum");
6075
6076        adapter_prepared
6077            .append_prepared_frames(&cx, &mut prepared)
6078            .expect("append prepared");
6079
6080        assert_eq!(
6081            prepared.finalized_for,
6082            Some(finalized_for),
6083            "stable append window should reuse the pre-lock finalization state"
6084        );
6085        assert_eq!(
6086            prepared.finalized_running_checksum,
6087            Some(finalized_running_checksum),
6088            "stable append window should reuse the pre-lock finalized checksum"
6089        );
6090        assert_eq!(
6091            adapter_single.wal.running_checksum(),
6092            adapter_prepared.wal.running_checksum(),
6093            "stable reuse path must preserve checksum chain"
6094        );
6095    }
6096
6097    #[test]
6098    fn test_adapter_pre_finalize_reseeds_after_intervening_external_append() {
6099        let cx = test_cx();
6100        let baseline_vfs = MemoryVfs::new();
6101        let shared_vfs = MemoryVfs::new();
6102
6103        let mut baseline = make_adapter(&baseline_vfs, &cx);
6104        let mut prepared_writer = make_adapter(&shared_vfs, &cx);
6105        let intruder_file = open_wal_file(&shared_vfs, &cx);
6106        let intruder_wal = WalFile::open(&cx, intruder_file).expect("open shared WAL");
6107        let mut intruder = WalBackendAdapter::new(intruder_wal);
6108
6109        let pages: Vec<Vec<u8>> = (0..3u8).map(sample_page).collect();
6110        let commit_sizes = [0_u32, 0, 3];
6111        let intruder_page = sample_page(0xEE);
6112
6113        baseline
6114            .append_frame(&cx, 99, &intruder_page, 1)
6115            .expect("baseline intruder append");
6116        for (index, page) in pages.iter().enumerate() {
6117            baseline
6118                .append_frame(
6119                    &cx,
6120                    u32::try_from(index + 1).expect("page number fits u32"),
6121                    page,
6122                    commit_sizes[index],
6123                )
6124                .expect("baseline append");
6125        }
6126
6127        let batch_frames: Vec<_> = pages
6128            .iter()
6129            .enumerate()
6130            .map(|(index, page)| WalFrameRef {
6131                page_number: u32::try_from(index + 1).expect("page number fits u32"),
6132                page_data: page,
6133                db_size_if_commit: commit_sizes[index],
6134            })
6135            .collect();
6136        let mut prepared = prepared_writer
6137            .prepare_append_frames(&batch_frames)
6138            .expect("prepare append")
6139            .expect("prepared batch");
6140        prepared_writer
6141            .finalize_prepared_frames(&cx, &mut prepared)
6142            .expect("pre-finalize prepared batch");
6143        let stale_finalization_state = prepared.finalized_for;
6144
6145        intruder
6146            .append_frame(&cx, 99, &intruder_page, 1)
6147            .expect("intruder append");
6148        intruder.sync(&cx).expect("intruder sync");
6149
6150        prepared_writer
6151            .append_prepared_frames(&cx, &mut prepared)
6152            .expect("append prepared after external growth");
6153
6154        assert_ne!(
6155            prepared.finalized_for, stale_finalization_state,
6156            "intervening external growth should force prepared batch reseeding"
6157        );
6158        assert_eq!(
6159            baseline.wal.running_checksum(),
6160            prepared_writer.wal.running_checksum(),
6161            "reseeding path must preserve checksum chain"
6162        );
6163        assert_eq!(
6164            baseline.frame_count(),
6165            prepared_writer.frame_count(),
6166            "reseeding path must preserve frame count"
6167        );
6168    }
6169
6170    #[test]
6171    fn test_adapter_pins_read_snapshot_until_next_begin() {
6172        init_wal_publication_test_tracing();
6173        let cx = test_cx();
6174        let vfs = MemoryVfs::new();
6175
6176        let file_writer = open_wal_file(&vfs, &cx);
6177        let wal_writer =
6178            WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6179        let mut writer = WalBackendAdapter::new(wal_writer);
6180
6181        let file_reader = open_wal_file(&vfs, &cx);
6182        let wal_reader = WalFile::open(&cx, file_reader).expect("open WAL");
6183        let mut reader = WalBackendAdapter::new(wal_reader);
6184
6185        let v1 = sample_page(0x41);
6186        writer.append_frame(&cx, 3, &v1, 3).expect("append v1");
6187        writer.sync(&cx).expect("sync v1");
6188
6189        reader
6190            .begin_transaction(&cx)
6191            .expect("begin reader snapshot 1");
6192        let pinned_v1 = reader
6193            .pinned_read_snapshot()
6194            .expect("reader pins publication snapshot");
6195        assert_eq!(pinned_v1.last_commit_frame, Some(0));
6196        assert_eq!(pinned_v1.commit_count, 1);
6197        assert_eq!(pinned_v1.latest_frame_entries, 1);
6198        assert!(pinned_v1.lookup_contract_is_authoritative());
6199        assert_eq!(
6200            reader.read_page(&cx, 3).expect("reader sees v1"),
6201            Some(v1.clone())
6202        );
6203
6204        let v2 = sample_page(0x42);
6205        writer.append_frame(&cx, 3, &v2, 3).expect("append v2");
6206        writer.sync(&cx).expect("sync v2");
6207
6208        // Same transaction snapshot must stay stable (no mid-transaction drift).
6209        assert_eq!(
6210            reader
6211                .read_page(&cx, 3)
6212                .expect("reader remains on pinned snapshot"),
6213            Some(v1.clone())
6214        );
6215        assert_eq!(
6216            reader
6217                .pinned_read_snapshot()
6218                .expect("reader keeps the same pinned snapshot"),
6219            pinned_v1,
6220            "pinned publication metadata must stay stable until the next begin"
6221        );
6222
6223        // A new transaction snapshot should pick up the latest commit.
6224        reader
6225            .begin_transaction(&cx)
6226            .expect("begin reader snapshot 2");
6227        let pinned_v2 = reader
6228            .pinned_read_snapshot()
6229            .expect("reader repins publication snapshot");
6230        assert!(pinned_v2.publication_seq > pinned_v1.publication_seq);
6231        assert_eq!(pinned_v2.commit_count, 2);
6232        assert_eq!(pinned_v2.latest_frame_entries, 1);
6233        assert_eq!(reader.read_page(&cx, 3).expect("reader sees v2"), Some(v2));
6234    }
6235
6236    #[test]
6237    fn test_adapter_read_page_hides_uncommitted_frames() {
6238        let cx = test_cx();
6239        let vfs = MemoryVfs::new();
6240        let mut adapter = make_adapter(&vfs, &cx);
6241
6242        let committed = sample_page(0x31);
6243        let uncommitted = sample_page(0x32);
6244
6245        adapter
6246            .append_frame(&cx, 7, &committed, 7)
6247            .expect("append committed frame");
6248        // Publish the committed frame; the tail frame appended after the
6249        // publication stays staged AND uncommitted.
6250        adapter.sync(&cx).expect("publish committed frame");
6251        adapter
6252            .append_frame(&cx, 7, &uncommitted, 0)
6253            .expect("append uncommitted frame");
6254
6255        let result = adapter.read_page(&cx, 7).expect("read committed page");
6256        assert_eq!(
6257            result,
6258            Some(committed),
6259            "reader must ignore uncommitted (and unpublished) tail frames"
6260        );
6261    }
6262
6263    #[test]
6264    fn test_adapter_read_page_none_when_wal_has_no_commit_frame() {
6265        let cx = test_cx();
6266        let vfs = MemoryVfs::new();
6267        let mut adapter = make_adapter(&vfs, &cx);
6268
6269        adapter
6270            .append_frame(&cx, 3, &sample_page(0x44), 0)
6271            .expect("append uncommitted frame");
6272
6273        let result = adapter.read_page(&cx, 3).expect("read page");
6274        assert_eq!(result, None, "uncommitted WAL frames must stay invisible");
6275    }
6276
6277    #[test]
6278    fn test_adapter_read_page_empty_wal() {
6279        let cx = test_cx();
6280        let vfs = MemoryVfs::new();
6281        let mut adapter = make_adapter(&vfs, &cx);
6282
6283        let result = adapter.read_page(&cx, 1).expect("read from empty WAL");
6284        assert_eq!(result, None);
6285    }
6286
6287    #[test]
6288    fn test_adapter_sync() {
6289        let cx = test_cx();
6290        let vfs = MemoryVfs::new();
6291        let mut adapter = make_adapter(&vfs, &cx);
6292
6293        adapter
6294            .append_frame(&cx, 1, &sample_page(0), 1)
6295            .expect("append");
6296        adapter.sync(&cx).expect("sync should not fail");
6297    }
6298
6299    #[test]
6300    fn test_adapter_into_inner_fails_closed_until_sync() {
6301        let cx = test_cx();
6302        let staged_vfs = MemoryVfs::new();
6303        let mut staged = make_adapter(&staged_vfs, &cx);
6304
6305        staged
6306            .append_frame(&cx, 1, &sample_page(0), 1)
6307            .expect("append");
6308        assert!(
6309            matches!(staged.into_inner(), Err(FrankenError::Busy)),
6310            "an unsynced commit must prevent consuming the adapter"
6311        );
6312
6313        let synced_vfs = MemoryVfs::new();
6314        let mut synced = make_adapter(&synced_vfs, &cx);
6315        synced
6316            .append_frame(&cx, 1, &sample_page(0), 1)
6317            .expect("append");
6318        synced.sync(&cx).expect("sync staged commit");
6319
6320        assert_eq!(synced.inner().frame_count(), 1);
6321
6322        let wal = synced.into_inner().expect("sync drained the staged frames");
6323        assert_eq!(wal.frame_count(), 1);
6324    }
6325
6326    #[test]
6327    fn test_adapter_as_dyn_wal_backend() {
6328        let cx = test_cx();
6329        let vfs = MemoryVfs::new();
6330        let mut adapter = make_adapter(&vfs, &cx);
6331
6332        // Verify it can be used as a trait object.
6333        let backend: &mut dyn WalBackend = &mut adapter;
6334        backend
6335            .append_frame(&cx, 1, &sample_page(0x77), 1)
6336            .expect("append via dyn");
6337        assert_eq!(backend.frame_count(), 1);
6338
6339        // Durable-certificate contract: publication gates dyn reads too.
6340        backend.sync(&cx).expect("publish via dyn");
6341        let page = backend.read_page(&cx, 1).expect("read via dyn");
6342        assert_eq!(page, Some(sample_page(0x77)));
6343    }
6344
6345    #[test]
6346    fn test_publication_snapshots_are_visible_through_wal_backend_trait() {
6347        init_wal_publication_test_tracing();
6348        let cx = test_cx();
6349        let vfs = MemoryVfs::new();
6350
6351        let file_writer = open_wal_file(&vfs, &cx);
6352        let wal_writer =
6353            WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6354        let mut writer = WalBackendAdapter::new(wal_writer);
6355
6356        writer
6357            .append_frame(&cx, 4, &sample_page(0x84), 4)
6358            .expect("append committed frame");
6359        writer.sync(&cx).expect("sync committed frame");
6360
6361        let file_reader = open_wal_file(&vfs, &cx);
6362        let wal_reader = WalFile::open(&cx, file_reader).expect("open WAL");
6363        let mut reader = WalBackendAdapter::new(wal_reader);
6364        let backend: &mut dyn WalBackend = &mut reader;
6365
6366        let published_before = backend
6367            .published_snapshot()
6368            .expect("trait should expose the adapter publication summary");
6369        assert_eq!(published_before.last_commit_frame, None);
6370        assert_eq!(published_before.commit_count, 0);
6371
6372        let refreshed = backend
6373            .refresh_published_snapshot(&cx)
6374            .expect("refresh through trait should succeed")
6375            .expect("adapter should republish an existing committed prefix");
6376        assert_eq!(refreshed.last_commit_frame, Some(0));
6377        assert_eq!(refreshed.commit_count, 1);
6378        assert_eq!(refreshed.latest_frame_entries, 1);
6379
6380        backend
6381            .begin_transaction(&cx)
6382            .expect("begin_transaction through trait should pin snapshot");
6383        let pinned = backend
6384            .pinned_read_snapshot()
6385            .expect("trait should expose the pinned read snapshot");
6386        assert_eq!(pinned, refreshed);
6387    }
6388
6389    // -- Page index O(1) lookup tests --
6390
6391    #[test]
6392    fn test_page_index_returns_correct_data() {
6393        // Write several pages, verify O(1) index returns the right data.
6394        let cx = test_cx();
6395        let vfs = MemoryVfs::new();
6396        let mut adapter = make_adapter(&vfs, &cx);
6397
6398        let page1 = sample_page(0x01);
6399        let page2 = sample_page(0x02);
6400        let page3 = sample_page(0x03);
6401
6402        adapter.append_frame(&cx, 1, &page1, 0).expect("append");
6403        adapter.append_frame(&cx, 2, &page2, 0).expect("append");
6404        adapter
6405            .append_frame(&cx, 3, &page3, 3)
6406            .expect("append commit");
6407        adapter.sync(&cx).expect("publish staged frames");
6408
6409        // All three pages should be readable via the index.
6410        assert_eq!(adapter.read_page(&cx, 1).expect("read"), Some(page1));
6411        assert_eq!(adapter.read_page(&cx, 2).expect("read"), Some(page2));
6412        assert_eq!(adapter.read_page(&cx, 3).expect("read"), Some(page3));
6413
6414        // Non-existent page returns None.
6415        assert_eq!(adapter.read_page(&cx, 99).expect("read"), None);
6416    }
6417
6418    #[test]
6419    fn test_page_index_returns_latest_version() {
6420        // Write the same page twice; the index should point to the newer frame.
6421        let cx = test_cx();
6422        let vfs = MemoryVfs::new();
6423        let mut adapter = make_adapter(&vfs, &cx);
6424
6425        let old_data = sample_page(0xAA);
6426        let new_data = sample_page(0xBB);
6427
6428        adapter
6429            .append_frame(&cx, 5, &old_data, 0)
6430            .expect("append old");
6431        adapter
6432            .append_frame(&cx, 5, &new_data, 1)
6433            .expect("append new (commit)");
6434        adapter.sync(&cx).expect("publish staged frames");
6435
6436        assert_eq!(
6437            adapter.read_page(&cx, 5).expect("read"),
6438            Some(new_data),
6439            "page index must return the latest frame for a page"
6440        );
6441    }
6442
6443    #[test]
6444    fn test_page_index_invalidated_on_wal_reset() {
6445        // Simulate a WAL reset with new salts. The index must be rebuilt so
6446        // stale entries from the old generation are not returned.
6447        let cx = test_cx();
6448        let vfs = MemoryVfs::new();
6449        let mut adapter = make_adapter(&vfs, &cx);
6450
6451        let old_data = sample_page(0x11);
6452        adapter
6453            .append_frame(&cx, 1, &old_data, 1)
6454            .expect("append commit");
6455        adapter.sync(&cx).expect("publish staged frames");
6456
6457        // Read page 1 to populate the index.
6458        assert_eq!(adapter.read_page(&cx, 1).expect("read old"), Some(old_data));
6459
6460        // Reset WAL with new salts (simulates checkpoint reset).
6461        let new_salts = WalSalts {
6462            salt1: 0xAAAA_BBBB,
6463            salt2: 0xCCCC_DDDD,
6464        };
6465        adapter
6466            .inner_mut()
6467            .expect("no staged batch blocks inner access")
6468            .reset(&cx, 1, new_salts, false)
6469            .expect("WAL reset");
6470
6471        // Write new data for the same page number in the new generation.
6472        let new_data = sample_page(0x22);
6473        adapter
6474            .append_frame(&cx, 1, &new_data, 1)
6475            .expect("append new generation commit");
6476        adapter.sync(&cx).expect("publish new generation commit");
6477
6478        // The index must have been invalidated; we should get the new data.
6479        let result = adapter.read_page(&cx, 1).expect("read after reset");
6480        assert_eq!(
6481            result,
6482            Some(new_data),
6483            "after WAL reset, page index must return new-generation data, not stale cached data"
6484        );
6485
6486        // A page that existed only in the old generation should be gone.
6487        let old_only = sample_page(0x33);
6488        // (We never wrote page 99 in the new generation.)
6489        assert_eq!(
6490            adapter.read_page(&cx, 99).expect("read non-existent"),
6491            None,
6492            "pages from old WAL generation must not appear after reset"
6493        );
6494        // Suppress unused variable warning.
6495        drop(old_only);
6496    }
6497
6498    #[test]
6499    fn test_page_index_invalidated_on_same_salt_generation_change() {
6500        init_wal_publication_test_tracing();
6501        // Generation identity must include checkpoint_seq. Reusing salts across
6502        // reset must still invalidate the cached page index and avoid ABA bugs.
6503        let cx = test_cx();
6504        let vfs = MemoryVfs::new();
6505        let mut adapter = make_adapter(&vfs, &cx);
6506
6507        let reused_salts = adapter.inner().header().salts;
6508        let old_data = sample_page(0x11);
6509        adapter
6510            .append_frame(&cx, 1, &old_data, 1)
6511            .expect("append commit");
6512        adapter.sync(&cx).expect("publish staged frames");
6513        assert_eq!(adapter.read_page(&cx, 1).expect("read old"), Some(old_data));
6514
6515        adapter
6516            .inner_mut()
6517            .expect("no staged batch blocks inner access")
6518            .reset(&cx, 1, reused_salts, false)
6519            .expect("reset with same salts");
6520        let new_data = sample_page(0x22);
6521        adapter
6522            .append_frame(&cx, 2, &new_data, 2)
6523            .expect("append new generation commit");
6524        adapter.sync(&cx).expect("publish new generation commit");
6525        let refreshed = adapter
6526            .refresh_published_snapshot(&cx)
6527            .expect("refresh published snapshot after same-salt reset");
6528        assert_eq!(refreshed.generation.checkpoint_seq, 1);
6529        assert_eq!(refreshed.generation.salts, reused_salts);
6530        assert_eq!(refreshed.last_commit_frame, Some(0));
6531        assert_eq!(refreshed.commit_count, 1);
6532        assert_eq!(refreshed.latest_frame_entries, 1);
6533
6534        assert_eq!(
6535            adapter.read_page(&cx, 1).expect("old page should be gone"),
6536            None,
6537            "cached index entries from the previous generation must be invalidated"
6538        );
6539        assert_eq!(
6540            adapter.read_page(&cx, 2).expect("read new page"),
6541            Some(new_data),
6542            "adapter must resolve pages from the new generation even when salts are reused"
6543        );
6544    }
6545
6546    #[test]
6547    fn test_refresh_published_snapshot_materializes_existing_committed_prefix() {
6548        init_wal_publication_test_tracing();
6549        let cx = test_cx();
6550        let vfs = MemoryVfs::new();
6551
6552        let file_writer = open_wal_file(&vfs, &cx);
6553        let wal_writer =
6554            WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
6555        let mut writer = WalBackendAdapter::new(wal_writer);
6556
6557        let p1 = sample_page(0x71);
6558        let p2 = sample_page(0x72);
6559        writer.append_frame(&cx, 1, &p1, 0).expect("append p1");
6560        writer
6561            .append_frame(&cx, 2, &p2, 2)
6562            .expect("append p2 commit");
6563        writer.sync(&cx).expect("sync writer");
6564
6565        let file_reader = open_wal_file(&vfs, &cx);
6566        let wal_reader = WalFile::open(&cx, file_reader).expect("open reader WAL");
6567        let mut reader = WalBackendAdapter::new(wal_reader);
6568
6569        let before = reader.published_snapshot();
6570        assert_eq!(before.last_commit_frame, None);
6571        assert_eq!(before.commit_count, 0);
6572        assert_eq!(before.latest_frame_entries, 0);
6573
6574        let refreshed = reader
6575            .refresh_published_snapshot(&cx)
6576            .expect("refresh published snapshot");
6577        assert_eq!(refreshed.last_commit_frame, Some(1));
6578        assert_eq!(refreshed.commit_count, 1);
6579        assert_eq!(refreshed.latest_frame_entries, 2);
6580        assert!(refreshed.lookup_contract_is_authoritative());
6581        assert_eq!(reader.read_page(&cx, 1).expect("read p1"), Some(p1));
6582        assert_eq!(reader.read_page(&cx, 2).expect("read p2"), Some(p2));
6583    }
6584
6585    #[test]
6586    fn test_page_index_incremental_extend_after_durable_sync() {
6587        // Verify that the index extends incrementally once each commit crosses
6588        // the durable-sync publication barrier.
6589        let cx = test_cx();
6590        let vfs = MemoryVfs::new();
6591        let mut adapter = make_adapter(&vfs, &cx);
6592
6593        let page1 = sample_page(0x10);
6594        adapter
6595            .append_frame(&cx, 1, &page1, 1)
6596            .expect("append commit 1");
6597        adapter.sync(&cx).expect("durably publish commit 1");
6598
6599        // First read builds the index.
6600        assert_eq!(
6601            adapter.read_page(&cx, 1).expect("read"),
6602            Some(page1.clone())
6603        );
6604
6605        // Append more committed frames.
6606        let page2 = sample_page(0x20);
6607        let page1_v2 = sample_page(0x30);
6608        adapter
6609            .append_frame(&cx, 2, &page2, 0)
6610            .expect("append page 2");
6611        adapter
6612            .append_frame(&cx, 1, &page1_v2, 3)
6613            .expect("append page 1 v2 (commit)");
6614        adapter.sync(&cx).expect("durably publish commit 2");
6615
6616        // Reading should trigger incremental extend, not full rebuild.
6617        assert_eq!(
6618            adapter.read_page(&cx, 1).expect("read page 1 v2"),
6619            Some(page1_v2),
6620            "incremental index extend should pick up the updated page"
6621        );
6622        assert_eq!(adapter.read_page(&cx, 2).expect("read page 2"), Some(page2));
6623    }
6624
6625    /// Frames for a two-page commit batch, the second frame carrying the commit.
6626    fn commit_batch_pages() -> (Vec<u8>, Vec<u8>) {
6627        (sample_page(0x71), sample_page(0x72))
6628    }
6629
6630    /// Assert no commit horizon has been published yet.
6631    fn assert_publication_unchanged(adapter: &WalBackendAdapter<impl VfsFile>, context: &str) {
6632        assert_eq!(
6633            adapter.published_snapshot.last_commit_frame, None,
6634            "{context}: publication must not advance before a successful sync"
6635        );
6636        assert_eq!(
6637            adapter.published_snapshot.commit_count, 0,
6638            "{context}: commit count must not advance before a successful sync"
6639        );
6640        assert!(
6641            adapter.published_snapshot.page_index.is_empty(),
6642            "{context}: no page may be visible before a successful sync"
6643        );
6644    }
6645
6646    #[test]
6647    fn test_append_frame_without_sync_leaves_publication_unchanged() {
6648        let cx = test_cx();
6649        let vfs = MemoryVfs::new();
6650        let mut adapter = make_adapter(&vfs, &cx);
6651
6652        let (p1, p2) = commit_batch_pages();
6653        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6654        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6655
6656        assert_publication_unchanged(&adapter, "append_frame");
6657        assert_eq!(
6658            adapter.pending_publication_commit,
6659            Some(1),
6660            "append_frame must stage the commit horizon for a later sync"
6661        );
6662    }
6663
6664    #[test]
6665    fn test_append_frames_without_sync_leaves_publication_unchanged() {
6666        let cx = test_cx();
6667        let vfs = MemoryVfs::new();
6668        let mut adapter = make_adapter(&vfs, &cx);
6669
6670        let (p1, p2) = commit_batch_pages();
6671        let frames = [
6672            WalFrameRef {
6673                page_number: 1,
6674                page_data: &p1,
6675                db_size_if_commit: 0,
6676            },
6677            WalFrameRef {
6678                page_number: 2,
6679                page_data: &p2,
6680                db_size_if_commit: 2,
6681            },
6682        ];
6683        adapter
6684            .append_frames(&cx, &frames)
6685            .expect("append frames batch");
6686
6687        assert_publication_unchanged(&adapter, "append_frames");
6688        assert_eq!(
6689            adapter.pending_publication_commit,
6690            Some(1),
6691            "append_frames must stage the commit horizon for a later sync"
6692        );
6693    }
6694
6695    #[test]
6696    fn test_append_frames_tracked_without_sync_leaves_publication_unchanged() {
6697        let cx = test_cx();
6698        let vfs = MemoryVfs::new();
6699        let mut adapter = make_adapter(&vfs, &cx);
6700
6701        let (p1, p2) = commit_batch_pages();
6702        let frames = [
6703            WalFrameRef {
6704                page_number: 1,
6705                page_data: &p1,
6706                db_size_if_commit: 0,
6707            },
6708            WalFrameRef {
6709                page_number: 2,
6710                page_data: &p2,
6711                db_size_if_commit: 2,
6712            },
6713        ];
6714        adapter
6715            .append_frames_tracked(&cx, &frames, VfsWriteCompletion::new())
6716            .expect("append tracked frames batch");
6717
6718        assert_publication_unchanged(&adapter, "append_frames_tracked");
6719        assert_eq!(
6720            adapter.pending_publication_commit,
6721            Some(1),
6722            "append_frames_tracked must stage the commit horizon for a later sync"
6723        );
6724    }
6725
6726    #[test]
6727    fn test_append_prepared_frames_without_sync_leaves_publication_unchanged() {
6728        let cx = test_cx();
6729        let vfs = MemoryVfs::new();
6730        let mut adapter = make_adapter(&vfs, &cx);
6731
6732        let (p1, p2) = commit_batch_pages();
6733        let frames = [
6734            WalFrameRef {
6735                page_number: 1,
6736                page_data: &p1,
6737                db_size_if_commit: 0,
6738            },
6739            WalFrameRef {
6740                page_number: 2,
6741                page_data: &p2,
6742                db_size_if_commit: 2,
6743            },
6744        ];
6745        let mut prepared = adapter
6746            .prepare_append_frames(&frames)
6747            .expect("prepare append")
6748            .expect("prepared batch");
6749        adapter
6750            .append_prepared_frames(&cx, &mut prepared)
6751            .expect("append prepared");
6752
6753        assert_publication_unchanged(&adapter, "append_prepared_frames");
6754        assert_eq!(
6755            adapter.pending_publication_commit,
6756            Some(1),
6757            "append_prepared_frames must stage the commit horizon for a later sync"
6758        );
6759    }
6760
6761    #[test]
6762    fn test_append_prepared_frames_tracked_without_sync_leaves_publication_unchanged() {
6763        let cx = test_cx();
6764        let vfs = MemoryVfs::new();
6765        let mut adapter = make_adapter(&vfs, &cx);
6766
6767        let (p1, p2) = commit_batch_pages();
6768        let frames = [
6769            WalFrameRef {
6770                page_number: 1,
6771                page_data: &p1,
6772                db_size_if_commit: 0,
6773            },
6774            WalFrameRef {
6775                page_number: 2,
6776                page_data: &p2,
6777                db_size_if_commit: 2,
6778            },
6779        ];
6780        let mut prepared = adapter
6781            .prepare_append_frames(&frames)
6782            .expect("prepare append")
6783            .expect("prepared batch");
6784        adapter
6785            .append_prepared_frames_tracked(&cx, &mut prepared, VfsWriteCompletion::new())
6786            .expect("append prepared tracked");
6787
6788        assert_publication_unchanged(&adapter, "append_prepared_frames_tracked");
6789        assert_eq!(
6790            adapter.pending_publication_commit,
6791            Some(1),
6792            "append_prepared_frames_tracked must stage the commit horizon for a later sync"
6793        );
6794    }
6795
6796    #[test]
6797    fn test_successful_sync_publishes_staged_commit_horizon() {
6798        let cx = test_cx();
6799        let vfs = MemoryVfs::new();
6800        let mut adapter = make_adapter(&vfs, &cx);
6801
6802        let (p1, p2) = commit_batch_pages();
6803        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6804        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6805        assert_publication_unchanged(&adapter, "before sync");
6806
6807        adapter.sync(&cx).expect("sync must succeed");
6808
6809        assert_eq!(
6810            adapter.published_snapshot.last_commit_frame,
6811            Some(1),
6812            "a successful sync must publish the staged commit horizon"
6813        );
6814        assert_eq!(
6815            adapter.published_snapshot.commit_count, 1,
6816            "a successful sync must publish the staged commit count"
6817        );
6818        assert_eq!(
6819            adapter.published_snapshot.page_index.len(),
6820            2,
6821            "a successful sync must publish every staged page"
6822        );
6823        assert_eq!(
6824            adapter.pending_publication_commit, None,
6825            "a published batch must no longer be staged"
6826        );
6827        assert!(
6828            adapter.pending_publication_frames.is_empty(),
6829            "a published batch must drain its staged frames"
6830        );
6831    }
6832
6833    #[test]
6834    fn test_failed_sync_advances_no_publication_and_retry_publishes() {
6835        let cx = test_cx();
6836        let vfs = CheckpointHandoffFaultVfs::new();
6837        let mut adapter = make_fault_adapter(&vfs, &cx);
6838
6839        let (p1, p2) = commit_batch_pages();
6840        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6841        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6842
6843        vfs.fail_next_wal_sync();
6844        let failure = adapter
6845            .sync(&cx)
6846            .expect_err("injected WAL sync failure must surface");
6847        assert!(
6848            failure.to_string().contains("injected WAL sync failure"),
6849            "sync must report the injected durability failure, got: {failure}"
6850        );
6851
6852        assert_publication_unchanged(&adapter, "after failed sync");
6853        assert_eq!(
6854            adapter.pending_publication_commit,
6855            Some(1),
6856            "a failed sync must preserve the staged horizon for retry"
6857        );
6858        assert!(
6859            !adapter.pending_publication_frames.is_empty(),
6860            "a failed sync must preserve staged frames for retry"
6861        );
6862
6863        // Retry: the same staged batch publishes once durability succeeds.
6864        adapter.sync(&cx).expect("retry sync must succeed");
6865
6866        assert_eq!(
6867            adapter.published_snapshot.last_commit_frame,
6868            Some(1),
6869            "retrying sync must publish the preserved commit horizon"
6870        );
6871        assert_eq!(
6872            adapter.published_snapshot.commit_count, 1,
6873            "retrying sync must publish the preserved commit count"
6874        );
6875        assert_eq!(
6876            adapter.published_snapshot.page_index.len(),
6877            2,
6878            "retrying sync must publish every preserved page"
6879        );
6880        assert_eq!(
6881            adapter.pending_publication_commit, None,
6882            "a retried publication must clear the staged horizon"
6883        );
6884    }
6885
6886    #[test]
6887    fn test_failed_sync_then_append_cannot_drop_or_publish_pending() {
6888        let cx = test_cx();
6889        let vfs = CheckpointHandoffFaultVfs::new();
6890        let mut adapter = make_fault_adapter(&vfs, &cx);
6891
6892        let (p1, p2) = commit_batch_pages();
6893        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6894        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6895
6896        vfs.fail_next_wal_sync();
6897        adapter
6898            .sync(&cx)
6899            .expect_err("injected WAL sync failure must surface");
6900
6901        let staged_after_failure = adapter.pending_publication_commit;
6902        let staged_frames_after_failure = adapter.pending_publication_frames.len();
6903        assert_eq!(
6904            staged_after_failure,
6905            Some(1),
6906            "failed sync must preserve the staged horizon"
6907        );
6908
6909        // A further append must not run the pre-append resynchronization, which
6910        // would discard the preserved batch and republish the unsynced horizon.
6911        let p3 = sample_page(0x73);
6912        adapter
6913            .append_frame(&cx, 3, &p3, 3)
6914            .expect("append after failed sync");
6915
6916        assert_publication_unchanged(&adapter, "append after failed sync");
6917        assert!(
6918            adapter.pending_publication_frames.len() > staged_frames_after_failure,
6919            "append after a failed sync must extend, never discard, the staged batch"
6920        );
6921        assert_eq!(
6922            adapter.pending_publication_commit,
6923            Some(2),
6924            "append after a failed sync must carry the staged horizon forward"
6925        );
6926
6927        // Durability finally succeeds: the whole preserved batch publishes.
6928        adapter.sync(&cx).expect("sync after failed attempt");
6929        assert_eq!(
6930            adapter.published_snapshot.last_commit_frame,
6931            Some(2),
6932            "recovered sync must publish the full preserved horizon"
6933        );
6934        assert_eq!(
6935            adapter.pending_publication_commit, None,
6936            "recovered sync must clear the staged horizon"
6937        );
6938    }
6939
6940    #[test]
6941    fn test_failed_sync_then_begin_transaction_then_append_fails_closed() {
6942        let cx = test_cx();
6943        let vfs = CheckpointHandoffFaultVfs::new();
6944        let mut adapter = make_fault_adapter(&vfs, &cx);
6945
6946        let (p1, p2) = commit_batch_pages();
6947        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
6948        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
6949
6950        vfs.fail_next_wal_sync();
6951        adapter
6952            .sync(&cx)
6953            .expect_err("injected WAL sync failure must surface");
6954        assert_eq!(
6955            adapter.pending_publication_commit,
6956            Some(1),
6957            "failed sync must preserve the staged horizon"
6958        );
6959
6960        // `begin_transaction` must reject at the earliest illegal transition,
6961        // before refreshing, pinning a read snapshot, or re-arming the
6962        // pre-append guard — and it must be a retryable Busy, not corruption.
6963        let begin_error = adapter
6964            .begin_transaction(&cx)
6965            .expect_err("begin_transaction must fail closed while frames are staged");
6966        assert!(
6967            matches!(begin_error, FrankenError::Busy),
6968            "staged-state rejection must be retryable Busy, not corruption: {begin_error:?}"
6969        );
6970        assert_publication_unchanged(&adapter, "begin_transaction refused after failed sync");
6971        assert_eq!(
6972            adapter.pending_publication_commit,
6973            Some(1),
6974            "a refused begin_transaction must not drop the staged horizon"
6975        );
6976        assert!(
6977            adapter.pinned_read_snapshot().is_none(),
6978            "a refused begin_transaction must not pin a read snapshot"
6979        );
6980
6981        // Defense in depth: the pre-append choke guard still refuses for any
6982        // other path that re-arms `refresh_before_append`.
6983        adapter.refresh_before_append = true;
6984        let p3 = sample_page(0x74);
6985        let append_error = adapter
6986            .append_frame(&cx, 3, &p3, 3)
6987            .expect_err("append must fail closed while frames are staged");
6988        assert!(
6989            matches!(append_error, FrankenError::Busy),
6990            "append rejection must be retryable Busy: {append_error:?}"
6991        );
6992        assert_publication_unchanged(&adapter, "append refused after failed sync");
6993        assert_eq!(
6994            adapter.pending_publication_commit,
6995            Some(1),
6996            "a refused append must leave the staged horizon intact"
6997        );
6998        assert!(
6999            !adapter.pending_publication_frames.is_empty(),
7000            "a refused append must leave the staged frames intact"
7001        );
7002        adapter.refresh_before_append = false;
7003
7004        // The batch is still recoverable: a successful sync publishes it.
7005        adapter.sync(&cx).expect("sync after failed attempt");
7006        assert_eq!(
7007            adapter.published_snapshot.last_commit_frame,
7008            Some(1),
7009            "recovered sync must publish the preserved horizon"
7010        );
7011    }
7012
7013    #[test]
7014    fn test_failed_sync_then_checkpoint_fails_closed_and_preserves_state() {
7015        let cx = test_cx();
7016        let vfs = CheckpointHandoffFaultVfs::new();
7017        let mut adapter = make_fault_adapter(&vfs, &cx);
7018
7019        let (p1, p2) = commit_batch_pages();
7020        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7021        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7022
7023        vfs.fail_next_wal_sync();
7024        adapter
7025            .sync(&cx)
7026            .expect_err("injected WAL sync failure must surface");
7027
7028        let frames_before = adapter.frame_count();
7029        let staged_before = adapter.pending_publication_commit;
7030        let staged_frame_count_before = adapter.pending_publication_frames.len();
7031
7032        // Checkpoint must refuse before touching the WAL: it backfills, may
7033        // reset, and can invalidate the publication plane, all of which would
7034        // destroy the staged batch.
7035        let mut writer = MockCheckpointPageWriter;
7036        let checkpoint_error = adapter
7037            .checkpoint(&cx, CheckpointMode::Passive, &mut writer, 0, None)
7038            .expect_err("checkpoint must fail closed while frames are staged");
7039        assert!(
7040            matches!(checkpoint_error, FrankenError::CheckpointFailed { .. }),
7041            "checkpoint rejection must be CheckpointFailed, not corruption: {checkpoint_error:?}"
7042        );
7043
7044        assert_eq!(
7045            adapter.frame_count(),
7046            frames_before,
7047            "a refused checkpoint must not mutate WAL bytes"
7048        );
7049        assert_publication_unchanged(&adapter, "checkpoint refused");
7050        assert_eq!(
7051            adapter.pending_publication_commit, staged_before,
7052            "a refused checkpoint must preserve the staged horizon"
7053        );
7054        assert_eq!(
7055            adapter.pending_publication_frames.len(),
7056            staged_frame_count_before,
7057            "a refused checkpoint must preserve the staged frames"
7058        );
7059
7060        // Retry: durability succeeds and the preserved batch publishes.
7061        adapter.sync(&cx).expect("retry sync must succeed");
7062        assert_eq!(
7063            adapter.published_snapshot.last_commit_frame,
7064            Some(1),
7065            "retry sync must publish the preserved horizon"
7066        );
7067        assert_eq!(
7068            adapter.pending_publication_commit, None,
7069            "a published batch must no longer be staged"
7070        );
7071    }
7072
7073    #[test]
7074    fn test_midtransaction_sync_preserves_uncommitted_frames_and_allows_continuation() {
7075        let cx = test_cx();
7076        let vfs = MemoryVfs::new();
7077        let mut adapter = make_adapter(&vfs, &cx);
7078
7079        let (p1, p2) = commit_batch_pages();
7080
7081        // Append a non-commit frame, then sync. The frame becomes durable but is
7082        // not committed, so nothing may be published.
7083        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7084        assert_eq!(
7085            adapter.pending_publication_commit, None,
7086            "a non-commit append stages no commit horizon"
7087        );
7088        adapter
7089            .sync(&cx)
7090            .expect("mid-transaction sync must succeed");
7091
7092        assert_publication_unchanged(&adapter, "sync of uncommitted frames");
7093        assert!(
7094            !adapter.pending_publication_frames.is_empty(),
7095            "a mid-transaction sync must preserve durable-but-uncommitted frames"
7096        );
7097
7098        // Continuation must remain possible: the commit marker still lands.
7099        adapter
7100            .append_frame(&cx, 2, &p2, 2)
7101            .expect("commit append after mid-transaction sync must be allowed");
7102        assert_eq!(
7103            adapter.pending_publication_commit,
7104            Some(1),
7105            "the commit append must stage the horizon for the whole batch"
7106        );
7107        assert_publication_unchanged(&adapter, "commit staged but not yet synced");
7108
7109        adapter.sync(&cx).expect("commit sync must succeed");
7110
7111        assert_eq!(
7112            adapter.published_snapshot.last_commit_frame,
7113            Some(1),
7114            "the commit sync must publish the whole batch"
7115        );
7116        assert_eq!(
7117            adapter.published_snapshot.commit_count, 1,
7118            "the batch must publish exactly one commit"
7119        );
7120        assert_eq!(
7121            adapter.published_snapshot.page_index.len(),
7122            2,
7123            "both pages must be published exactly once"
7124        );
7125        assert_eq!(
7126            adapter.published_snapshot.page_index.get(&1),
7127            Some(&0),
7128            "page 1 must map to its frame from before the mid-transaction sync"
7129        );
7130        assert_eq!(
7131            adapter.published_snapshot.page_index.get(&2),
7132            Some(&1),
7133            "page 2 must map to the commit frame"
7134        );
7135        assert!(
7136            !adapter.has_pending_publication(),
7137            "a published batch must leave nothing staged"
7138        );
7139    }
7140
7141    #[test]
7142    fn test_inner_mut_fails_closed_while_batch_is_staged() {
7143        let cx = test_cx();
7144        let vfs = CheckpointHandoffFaultVfs::new();
7145        let mut adapter = make_fault_adapter(&vfs, &cx);
7146
7147        let (p1, p2) = commit_batch_pages();
7148        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7149        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7150
7151        assert!(
7152            adapter.has_pending_publication(),
7153            "an appended-but-unsynced batch must report as pending"
7154        );
7155        // `expect_err` would require `WalFile: Debug`, which the fault-VFS file
7156        // type does not implement, so assert on the pattern directly.
7157        assert!(
7158            matches!(adapter.inner_mut(), Err(FrankenError::Busy)),
7159            "inner_mut must fail closed with retryable Busy while frames are staged"
7160        );
7161        assert_eq!(
7162            adapter.pending_publication_commit,
7163            Some(1),
7164            "a refused inner_mut must preserve the staged horizon"
7165        );
7166
7167        // Once drained, the escape hatch opens again.
7168        adapter.sync(&cx).expect("sync staged batch");
7169        assert!(
7170            !adapter.has_pending_publication(),
7171            "a published batch must clear the pending flag"
7172        );
7173        adapter
7174            .inner_mut()
7175            .expect("inner_mut must succeed once the batch is drained");
7176    }
7177
7178    #[test]
7179    fn test_unpinned_refresh_does_not_expose_staged_horizon_before_sync() {
7180        let cx = test_cx();
7181        let vfs = MemoryVfs::new();
7182        let mut adapter = make_adapter(&vfs, &cx);
7183
7184        let (p1, p2) = commit_batch_pages();
7185        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7186        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7187
7188        // An explicit refresh must not publish frames this handle has staged but
7189        // not yet made durable.
7190        adapter
7191            .refresh_published_snapshot(&cx)
7192            .expect("refresh published snapshot");
7193        assert_publication_unchanged(&adapter, "refresh with staged frames");
7194        assert_eq!(
7195            adapter.pending_publication_commit,
7196            Some(1),
7197            "refresh must leave the staged horizon intact"
7198        );
7199
7200        adapter.sync(&cx).expect("sync staged batch");
7201        assert_eq!(
7202            adapter.published_snapshot.last_commit_frame,
7203            Some(1),
7204            "sync must publish once the staged batch is durable"
7205        );
7206    }
7207
7208    #[test]
7209    fn test_authorized_deferred_commit_publishes_without_claiming_fsync() {
7210        let cx = test_cx();
7211        let vfs = MemoryVfs::new();
7212        let mut adapter = make_adapter(&vfs, &cx);
7213
7214        let (p1, p2) = commit_batch_pages();
7215        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7216        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7217        let fsynced_before = adapter.wal.last_fsynced_frame_count();
7218
7219        adapter
7220            .publish_authorized_deferred_commit(&cx)
7221            .expect("parallel-WAL authorization must publish the deferred commit");
7222
7223        assert_eq!(
7224            adapter.published_snapshot.last_commit_frame,
7225            Some(1),
7226            "the authorized commit marker must become visible"
7227        );
7228        assert_eq!(
7229            adapter.published_snapshot.commit_count, 1,
7230            "the authorized batch must publish exactly one commit"
7231        );
7232        assert!(
7233            !adapter.has_pending_publication(),
7234            "authorization must drain the staged publication horizon"
7235        );
7236        assert_eq!(
7237            adapter.wal.last_fsynced_frame_count(),
7238            fsynced_before,
7239            "deferred authorization must not claim or force an fsync"
7240        );
7241        adapter
7242            .begin_transaction(&cx)
7243            .expect("the next transaction must not see a stale Busy");
7244    }
7245
7246    #[test]
7247    fn test_commit_append_publishes_visibility_snapshot() {
7248        init_wal_publication_test_tracing();
7249        let cx = test_cx();
7250        let vfs = MemoryVfs::new();
7251        let mut adapter = make_adapter(&vfs, &cx);
7252
7253        let p1 = sample_page(0x41);
7254        let p2 = sample_page(0x42);
7255        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7256        adapter.append_frame(&cx, 2, &p2, 2).expect("append commit");
7257        // Publication is deferred to the durability barrier (#187); the commit
7258        // horizon only becomes visible once `sync` persists the frames.
7259        adapter.sync(&cx).expect("sync commit batch");
7260
7261        assert_eq!(
7262            adapter.published_snapshot.last_commit_frame,
7263            Some(1),
7264            "synced commit should publish the visible commit horizon"
7265        );
7266        assert_eq!(
7267            adapter.published_snapshot.commit_count, 1,
7268            "synced commit should track the visible WAL commit count"
7269        );
7270        assert_eq!(
7271            adapter.published_snapshot.page_index.len(),
7272            2,
7273            "published snapshot should track both committed pages"
7274        );
7275        assert_eq!(
7276            adapter.published_snapshot.page_index.get(&2),
7277            Some(&1),
7278            "published snapshot must map each page to its latest committed frame"
7279        );
7280    }
7281
7282    #[test]
7283    fn test_prepared_append_publishes_visibility_snapshot() {
7284        init_wal_publication_test_tracing();
7285        let cx = test_cx();
7286        let vfs = MemoryVfs::new();
7287        let mut adapter = make_adapter(&vfs, &cx);
7288
7289        let p1 = sample_page(0x51);
7290        let p2 = sample_page(0x52);
7291        let frames = [
7292            WalFrameRef {
7293                page_number: 1,
7294                page_data: &p1,
7295                db_size_if_commit: 0,
7296            },
7297            WalFrameRef {
7298                page_number: 2,
7299                page_data: &p2,
7300                db_size_if_commit: 2,
7301            },
7302        ];
7303        let mut prepared = adapter
7304            .prepare_append_frames(&frames)
7305            .expect("prepare append")
7306            .expect("prepared batch");
7307        adapter
7308            .append_prepared_frames(&cx, &mut prepared)
7309            .expect("append prepared");
7310        // Publication is deferred to the durability barrier (#187).
7311        adapter.sync(&cx).expect("sync prepared commit batch");
7312
7313        assert_eq!(
7314            adapter.published_snapshot.last_commit_frame,
7315            Some(1),
7316            "synced prepared commit should publish the visible commit horizon"
7317        );
7318        assert_eq!(
7319            adapter.published_snapshot.commit_count, 1,
7320            "synced prepared commit should track the visible WAL commit count"
7321        );
7322        assert_eq!(
7323            adapter.published_snapshot.page_index.len(),
7324            2,
7325            "synced prepared commit should publish all committed pages"
7326        );
7327        assert_eq!(
7328            adapter.published_snapshot.page_index.get(&2),
7329            Some(&1),
7330            "prepared commit append must map each page to its latest committed frame"
7331        );
7332    }
7333
7334    #[test]
7335    fn test_commit_publication_refreshes_external_prefix_before_local_commit() {
7336        let cx = test_cx();
7337        let vfs = MemoryVfs::new();
7338
7339        let file_writer = open_wal_file(&vfs, &cx);
7340        let wal_writer =
7341            WalFile::create(&cx, file_writer, PAGE_SIZE, 0, test_salts()).expect("create WAL");
7342        let mut writer = WalBackendAdapter::new(wal_writer);
7343
7344        let file_follower = open_wal_file(&vfs, &cx);
7345        let wal_follower = WalFile::open(&cx, file_follower).expect("open WAL");
7346        let mut follower = WalBackendAdapter::new(wal_follower);
7347
7348        let p1 = sample_page(0x61);
7349        writer
7350            .append_frame(&cx, 1, &p1, 1)
7351            .expect("writer commit 1");
7352        writer.sync(&cx).expect("sync writer commit 1");
7353
7354        let p2 = sample_page(0x62);
7355        writer
7356            .append_frame(&cx, 2, &p2, 2)
7357            .expect("writer commit 2");
7358        writer.sync(&cx).expect("sync writer commit 2");
7359
7360        let p3 = sample_page(0x63);
7361        follower
7362            .append_frame(&cx, 3, &p3, 3)
7363            .expect("follower local commit");
7364
7365        // Durable-certificate contract: the append refreshes the EXTERNAL
7366        // published prefix into the follower's snapshot, but the follower's
7367        // own commit stays staged until its sync.
7368        assert_eq!(
7369            follower.published_snapshot.last_commit_frame,
7370            Some(1),
7371            "refresh-before-append must publish the external prefix only"
7372        );
7373        assert_eq!(
7374            follower.published_snapshot.commit_count, 2,
7375            "the staged local commit must not count until publication"
7376        );
7377        assert_eq!(
7378            follower.published_snapshot.page_index.get(&1),
7379            Some(&0),
7380            "refresh-before-append should preserve earlier committed pages"
7381        );
7382        assert_eq!(
7383            follower.published_snapshot.page_index.get(&2),
7384            Some(&1),
7385            "refresh-before-append should publish externally committed pages"
7386        );
7387        assert_eq!(
7388            follower.published_snapshot.page_index.get(&3),
7389            None,
7390            "the staged local page must stay out of the published map"
7391        );
7392
7393        follower.sync(&cx).expect("publish follower local commit");
7394        assert_eq!(
7395            follower.published_snapshot.last_commit_frame,
7396            Some(2),
7397            "publication must extend the map with the local commit"
7398        );
7399        assert_eq!(follower.published_snapshot.commit_count, 3);
7400        assert_eq!(
7401            follower.published_snapshot.page_index.get(&3),
7402            Some(&2),
7403            "published local commit extends the WAL visibility map"
7404        );
7405        assert_eq!(follower.read_page(&cx, 1).expect("read p1"), Some(p1));
7406        assert_eq!(follower.read_page(&cx, 2).expect("read p2"), Some(p2));
7407        assert_eq!(follower.read_page(&cx, 3).expect("read p3"), Some(p3));
7408    }
7409
7410    #[test]
7411    fn test_truncate_checkpoint_republishes_empty_generation_snapshot() {
7412        init_wal_publication_test_tracing();
7413        let cx = test_cx();
7414        let vfs = MemoryVfs::new();
7415        let mut adapter = make_adapter(&vfs, &cx);
7416        let mut writer = MockCheckpointPageWriter;
7417
7418        adapter
7419            .append_frame(&cx, 1, &sample_page(0x61), 1)
7420            .expect("append committed frame");
7421        // Publication is deferred to the durability barrier (#187), and
7422        // checkpoint now fails closed while a batch is staged, so the batch must
7423        // be drained before checkpointing.
7424        adapter.sync(&cx).expect("sync committed frame");
7425        let before = adapter.published_snapshot();
7426        assert_eq!(before.last_commit_frame, Some(0));
7427        assert_eq!(before.commit_count, 1);
7428        assert_eq!(before.latest_frame_entries, 1);
7429
7430        let result = adapter
7431            .checkpoint(&cx, CheckpointMode::Truncate, &mut writer, 0, None)
7432            .expect("truncate checkpoint");
7433        assert!(result.completed);
7434        assert!(result.wal_was_reset);
7435
7436        let after = adapter.published_snapshot();
7437        assert_ne!(
7438            before.generation, after.generation,
7439            "truncate checkpoint should publish a new WAL generation"
7440        );
7441        assert_eq!(after.last_commit_frame, None);
7442        assert_eq!(after.commit_count, 0);
7443        assert_eq!(after.latest_frame_entries, 0);
7444        assert!(after.lookup_contract_is_authoritative());
7445    }
7446
7447    // -- Partial index fallback tests --
7448
7449    #[test]
7450    fn test_partial_index_falls_back_to_linear_scan() {
7451        init_wal_publication_test_tracing();
7452        // Verify that when the page index cap is hit, pages that weren't
7453        // indexed are still found via the backwards linear scan fallback.
7454        let cx = test_cx();
7455        let vfs = MemoryVfs::new();
7456        let mut adapter = make_adapter(&vfs, &cx);
7457
7458        // Set a very small cap so we can trigger the partial-index path
7459        // with just a handful of frames.
7460        adapter.set_page_index_cap(2);
7461
7462        // Write 5 distinct pages.  With a cap of 2, only the first 2 unique
7463        // pages will be indexed; pages 3-5 will be dropped from the index.
7464        let p1 = sample_page(0x01);
7465        let p2 = sample_page(0x02);
7466        let p3 = sample_page(0x03);
7467        let p4 = sample_page(0x04);
7468        let p5 = sample_page(0x05);
7469
7470        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7471        adapter.append_frame(&cx, 2, &p2, 0).expect("append p2");
7472        adapter.append_frame(&cx, 3, &p3, 0).expect("append p3");
7473        adapter.append_frame(&cx, 4, &p4, 0).expect("append p4");
7474        adapter
7475            .append_frame(&cx, 5, &p5, 5)
7476            .expect("append p5 (commit)");
7477        adapter.sync(&cx).expect("publish staged frames");
7478
7479        // Pages 1 and 2 should be in the index (fast path).
7480        assert_eq!(
7481            adapter.read_page(&cx, 1).expect("read p1"),
7482            Some(p1),
7483            "indexed page should be found via HashMap"
7484        );
7485        assert_eq!(
7486            adapter.read_page(&cx, 2).expect("read p2"),
7487            Some(p2),
7488            "indexed page should be found via HashMap"
7489        );
7490
7491        // Pages 3-5 were NOT indexed, but must still be found via the
7492        // backwards linear scan fallback.
7493        assert_eq!(
7494            adapter.read_page(&cx, 3).expect("read p3"),
7495            Some(p3),
7496            "non-indexed page must be found via linear scan fallback"
7497        );
7498        assert_eq!(
7499            adapter.read_page(&cx, 4).expect("read p4"),
7500            Some(p4),
7501            "non-indexed page must be found via linear scan fallback"
7502        );
7503        assert_eq!(
7504            adapter.read_page(&cx, 5).expect("read p5"),
7505            Some(p5),
7506            "non-indexed page must be found via linear scan fallback"
7507        );
7508
7509        // A page that was never written should still return None.
7510        assert_eq!(
7511            adapter.read_page(&cx, 99).expect("read non-existent"),
7512            None,
7513            "non-existent page must return None even with partial index"
7514        );
7515
7516        // Verify the index was indeed marked partial.
7517        assert!(
7518            adapter.published_snapshot.index_is_partial,
7519            "index_is_partial should be true when cap is exceeded"
7520        );
7521    }
7522
7523    #[test]
7524    fn test_partial_index_returns_latest_version_via_fallback() {
7525        // When the same page appears multiple times and overflows the index,
7526        // the backwards scan must return the LATEST (highest frame index)
7527        // version, not the first one it encounters in a forward scan.
7528        let cx = test_cx();
7529        let vfs = MemoryVfs::new();
7530        let mut adapter = make_adapter(&vfs, &cx);
7531
7532        // Cap at 1 so only page 1 fits in the index.
7533        adapter.set_page_index_cap(1);
7534
7535        let old_p2 = sample_page(0xAA);
7536        let new_p2 = sample_page(0xBB);
7537
7538        // Frame 0: page 1 (indexed)
7539        adapter
7540            .append_frame(&cx, 1, &sample_page(0x01), 0)
7541            .expect("append p1");
7542        // Frame 1: page 2 old version (NOT indexed -- cap exceeded)
7543        adapter
7544            .append_frame(&cx, 2, &old_p2, 0)
7545            .expect("append p2 old");
7546        // Frame 2: page 2 new version (NOT indexed -- cap exceeded, and
7547        // page 2 is not already in the index so it won't be updated)
7548        adapter
7549            .append_frame(&cx, 2, &new_p2, 3)
7550            .expect("append p2 new (commit)");
7551        adapter.sync(&cx).expect("publish staged frames");
7552
7553        // The backwards scan from frame 2 should find the newest version first.
7554        assert_eq!(
7555            adapter.read_page(&cx, 2).expect("read p2"),
7556            Some(new_p2),
7557            "backwards scan must return the most recent frame for the page"
7558        );
7559    }
7560
7561    #[test]
7562    fn test_lookup_contract_distinguishes_authoritative_and_fallback_paths() {
7563        init_wal_publication_test_tracing();
7564        let cx = test_cx();
7565        let vfs = MemoryVfs::new();
7566        let mut adapter = make_adapter(&vfs, &cx);
7567        adapter.set_page_index_cap(1);
7568
7569        let p1 = sample_page(0x01);
7570        let p2 = sample_page(0x02);
7571        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7572        adapter
7573            .append_frame(&cx, 2, &p2, 2)
7574            .expect("append p2 commit");
7575        adapter.sync(&cx).expect("publish staged frames");
7576
7577        let last_commit = adapter
7578            .inner_mut()
7579            .expect("no staged batch blocks inner access")
7580            .last_commit_frame(&cx)
7581            .expect("last commit")
7582            .expect("commit exists");
7583        adapter
7584            .publish_visible_snapshot(&cx, Some(last_commit), "lookup_contract_test")
7585            .expect("build published snapshot");
7586        let snapshot = adapter.published_snapshot.clone();
7587
7588        assert_eq!(
7589            adapter
7590                .resolve_visible_frame(&cx, &snapshot, 1)
7591                .expect("resolve indexed page"),
7592            WalPageLookupResolution::AuthoritativeHit { frame_index: 0 }
7593        );
7594        assert_eq!(
7595            adapter
7596                .resolve_visible_frame(&cx, &snapshot, 2)
7597                .expect("resolve fallback page"),
7598            WalPageLookupResolution::PartialIndexFallbackHit { frame_index: 1 }
7599        );
7600        assert_eq!(
7601            adapter
7602                .resolve_visible_frame(&cx, &snapshot, 99)
7603                .expect("resolve missing page"),
7604            WalPageLookupResolution::PartialIndexFallbackMiss
7605        );
7606    }
7607
7608    #[test]
7609    fn test_lookup_contract_is_authoritative_by_default() {
7610        let cx = test_cx();
7611        let vfs = MemoryVfs::new();
7612        let mut adapter = make_adapter(&vfs, &cx);
7613
7614        let p1 = sample_page(0x11);
7615        let p2 = sample_page(0x22);
7616        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7617        adapter
7618            .append_frame(&cx, 2, &p2, 2)
7619            .expect("append p2 commit");
7620        adapter.sync(&cx).expect("publish staged frames");
7621
7622        let last_commit = adapter
7623            .inner_mut()
7624            .expect("no staged batch blocks inner access")
7625            .last_commit_frame(&cx)
7626            .expect("last commit")
7627            .expect("commit exists");
7628        adapter
7629            .publish_visible_snapshot(&cx, Some(last_commit), "lookup_contract_default")
7630            .expect("build published snapshot");
7631        let snapshot = adapter.published_snapshot.clone();
7632
7633        assert!(
7634            !snapshot.index_is_partial,
7635            "default index should be authoritative"
7636        );
7637        assert_eq!(
7638            adapter
7639                .resolve_visible_frame(&cx, &snapshot, 1)
7640                .expect("resolve page 1"),
7641            WalPageLookupResolution::AuthoritativeHit { frame_index: 0 }
7642        );
7643        assert_eq!(
7644            adapter
7645                .resolve_visible_frame(&cx, &snapshot, 2)
7646                .expect("resolve page 2"),
7647            WalPageLookupResolution::AuthoritativeHit { frame_index: 1 }
7648        );
7649        assert_eq!(
7650            adapter
7651                .resolve_visible_frame(&cx, &snapshot, 99)
7652                .expect("resolve missing page"),
7653            WalPageLookupResolution::AuthoritativeMiss
7654        );
7655    }
7656
7657    #[test]
7658    fn test_committed_txns_since_page_uses_visible_frame_horizon() {
7659        let cx = test_cx();
7660        let vfs = MemoryVfs::new();
7661        let mut adapter = make_adapter(&vfs, &cx);
7662
7663        let p1 = sample_page(0x31);
7664        let p2 = sample_page(0x32);
7665        let p3 = sample_page(0x33);
7666
7667        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7668        adapter.append_frame(&cx, 2, &p2, 2).expect("commit tx1");
7669        adapter.append_frame(&cx, 3, &p3, 0).expect("append p3");
7670        adapter.append_frame(&cx, 2, &p2, 3).expect("commit tx2");
7671        // Durable-certificate contract: the visible frame horizon advances
7672        // only at publication; count txns against the published horizon.
7673        adapter.sync(&cx).expect("publish staged commits");
7674
7675        assert_eq!(
7676            adapter
7677                .committed_txns_since_page(&cx, 1)
7678                .expect("count txns since page 1"),
7679            1
7680        );
7681        assert_eq!(
7682            adapter
7683                .committed_txns_since_page(&cx, 2)
7684                .expect("count txns since page 2"),
7685            0
7686        );
7687        assert_eq!(
7688            adapter
7689                .committed_txns_since_page(&cx, 99)
7690                .expect("count txns since missing page"),
7691            2
7692        );
7693        assert_eq!(
7694            adapter
7695                .committed_txn_count(&cx)
7696                .expect("count visible transactions"),
7697            2
7698        );
7699    }
7700
7701    #[test]
7702    fn test_conflicting_pages_since_snapshot_detects_later_wal_writes() {
7703        let cx = test_cx();
7704        let vfs = MemoryVfs::new();
7705        let mut adapter = make_adapter(&vfs, &cx);
7706
7707        let p1 = sample_page(0x41);
7708        let p2_before = sample_page(0x42);
7709        let p2_after = sample_page(0x43);
7710        let p3 = sample_page(0x44);
7711
7712        adapter.append_frame(&cx, 1, &p1, 0).expect("append p1");
7713        adapter
7714            .append_frame(&cx, 2, &p2_before, 2)
7715            .expect("commit tx1");
7716        adapter.sync(&cx).expect("publish staged frames");
7717        adapter
7718            .begin_transaction(&cx)
7719            .expect("pin transaction snapshot");
7720        let pinned = adapter
7721            .pinned_read_snapshot()
7722            .expect("transaction should expose pinned WAL snapshot");
7723        let conflict_snapshot = TransactionConflictSnapshot {
7724            generation: pinned.generation,
7725            last_commit_frame: pinned.last_commit_frame,
7726            commit_count: pinned.commit_count,
7727            snapshot_db_size: 0,
7728        };
7729
7730        adapter
7731            .append_frame(&cx, 3, &p3, 0)
7732            .expect("append unrelated later page");
7733        adapter
7734            .append_frame(&cx, 2, &p2_after, 3)
7735            .expect("commit later page 2 update");
7736        // Publication gates conflict visibility exactly like reads: the
7737        // later commit must be published before it can conflict.
7738        adapter.sync(&cx).expect("publish later commit");
7739
7740        let conflicts = adapter
7741            .conflicting_pages_since_snapshot(&cx, conflict_snapshot, &[2, 99], &[])
7742            .expect("conflict check should scan later committed frames");
7743        assert_eq!(conflicts, vec![2]);
7744
7745        let unrelated = adapter
7746            .conflicting_pages_since_snapshot(&cx, conflict_snapshot, &[99], &[])
7747            .expect("unrelated page should stay conflict-free");
7748        assert!(unrelated.is_empty());
7749    }
7750
7751    // -- CheckpointTargetAdapterRef tests --
7752
7753    #[test]
7754    fn test_checkpoint_adapter_write_page() {
7755        let cx = test_cx();
7756        let mut writer = MockCheckpointPageWriter;
7757        let mut adapter = CheckpointTargetAdapterRef {
7758            writer: &mut writer,
7759        };
7760
7761        let page_no = PageNumber::new(1).expect("valid page number");
7762        adapter
7763            .write_page(&cx, page_no, &[0u8; 4096])
7764            .expect("write_page");
7765    }
7766
7767    #[test]
7768    fn test_checkpoint_adapter_truncate_db() {
7769        let cx = test_cx();
7770        let mut writer = MockCheckpointPageWriter;
7771        let mut adapter = CheckpointTargetAdapterRef {
7772            writer: &mut writer,
7773        };
7774
7775        adapter.truncate_db(&cx, 10).expect("truncate_db");
7776    }
7777
7778    #[test]
7779    fn test_checkpoint_adapter_sync_db() {
7780        let cx = test_cx();
7781        let mut writer = MockCheckpointPageWriter;
7782        let mut adapter = CheckpointTargetAdapterRef {
7783            writer: &mut writer,
7784        };
7785
7786        adapter.sync_db(&cx).expect("sync_db");
7787    }
7788
7789    #[test]
7790    fn test_checkpoint_adapter_as_dyn_target() {
7791        let cx = test_cx();
7792        let mut writer = MockCheckpointPageWriter;
7793        let mut adapter = CheckpointTargetAdapterRef {
7794            writer: &mut writer,
7795        };
7796
7797        // Verify it can be used as a trait object.
7798        let target: &mut dyn CheckpointTarget = &mut adapter;
7799        let page_no = PageNumber::new(3).expect("valid page number");
7800        target
7801            .write_page(&cx, page_no, &[0u8; 4096])
7802            .expect("write via dyn");
7803        target.truncate_db(&cx, 5).expect("truncate via dyn");
7804        target.sync_db(&cx).expect("sync via dyn");
7805    }
7806}