Skip to main content

fsqlite_core/
wal_adapter.rs

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