Skip to main content

fsqlite_pager/
traits.rs

1//! Storage trait hierarchy for MVCC pager and checkpoint operations.
2//!
3//! This module defines the sealed, internal-only traits that encode
4//! MVCC safety invariants. Only the defining crate can implement these
5//! traits.
6//!
7//! # Sealed Trait Discipline (§9)
8//!
9//! Internal traits use `mod sealed { pub trait Sealed {} }` so that
10//! downstream crates cannot provide alternate implementations.
11//!
12//! - **Sealed:** [`MvccPager`], [`TransactionHandle`], [`CheckpointPageWriter`]
13//! - **Open (user-implementable):** `Vfs`, `VfsFile` (in `fsqlite-vfs`)
14
15use std::collections::HashMap;
16use std::future::Future;
17use std::pin::Pin;
18
19use crate::pager::SimpleTransaction;
20use fsqlite_error::{FrankenError, Result};
21use fsqlite_types::cx::Cx;
22use fsqlite_types::{CommitSeq, PageData, PageNumber, PageSize};
23#[cfg(all(feature = "native", target_os = "linux"))]
24use fsqlite_vfs::IoUringVfs;
25#[cfg(all(feature = "native", unix))]
26use fsqlite_vfs::UnixVfs;
27#[cfg(all(feature = "native", target_os = "windows"))]
28use fsqlite_vfs::WindowsVfs;
29use fsqlite_vfs::{MemoryVfs, VfsWriteCompletion};
30use fsqlite_wal::{
31    ParallelWalCommitCertificate, TransactionConflictPageBaseline, TransactionConflictSnapshot,
32    WalGenerationIdentity, checksum::WalChecksumTransform,
33};
34
35// ---------------------------------------------------------------------------
36// Sealed trait discipline
37// ---------------------------------------------------------------------------
38
39/// Sealed trait module — prevents external crates from implementing
40/// internal traits that encode MVCC safety invariants.
41pub(crate) mod sealed {
42    /// Marker trait restricting implementation to this crate.
43    pub trait Sealed {}
44}
45
46// ---------------------------------------------------------------------------
47// Journal mode
48// ---------------------------------------------------------------------------
49
50/// The journal mode for database persistence (PRAGMA journal_mode).
51///
52/// Determines how changes are committed — either through a rollback journal
53/// (the default) or through a write-ahead log (WAL mode). WAL mode enables
54/// concurrent readers alongside a single writer without blocking.
55///
56/// Only `Delete` and `Wal` are currently supported; the remaining SQLite
57/// journal modes (`Truncate`, `Persist`, `Memory`, `Off`) may be added in
58/// future phases.
59#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
60pub enum JournalMode {
61    /// Rollback journal — the journal file is deleted after each commit.
62    /// This is the default mode.
63    #[default]
64    Delete,
65    /// Write-ahead log — frames are appended to a WAL file; checkpoints
66    /// transfer committed pages back to the database. Concurrent readers
67    /// see consistent snapshots without blocking the writer.
68    Wal,
69}
70
71// ---------------------------------------------------------------------------
72// WAL backend trait (open, for `fsqlite-core` adapter)
73// ---------------------------------------------------------------------------
74
75// ---------------------------------------------------------------------------
76// Checkpoint mode (mirrors fsqlite-wal::CheckpointMode without adding a dep)
77// ---------------------------------------------------------------------------
78
79/// Checkpoint mode for WAL checkpointing.
80///
81/// This mirrors `fsqlite_wal::CheckpointMode` but is defined here to avoid
82/// a circular dependency between `fsqlite-pager` and `fsqlite-wal`.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub enum CheckpointMode {
85    /// PASSIVE: Checkpoint as many frames as possible without blocking.
86    /// Does not wait for readers or acquire a write lock.
87    #[default]
88    Passive,
89    /// FULL: Checkpoint all frames, waiting for readers if necessary.
90    /// Does not reset the WAL.
91    Full,
92    /// RESTART: Like FULL, but also resets the WAL after completion.
93    Restart,
94    /// TRUNCATE: Like RESTART, but also truncates the WAL file to zero.
95    Truncate,
96}
97
98/// Result of a checkpoint operation.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct CheckpointResult {
101    /// Number of frames in the WAL before the checkpoint.
102    pub total_frames: u32,
103    /// Number of frames actually transferred to the database.
104    pub frames_backfilled: u32,
105    /// Whether the checkpoint completed (all frames transferred).
106    pub completed: bool,
107    /// Whether the WAL was reset after the checkpoint.
108    pub wal_was_reset: bool,
109    /// The mode the caller originally requested.
110    pub requested_mode: CheckpointMode,
111    /// The mode actually executed (may differ from `requested_mode` if the
112    /// pager conservatively downgraded due to safety constraints).
113    pub effective_mode: CheckpointMode,
114}
115
116/// Public summary of the commit-published WAL visibility plane.
117///
118/// This lets callers bind to generation-stamped WAL metadata without reaching
119/// into backend-specific page-index storage.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct WalPublicationSnapshot {
122    /// Monotonic publication sequence for this backend handle.
123    pub publication_seq: u64,
124    /// WAL generation visible through this publication.
125    pub generation: WalGenerationIdentity,
126    /// Latest visible commit frame for this generation, if any.
127    pub last_commit_frame: Option<usize>,
128    /// Number of committed transactions visible through this publication.
129    pub commit_count: u64,
130    /// Number of latest-frame entries published in the visibility map.
131    pub latest_frame_entries: usize,
132    /// Whether the page index is partial and may fall back to bounded scans.
133    pub index_is_partial: bool,
134}
135
136impl WalPublicationSnapshot {
137    #[must_use]
138    pub const fn lookup_contract_is_authoritative(self) -> bool {
139        !self.index_is_partial
140    }
141}
142
143/// Logical commit horizon bound to one already-pinned WAL read snapshot.
144///
145/// A physical WAL commit marker may represent more than one logical parallel
146/// commit. Implementations may report that wider horizon only when it is
147/// authorized for this exact WAL generation and final committed frame. It is
148/// intentionally distinct from a combiner clock seed: a checkpoint handoff
149/// from an earlier generation is never a reader-visible snapshot.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct WalLogicalReadSnapshot {
152    /// WAL generation containing the authorized logical horizon.
153    pub generation: WalGenerationIdentity,
154    /// Final committed WAL frame included by the logical horizon.
155    pub last_commit_frame: Option<usize>,
156    /// Global logical commit sequence visible through that exact horizon.
157    pub visible_commit_seq: CommitSeq,
158}
159
160/// Durable recovery verdict for one exact certificate/WAL interval.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum ParallelWalCommitReconciliation {
163    /// The live WAL generation contains the complete interval and its matching
164    /// commit marker, and the supplied certificate is the authorizing record.
165    Authorized,
166    /// Recovery proved that the interval has no matching committed marker.
167    NotCommitted,
168}
169
170/// Backend interface for WAL operations consumed by the pager.
171///
172/// This trait breaks the `pager ↔ wal` circular dependency: it is defined
173/// here in `fsqlite-pager` but implemented by an adapter in `fsqlite-core`
174/// that wraps `WalFile` from `fsqlite-wal`.
175///
176/// The pager calls into this trait during WAL-mode commits and page lookups
177/// instead of writing a rollback journal.
178pub type WalFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
179
180/// Source guard for the conservative tracked-WAL defaults.
181///
182/// Constructing the guard before the async block is returned is intentional:
183/// dropping an unpolled future must still make the caller-retained completion
184/// token terminal. `VfsWriteCompletion` has sticky terminal states, so the
185/// guard's final `Error` cannot overwrite an explicitly recorded `Success`.
186struct WalTrackedCompletionGuard(VfsWriteCompletion);
187
188impl WalTrackedCompletionGuard {
189    fn complete_success(&self) {
190        self.0.complete_success();
191    }
192
193    fn complete_error(&self) {
194        self.0.complete_error();
195    }
196}
197
198impl Drop for WalTrackedCompletionGuard {
199    fn drop(&mut self) {
200        self.0.complete_error();
201    }
202}
203
204pub trait WalBackend: Send + Sync {
205    /// Prepare WAL state for a newly-started transaction.
206    ///
207    /// Implementations may refresh internal snapshot metadata so reads during
208    /// this transaction see a coherent view without per-page refresh costs.
209    fn begin_transaction<'a>(&'a mut self, _cx: &'a Cx) -> WalFuture<'a, ()> {
210        Box::pin(async { Ok(()) })
211    }
212
213    /// Capture the currently published WAL visibility summary for this handle.
214    ///
215    /// Backends that do not maintain a commit-published visibility plane may
216    /// return `None`.
217    #[must_use]
218    fn published_snapshot(&self) -> Option<WalPublicationSnapshot> {
219        None
220    }
221
222    /// Capture the currently pinned read snapshot for this handle, if any.
223    ///
224    /// Backends that do not pin generation-stamped read snapshots may return
225    /// `None`.
226    #[must_use]
227    fn pinned_read_snapshot(&self) -> Option<WalPublicationSnapshot> {
228        None
229    }
230
231    /// Return an authorized logical horizon for the currently pinned reader
232    /// snapshot, if the backend can prove one.
233    ///
234    /// The returned generation and final frame must exactly match
235    /// [`Self::pinned_read_snapshot`]. Implementations must return `None` when
236    /// their only available certificate belongs to an earlier WAL generation.
237    fn pinned_logical_read_snapshot<'a>(
238        &'a self,
239        _cx: &'a Cx,
240    ) -> WalFuture<'a, Option<WalLogicalReadSnapshot>> {
241        Box::pin(async { Ok(None) })
242    }
243
244    /// Refresh the published WAL visibility summary without pinning a new
245    /// read transaction.
246    ///
247    /// The default implementation reports the current published snapshot
248    /// unchanged.
249    fn refresh_published_snapshot<'a>(
250        &'a mut self,
251        _cx: &'a Cx,
252    ) -> WalFuture<'a, Option<WalPublicationSnapshot>> {
253        Box::pin(async { Ok(self.published_snapshot()) })
254    }
255
256    /// Publish a commit batch that the pager's parallel-WAL protocol has
257    /// already authorized after every tracked write completed.
258    ///
259    /// This is distinct from [`Self::sync`]: `PRAGMA synchronous=NORMAL` may
260    /// make a completed WAL commit visible without forcing an fsync. Backends
261    /// that stage visibility until explicit authorization can override this
262    /// hook; backends without such staging need no action.
263    fn publish_authorized_deferred_commit<'a>(&'a mut self, _cx: &'a Cx) -> WalFuture<'a, ()> {
264        Box::pin(async { Ok(()) })
265    }
266
267    /// Append a single frame to the WAL.
268    ///
269    /// `page_number` is the 1-based database page.
270    /// `page_data` must be exactly `page_size` bytes.
271    /// `db_size_if_commit` is the database size in pages for commit frames,
272    /// or 0 for non-commit frames.
273    fn append_frame<'a>(
274        &'a mut self,
275        cx: &'a Cx,
276        page_number: u32,
277        page_data: &'a [u8],
278        db_size_if_commit: u32,
279    ) -> WalFuture<'a, ()>;
280
281    /// Append a batch of frames to the WAL.
282    ///
283    /// The default path preserves existing behavior by delegating to
284    /// [`Self::append_frame`] one frame at a time.
285    fn append_frames<'a>(
286        &'a mut self,
287        cx: &'a Cx,
288        frames: &'a [WalFrameRef<'a>],
289    ) -> WalFuture<'a, ()> {
290        Box::pin(async move {
291            for frame in frames {
292                self.append_frame(
293                    cx,
294                    frame.page_number,
295                    frame.page_data,
296                    frame.db_size_if_commit,
297                )
298                .await?;
299            }
300            Ok(())
301        })
302    }
303
304    /// Append a batch while retaining a source-level completion observation.
305    ///
306    /// A backend whose physical write can outlive this returned future must
307    /// override this method and complete `completion` at that source. The
308    /// conservative default records `Error` when the returned future is
309    /// dropped, including before its first poll. That terminal state means
310    /// "the wrapper did not observe success", not "zero bytes reached storage";
311    /// exact reconciliation must still classify the live WAL boundary.
312    fn append_frames_tracked<'a>(
313        &'a mut self,
314        cx: &'a Cx,
315        frames: &'a [WalFrameRef<'a>],
316        completion: VfsWriteCompletion,
317    ) -> WalFuture<'a, ()> {
318        let completion = WalTrackedCompletionGuard(completion);
319        Box::pin(async move {
320            let result = self.append_frames(cx, frames).await;
321            if result.is_ok() {
322                completion.complete_success();
323            } else {
324                completion.complete_error();
325            }
326            result
327        })
328    }
329
330    /// Prepare a batch of frames for a later append.
331    ///
332    /// Implementations may use this to move pure serialization and copy work
333    /// ahead of the serialized append window. Returning `None` keeps the
334    /// existing `append_frames` path.
335    fn prepare_append_frames(
336        &self,
337        _frames: &[WalFrameRef<'_>],
338    ) -> Result<Option<PreparedWalFrameBatch>> {
339        Ok(None)
340    }
341
342    /// Optionally finalize a prepared batch before the serialized append.
343    ///
344    /// Backends can use this hook to move seed-dependent checksum stamping or
345    /// similar pure compute out of the exclusive publish window. Callers must
346    /// still tolerate the backend redoing that work later if the live append
347    /// state changed before the actual write.
348    fn finalize_prepared_frames(
349        &self,
350        _cx: &Cx,
351        _prepared: &mut PreparedWalFrameBatch,
352    ) -> Result<()> {
353        Ok(())
354    }
355
356    /// Append a previously prepared frame batch.
357    ///
358    /// The default path rebuilds borrowed frame refs and delegates back to
359    /// [`Self::append_frames`]. Backends that can preserve more pre-serialized
360    /// state should override this.
361    fn append_prepared_frames<'a>(
362        &'a mut self,
363        cx: &'a Cx,
364        prepared: &'a mut PreparedWalFrameBatch,
365    ) -> WalFuture<'a, ()> {
366        Box::pin(async move {
367            for index in 0..prepared.frame_count() {
368                let meta = prepared.frame_metas[index];
369                self.append_frame(
370                    cx,
371                    meta.page_number,
372                    prepared.page_data(index),
373                    meta.db_size_if_commit,
374                )
375                .await?;
376            }
377            Ok(())
378        })
379    }
380
381    /// Append a prepared batch with a caller-retained completion token.
382    fn append_prepared_frames_tracked<'a>(
383        &'a mut self,
384        cx: &'a Cx,
385        prepared: &'a mut PreparedWalFrameBatch,
386        completion: VfsWriteCompletion,
387    ) -> WalFuture<'a, ()> {
388        let completion = WalTrackedCompletionGuard(completion);
389        Box::pin(async move {
390            let result = self.append_prepared_frames(cx, prepared).await;
391            if result.is_ok() {
392                completion.complete_success();
393            } else {
394                completion.complete_error();
395            }
396            result
397        })
398    }
399
400    /// Append the certificate proof that authorizes the next WAL frame
401    /// interval. Implementations must bind the record to their current WAL
402    /// generation and make it durable when `sync` is true.
403    ///
404    /// `sync` is the transaction's existing WAL synchronous policy. `false`
405    /// preserves SQLite-style synchronous-OFF semantics: the ordered VFS write
406    /// must precede the WAL marker write, but neither write claims stable-media
407    /// survival across power loss. The receipt is therefore policy-relative,
408    /// never a stronger persistence guarantee than the matching WAL commit.
409    ///
410    /// The record is written before the interval's commit marker. A crash may
411    /// therefore leave an orphan certificate, which recovery must ignore
412    /// unless the matching generation, complete interval, and commit marker
413    /// are all present.
414    fn persist_parallel_wal_commit_certificate<'a>(
415        &'a mut self,
416        _cx: &'a Cx,
417        _certificate: &'a ParallelWalCommitCertificate,
418        _wal_frame_start: u64,
419        _wal_frame_end: u64,
420        _sync: bool,
421    ) -> WalFuture<'a, ()> {
422        Box::pin(async { Err(FrankenError::Unsupported) })
423    }
424
425    /// Persist the certificate sidecar write with source-level completion
426    /// evidence retained independently of this future.
427    fn persist_parallel_wal_commit_certificate_tracked<'a>(
428        &'a mut self,
429        cx: &'a Cx,
430        certificate: &'a ParallelWalCommitCertificate,
431        wal_frame_start: u64,
432        wal_frame_end: u64,
433        sync: bool,
434        completion: VfsWriteCompletion,
435    ) -> WalFuture<'a, ()> {
436        let completion = WalTrackedCompletionGuard(completion);
437        Box::pin(async move {
438            let result = self
439                .persist_parallel_wal_commit_certificate(
440                    cx,
441                    certificate,
442                    wal_frame_start,
443                    wal_frame_end,
444                    sync,
445                )
446                .await;
447            if result.is_ok() {
448                completion.complete_success();
449            } else {
450                completion.complete_error();
451            }
452            result
453        })
454    }
455
456    /// Reconcile one exact in-doubt certificate and WAL interval while the
457    /// caller retains the external writer gate.
458    ///
459    /// Implementations must validate the live WAL generation, complete frame
460    /// boundaries, the interval's commit marker, and the exact certificate.
461    /// `Error` completion tokens are not evidence of zero bytes. On
462    /// [`ParallelWalCommitReconciliation::Authorized`], a synchronous policy
463    /// must re-establish the required sidecar, WAL, and directory durability
464    /// fences before returning. On `NotCommitted`, any incomplete tail must be
465    /// repaired before the ordered combiner residue may be aborted.
466    fn reconcile_parallel_wal_commit<'a>(
467        &'a mut self,
468        _cx: &'a Cx,
469        _certificate: &'a ParallelWalCommitCertificate,
470        _wal_frame_start: u64,
471        _wal_frame_end: u64,
472        _sync: bool,
473    ) -> WalFuture<'a, ParallelWalCommitReconciliation> {
474        Box::pin(async { Err(FrankenError::Unsupported) })
475    }
476
477    /// Return the newest durable certificate usable to seed the next logical
478    /// commit clock.
479    ///
480    /// A current-generation record is authorized against its complete frame
481    /// boundary and commit marker. After a checkpoint reset, a file-backed
482    /// backend may instead return the persisted certificate handoff from the
483    /// previous generation solely to continue the writer-side clock. That
484    /// handoff is not reader visibility; use
485    /// [`Self::pinned_logical_read_snapshot`] for a generation-bound reader
486    /// horizon. Backends without a cross-process durable namespace have no
487    /// seed.
488    fn latest_authorized_parallel_wal_commit_certificate<'a>(
489        &'a mut self,
490        _cx: &'a Cx,
491    ) -> WalFuture<'a, Option<ParallelWalCommitCertificate>> {
492        Box::pin(async { Ok(None) })
493    }
494
495    /// Look up the latest version of a page in the current visible WAL snapshot.
496    ///
497    /// Implementations should prefer an authoritative per-generation lookup
498    /// structure for the steady-state path. Any slower fallback path should be
499    /// explicit and reserved for exceptional cases such as a deliberately
500    /// partial index or recovery-oriented handling.
501    fn read_page<'a>(&'a mut self, cx: &'a Cx, page_number: u32) -> WalFuture<'a, Option<Vec<u8>>>;
502
503    /// bd-dw8oe: look up the latest version of a page in the PHYSICAL appended
504    /// WAL tail, ignoring any reader-visibility publication plane.
505    ///
506    /// The published snapshot used by [`Self::read_page`] may clamp to the
507    /// fsynced prefix (deferred-sync), hiding PEER commits that are appended
508    /// but not yet fsynced. Append-gate guards that validate against "the
509    /// current durable freelist / header" must see every appended frame, or a
510    /// flusher refreshing mid-flush republishes stale page-1 freelist bytes
511    /// over a peer's just-published state (the bd-dw8oe trunk-corruption
512    /// mechanism: head=753 republished nine frames after a peer published
513    /// head=0). Only call this while the append gate (backend write lock plus
514    /// commit-serialization lock) is held: under the gate the appended tail is
515    /// stable and every appended frame belongs to a completed batch.
516    ///
517    /// The default implementation falls back to [`Self::read_page`], which is
518    /// exact for backends whose read plane never lags their appended tail.
519    fn read_page_at_appended_tail<'a>(
520        &'a mut self,
521        cx: &'a Cx,
522        page_number: u32,
523    ) -> WalFuture<'a, Option<Vec<u8>>> {
524        self.read_page(cx, page_number)
525    }
526
527    /// Read a page from the WAL using a previously pinned read snapshot.
528    ///
529    /// This method takes `&self` instead of `&mut self`, enabling callers to
530    /// hold only a shared (read) lock on the WAL backend when the transaction
531    /// has already pinned its snapshot via `begin_transaction`.
532    ///
533    /// The default implementation falls back to `read_page(&mut self)` which
534    /// requires exclusive access. Implementors that can serve reads from an
535    /// immutable pinned snapshot should override this to avoid contention with
536    /// the append path.
537    ///
538    /// # bd-db300.3.8.7: write-lock-scope narrowing
539    fn read_page_pinned<'a>(
540        &'a self,
541        _cx: &'a Cx,
542        _page_number: u32,
543    ) -> WalFuture<'a, Option<Vec<u8>>> {
544        Box::pin(async {
545            // Default: signal that the implementation doesn't support pinned reads.
546            // Callers must fall back to read_page(&mut self) via write lock.
547            Err(FrankenError::internal(
548                "read_page_pinned not supported by this WalBackend; use read_page",
549            ))
550        })
551    }
552
553    /// Whether this backend supports `read_page_pinned` (shared-lock reads).
554    ///
555    /// Callers check this before choosing the read vs write lock path.
556    fn supports_pinned_reads(&self) -> bool {
557        false
558    }
559
560    /// Count committed transactions that occur after the latest committed
561    /// frame for `page_number` in the current visible WAL snapshot.
562    ///
563    /// This lets the pager derive an exact visible commit sequence even when a
564    /// WAL commit does not need to rewrite page 1. Implementations may return
565    /// 0 when they cannot provide a more precise answer.
566    fn committed_txns_since_page<'a>(
567        &'a mut self,
568        _cx: &'a Cx,
569        _page_number: u32,
570    ) -> WalFuture<'a, u64> {
571        Box::pin(async { Ok(0) })
572    }
573
574    /// Return conflict pages that were committed after `snapshot`.
575    ///
576    /// This is the cross-process half of first-committer-wins. The
577    /// connection-local MVCC registry protects writers in one process, but a
578    /// WAL flusher can also receive batches from transactions whose stale page
579    /// images race with commits made by another process. Implementations that
580    /// can inspect the WAL frame stream should reject those stale batches
581    /// before append.
582    fn conflicting_pages_since_snapshot<'a>(
583        &'a mut self,
584        _cx: &'a Cx,
585        _snapshot: TransactionConflictSnapshot,
586        _page_numbers: &'a [u32],
587        _page_baselines: &'a [TransactionConflictPageBaseline],
588    ) -> WalFuture<'a, Vec<u32>> {
589        Box::pin(async { Ok(Vec::new()) })
590    }
591
592    /// Count committed transactions visible in the current WAL snapshot.
593    ///
594    /// This lets the pager derive a connection-local visible commit sequence
595    /// from the durable database header change-counter plus the currently
596    /// visible WAL commit horizon, without depending on whether page 1 was
597    /// rewritten in recent WAL commits.
598    fn committed_txn_count<'a>(&'a mut self, _cx: &'a Cx) -> WalFuture<'a, u64> {
599        Box::pin(async { Ok(0) })
600    }
601
602    /// Sync the WAL file to stable storage.
603    fn sync(&mut self, cx: &Cx) -> Result<()>;
604
605    /// Number of valid frames currently in the WAL.
606    fn frame_count(&self) -> usize;
607
608    /// Run a checkpoint to transfer frames from the WAL to the database.
609    ///
610    /// Takes a `CheckpointPageWriter` that handles the actual page writes
611    /// to the database file. The writer is typically provided by the pager.
612    ///
613    /// # Arguments
614    ///
615    /// * `cx` - Cancellation/deadline context
616    /// * `mode` - Checkpoint mode (Passive, Full, Restart, Truncate)
617    /// * `writer` - Writer to transfer pages to the database file
618    /// * `backfilled_frames` - Number of frames already backfilled (for resume)
619    /// * `oldest_reader_frame` - Frame index of oldest active reader (None if no readers)
620    ///
621    /// # Returns
622    ///
623    /// A `CheckpointResult` describing what was accomplished.
624    fn checkpoint<'a>(
625        &'a mut self,
626        cx: &'a Cx,
627        mode: CheckpointMode,
628        writer: &'a mut dyn CheckpointPageWriter,
629        backfilled_frames: u32,
630        oldest_reader_frame: Option<u32>,
631    ) -> WalFuture<'a, CheckpointResult>;
632}
633
634/// Borrowed frame descriptor used for WAL batch appends.
635#[derive(Debug, Clone, Copy)]
636pub struct WalFrameRef<'a> {
637    /// Database page number this frame writes.
638    pub page_number: u32,
639    /// Page data for the frame. Must be exactly `page_size` bytes.
640    pub page_data: &'a [u8],
641    /// Database size in pages for commit frames, or 0 for non-commit frames.
642    pub db_size_if_commit: u32,
643}
644
645/// Metadata describing one frame within a prepared WAL batch.
646#[derive(Debug, Clone, Copy, PartialEq, Eq)]
647pub struct PreparedWalFrameMeta {
648    /// Database page number this frame writes.
649    pub page_number: u32,
650    /// Database size in pages for commit frames, or 0 for non-commit frames.
651    pub db_size_if_commit: u32,
652}
653
654/// Affine checksum transform for one prepared WAL frame.
655///
656/// Alias the canonical WAL transform type so prepared batches can flow through
657/// finalize/append paths without a per-frame transform copy.
658pub type PreparedWalChecksumTransform = WalChecksumTransform;
659
660/// Rolling-checksum seed/result captured for a prepared WAL batch.
661#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
662pub struct PreparedWalChecksumSeed {
663    /// First checksum word.
664    pub s1: u32,
665    /// Second checksum word.
666    pub s2: u32,
667}
668
669/// Live WAL state that a prepared batch was finalized against.
670///
671/// This lets the append path cheaply decide whether a pre-lock finalize pass
672/// is still valid once the serialized publish window opens.
673#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
674pub struct PreparedWalFinalizationState {
675    /// WAL checkpoint sequence for the generation being appended to.
676    pub checkpoint_seq: u32,
677    /// WAL salt1 for the generation being appended to.
678    pub salt1: u32,
679    /// WAL salt2 for the generation being appended to.
680    pub salt2: u32,
681    /// Frame index where this batch expects to start appending.
682    pub start_frame_index: usize,
683    /// Rolling checksum seed seen before finalizing this batch.
684    pub seed: PreparedWalChecksumSeed,
685}
686
687/// Owned WAL batch representation that can be prepared before append.
688#[derive(Debug, Clone, PartialEq, Eq)]
689pub struct PreparedWalFrameBatch {
690    /// Byte width of each serialized frame record.
691    pub frame_size: usize,
692    /// Offset of the page payload inside each serialized frame record.
693    pub page_data_offset: usize,
694    /// Whether checksum words use big-endian encoding for transform derivation.
695    pub big_endian_checksum: bool,
696    /// Per-frame metadata in order.
697    pub frame_metas: Vec<PreparedWalFrameMeta>,
698    /// Per-frame checksum transforms in order.
699    pub checksum_transforms: Vec<PreparedWalChecksumTransform>,
700    /// Serialized frame bytes in order.
701    pub frame_bytes: Vec<u8>,
702    /// Offset of the last commit frame inside this batch, if any.
703    pub last_commit_frame_offset: Option<usize>,
704    /// WAL state that `frame_bytes` were last finalized against.
705    pub finalized_for: Option<PreparedWalFinalizationState>,
706    /// Final running checksum after the last finalize pass.
707    pub finalized_running_checksum: Option<PreparedWalChecksumSeed>,
708}
709
710impl PreparedWalFrameBatch {
711    /// Number of frames carried by this batch.
712    #[must_use]
713    pub fn frame_count(&self) -> usize {
714        self.frame_metas.len()
715    }
716
717    /// Page size carried by each prepared frame.
718    #[must_use]
719    pub fn page_size(&self) -> usize {
720        self.frame_size.saturating_sub(self.page_data_offset)
721    }
722
723    /// Borrow this batch as pager-facing frame refs.
724    #[must_use]
725    pub fn frame_refs(&self) -> Vec<WalFrameRef<'_>> {
726        self.frame_metas
727            .iter()
728            .enumerate()
729            .map(|(index, meta)| {
730                let frame_start = index * self.frame_size;
731                let page_start = frame_start + self.page_data_offset;
732                let page_end = frame_start + self.frame_size;
733                WalFrameRef {
734                    page_number: meta.page_number,
735                    page_data: &self.frame_bytes[page_start..page_end],
736                    db_size_if_commit: meta.db_size_if_commit,
737                }
738            })
739            .collect()
740    }
741
742    /// Borrow the page payload for a prepared frame.
743    #[must_use]
744    pub fn page_data(&self, index: usize) -> &[u8] {
745        let frame_start = index * self.frame_size;
746        let page_start = frame_start + self.page_data_offset;
747        let page_end = frame_start + self.frame_size;
748        &self.frame_bytes[page_start..page_end]
749    }
750
751    /// Borrow the full serialized frame record at `index`.
752    #[must_use]
753    pub fn frame_slice(&self, index: usize) -> &[u8] {
754        let frame_start = index * self.frame_size;
755        let frame_end = frame_start + self.frame_size;
756        &self.frame_bytes[frame_start..frame_end]
757    }
758
759    /// Update the commit-marker db-size for one frame and clear stale finalize state.
760    pub fn set_db_size_if_commit(&mut self, index: usize, db_size_if_commit: u32) {
761        self.frame_metas[index].db_size_if_commit = db_size_if_commit;
762        let frame_start = index * self.frame_size;
763        let db_size_offset = frame_start + 4;
764        self.frame_bytes[db_size_offset..db_size_offset + 4]
765            .copy_from_slice(&db_size_if_commit.to_be_bytes());
766        self.finalized_for = None;
767        self.finalized_running_checksum = None;
768    }
769
770    /// Recompute checksum transforms after header-level metadata changes.
771    pub fn recompute_checksum_transforms(&mut self) -> Result<()> {
772        let page_size = self.page_size();
773        self.checksum_transforms = (0..self.frame_count())
774            .map(|index| {
775                WalChecksumTransform::for_wal_frame(
776                    self.frame_slice(index),
777                    page_size,
778                    self.big_endian_checksum,
779                )
780            })
781            .collect::<Result<Vec<_>>>()?;
782        self.finalized_for = None;
783        self.finalized_running_checksum = None;
784        Ok(())
785    }
786}
787
788// ---------------------------------------------------------------------------
789// Transaction mode
790// ---------------------------------------------------------------------------
791
792/// How a transaction should be opened.
793///
794/// Matches SQLite's `BEGIN [DEFERRED|IMMEDIATE|EXCLUSIVE]` semantics
795/// adapted for MVCC.
796#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
797pub enum TransactionMode {
798    /// Deferred: starts as read-only, upgrades to writer on first write.
799    /// This is the default mode.
800    #[default]
801    Deferred,
802    /// Immediate: acquires write intent at `BEGIN` time. Corresponds to
803    /// `BEGIN IMMEDIATE` in SQLite. Under MVCC this takes a reservation
804    /// on the serialized writer token.
805    Immediate,
806    /// Exclusive: like Immediate but also prevents new readers from
807    /// starting. Used for schema changes and `VACUUM`.
808    Exclusive,
809    /// Concurrent: `BEGIN CONCURRENT` mode.
810    ///
811    /// This is the MVCC concurrent-writer entry point from the SQL layer.
812    /// Pager implementations may initially map it to deferred semantics,
813    /// but must preserve the mode so upper layers can engage concurrent
814    /// conflict detection/commit paths.
815    Concurrent,
816    /// Read-only: the transaction will never write. The pager can skip
817    /// SSI bookkeeping and use a lightweight snapshot.
818    ReadOnly,
819}
820
821// ---------------------------------------------------------------------------
822// MvccPager — primary storage interface
823// ---------------------------------------------------------------------------
824
825/// The MVCC-aware page-level storage interface.
826///
827/// This is the primary interface consumed by the B-tree layer and VDBE.
828/// It supports multiple concurrent transactions from different threads,
829/// with internal locking (version store `RwLock`, lock table `Mutex`).
830///
831/// The pager outlives all transactions it creates (via `Arc`).
832///
833/// # Cx Everywhere
834///
835/// Every method that touches I/O, acquires locks, or could block accepts
836/// `&Cx` for cancellation and deadline propagation (§9 cross-cutting rule).
837///
838/// # Sealed
839///
840/// This trait is sealed — only this crate can implement it.
841pub trait MvccPager: sealed::Sealed + Send + Sync {
842    /// The transaction handle type produced by this pager.
843    type Txn: TransactionHandle;
844
845    /// Begin a new transaction.
846    ///
847    /// Returns a [`TransactionHandle`] that provides page-level access
848    /// within the transaction's snapshot. The handle is `Send` so it
849    /// can be moved to another thread if needed.
850    fn begin<'a>(
851        &'a self,
852        cx: &'a Cx,
853        mode: TransactionMode,
854    ) -> impl Future<Output = Result<Self::Txn>> + 'a;
855
856    /// Return the current journal mode.
857    fn journal_mode(&self) -> JournalMode;
858
859    /// Whether this pager was opened read-only.
860    fn is_readonly(&self) -> bool;
861
862    /// Switch the journal mode.
863    ///
864    /// Switching from `Delete` to `Wal` requires providing a [`WalBackend`]
865    /// via [`set_wal_backend`](Self::set_wal_backend) first; otherwise the
866    /// call returns `FrankenError::Unsupported`.
867    ///
868    /// Returns the mode that is actually in effect after the call.
869    fn set_journal_mode<'a>(
870        &'a self,
871        cx: &'a Cx,
872        mode: JournalMode,
873    ) -> impl Future<Output = Result<JournalMode>> + 'a;
874
875    /// Install a WAL backend for WAL-mode operation.
876    ///
877    /// The backend is consumed and stored internally. It must be set before
878    /// calling `set_journal_mode(Wal)`.
879    fn set_wal_backend(&self, backend: Box<dyn WalBackend>) -> Result<()>;
880}
881
882// ---------------------------------------------------------------------------
883// TransactionHandle
884// ---------------------------------------------------------------------------
885
886/// Pager-owned state of one physical commit attempt.
887///
888/// `Result<()>` alone cannot distinguish a failure before WAL acceptance from
889/// an error observed after the commit marker became durable. Upper layers must
890/// only run rollback semantics for [`NotCommitted`](Self::NotCommitted);
891/// every other nonterminal state retains a commit obligation that must be
892/// reconciled by retrying the same transaction handle.
893#[derive(Debug, Clone, Copy, PartialEq, Eq)]
894pub enum PagerCommitState {
895    /// No physical commit is pending and rollback is still permitted.
896    NotCommitted,
897    /// Physical I/O may have started, but the exact WAL verdict is not final.
898    InDoubt,
899    /// Durability is authorized; pager publication/finalization remains.
900    DurableNeedsPublication,
901    /// Pager durability and publication are terminally committed.
902    Committed,
903}
904
905impl PagerCommitState {
906    /// Whether rollback must not interpret the current attempt as uncommitted.
907    #[must_use]
908    pub const fn retains_commit_obligation(self) -> bool {
909        !matches!(self, Self::NotCommitted)
910    }
911}
912
913/// A handle to an active MVCC transaction.
914///
915/// Provides page-level read/write access scoped to the transaction's
916/// snapshot. Dropping a handle without calling [`commit`](Self::commit)
917/// implicitly rolls back.
918///
919/// # Page resolution chain
920///
921/// `get_page` resolves through: write-set → version chain → disk.
922/// SSI `WitnessKey` tracking records which pages were read.
923///
924/// # Sealed
925///
926/// This trait is sealed — only this crate can implement it.
927pub trait TransactionHandle: sealed::Sealed + Send {
928    /// Read a page, resolving through the MVCC version chain.
929    ///
930    /// Resolution order: local write-set → version chain → on-disk.
931    /// Records the read in SSI witness tracking for conflict detection
932    /// at commit time.
933    fn get_page<'a>(
934        &'a self,
935        cx: &'a Cx,
936        page_no: PageNumber,
937    ) -> impl Future<Output = Result<PageData>> + 'a;
938
939    /// Hint that `page_no` is likely to be read soon.
940    ///
941    /// Implementations should keep this best-effort and non-blocking. It is
942    /// purely a latency-hiding hint and must not affect correctness.
943    fn prefetch_page_hint(&self, _cx: &Cx, _page_no: PageNumber) {}
944
945    /// Drop a just-freed page's buffer from the shared page cache.
946    ///
947    /// Default is a no-op. Pager-backed transactions override it to evict the
948    /// buffer so the bounded DROP teardown path (bd-pirr5, GH#371) releases each
949    /// freed page's memory instead of retaining the whole b-tree in the pool.
950    /// The page is already on the freelist and re-read fresh if it is
951    /// re-allocated, so this only bounds resident memory and never affects
952    /// correctness.
953    fn forget_cached_page(&self, _page_no: PageNumber) {}
954
955    /// Write a page within this transaction.
956    ///
957    /// Acquires a page-level lock and records the write for SSI
958    /// validation at commit time.
959    fn write_page<'a>(
960        &'a mut self,
961        cx: &'a Cx,
962        page_no: PageNumber,
963        data: &'a [u8],
964    ) -> impl Future<Output = Result<()>> + 'a;
965
966    /// Write owned page data within this transaction.
967    ///
968    /// The default implementation borrows the page bytes, but implementations
969    /// can override this to adopt owned buffers without another copy.
970    fn write_page_data<'a>(
971        &'a mut self,
972        cx: &'a Cx,
973        page_no: PageNumber,
974        data: PageData,
975    ) -> impl Future<Output = Result<()>> + 'a {
976        async move { self.write_page(cx, page_no, data.as_bytes()).await }
977    }
978
979    /// Temporarily take ownership of an unpublished staged page image.
980    ///
981    /// This exists for hot B-tree append paths that want to mutate the
982    /// transaction's authoritative staged page without cloning a separate
983    /// compatibility copy first. Implementations may return `None` when the
984    /// staged page is unavailable or has already been published for read reuse.
985    fn try_take_staged_page_data(&mut self, _page_no: PageNumber) -> Option<PageData> {
986        None
987    }
988
989    /// Mutate an unpublished staged page image in place.
990    ///
991    /// This is the cheapest hot-path option for repeated right-edge writes:
992    /// the transaction already owns the authoritative staged page, so callers
993    /// can patch it without removing and re-inserting the page in the write-set.
994    fn try_mutate_staged_page_data(
995        &mut self,
996        _page_no: PageNumber,
997        _f: &mut dyn FnMut(&mut PageData),
998    ) -> bool {
999        false
1000    }
1001
1002    /// Restore a page image previously taken with `try_take_staged_page_data`.
1003    ///
1004    /// The default implementation routes through `write_page_data`, which is
1005    /// correct but may copy. Implementations can override this to restore the
1006    /// staged page without extra allocation.
1007    fn restore_staged_page_data<'a>(
1008        &'a mut self,
1009        cx: &'a Cx,
1010        page_no: PageNumber,
1011        data: PageData,
1012    ) -> impl Future<Output = Result<()>> + 'a {
1013        async move { self.write_page_data(cx, page_no, data).await }
1014    }
1015
1016    /// Allocate a new page and return its page number.
1017    ///
1018    /// Searches the freelist first, then extends the database file.
1019    fn allocate_page<'a>(&'a mut self, cx: &'a Cx)
1020    -> impl Future<Output = Result<PageNumber>> + 'a;
1021
1022    /// Free a page, returning it to the freelist.
1023    fn free_page<'a>(
1024        &'a mut self,
1025        cx: &'a Cx,
1026        page_no: PageNumber,
1027    ) -> impl Future<Output = Result<()>> + 'a;
1028
1029    /// Commit this transaction.
1030    ///
1031    /// Performs SSI validation, First-Committer-Wins check, merge ladder,
1032    /// WAL append, and version publish. Returns `SQLITE_BUSY_SNAPSHOT`
1033    /// (via `FrankenError::Busy`) on serialization failure.
1034    fn commit<'a>(&'a mut self, cx: &'a Cx) -> impl Future<Output = Result<()>> + 'a;
1035
1036    /// Return the pager-owned physical commit state for this exact handle.
1037    ///
1038    /// Implementations must keep this state monotonic once durability is
1039    /// authorized: cancellation or a later local error cannot turn
1040    /// `DurableNeedsPublication` back into `NotCommitted`.
1041    fn pager_commit_state(&self) -> PagerCommitState {
1042        PagerCommitState::NotCommitted
1043    }
1044
1045    /// Commit dirty pages and reset for immediate reuse without destroying
1046    /// the transaction handle.
1047    ///
1048    /// This is a performance optimization for `:memory:` autocommit: instead
1049    /// of commit + destroy + begin, we commit the write set and clear it for
1050    /// the next statement while keeping the transaction alive.  The pager's
1051    /// `writer_active` and `active_transactions` state remain set, avoiding
1052    /// a full begin/commit ceremony on the next statement.
1053    ///
1054    /// Returns `Ok(true)` if the transaction was retained and can be reused.
1055    /// Returns `Ok(false)` if retention is not supported (falls back to
1056    /// regular commit semantics — the caller should treat the transaction
1057    /// as finished).
1058    ///
1059    /// Default implementation falls back to regular `commit`.
1060    fn commit_and_retain<'a>(&'a mut self, cx: &'a Cx) -> impl Future<Output = Result<bool>> + 'a {
1061        async move {
1062            self.commit(cx).await?;
1063            Ok(false)
1064        }
1065    }
1066
1067    /// Whether this transaction has been upgraded to a writer.
1068    ///
1069    /// Read-only and deferred transactions that never dirtied a page must
1070    /// return `false` so upper layers do not synthesize commit sequences for
1071    /// no-op commits.
1072    fn is_writer(&self) -> bool;
1073
1074    /// Whether this transaction still has net page changes to publish.
1075    ///
1076    /// This can become `false` again after `ROLLBACK TO` discards all pending
1077    /// writes, even if the transaction had previously upgraded to writer mode.
1078    fn has_pending_writes(&self) -> bool;
1079
1080    /// Visible commit sequence bound to this transaction's current snapshot.
1081    ///
1082    /// Pager-backed transactions can expose this so upper layers reuse the
1083    /// transaction's own visibility boundary instead of re-binding against the
1084    /// global published plane mid-transaction.
1085    fn published_visible_commit_seq_hint(&self) -> Option<fsqlite_types::CommitSeq> {
1086        None
1087    }
1088
1089    /// Return the full set of pages this transaction would mutate if it
1090    /// committed right now, including commit-time metadata synthesis such as
1091    /// freelist trunk rewrites.
1092    fn pending_commit_pages(&self) -> Result<Vec<PageNumber>> {
1093        Ok(Vec::new())
1094    }
1095
1096    /// Return the subset of pending commit pages that must participate in
1097    /// MVCC conflict tracking for concurrent commit planning.
1098    ///
1099    /// Pager-backed implementations may exclude commit-time-only synthetic
1100    /// metadata pages here when those bytes are reconciled under a serialized
1101    /// commit critical section and therefore do not represent true
1102    /// user-visible overlap.
1103    fn pending_conflict_pages(&self) -> Result<Vec<PageNumber>> {
1104        self.pending_commit_pages()
1105    }
1106
1107    /// Lock-free conservative conflict estimate for commit planning
1108    /// (bd-3qeu9.4).
1109    ///
1110    /// Implementations whose commits can mutate pages outside their explicit
1111    /// write set (for example, freed pages or freelist metadata) must override
1112    /// this method with a correctness-preserving superset. A shared metadata
1113    /// page may be used as the conflict token when enumerating every synthesized
1114    /// metadata page would require the pager-inner lock. The default is suitable
1115    /// only for implementations whose entire mutation surface is represented by
1116    /// `write_set_page_numbers()`.
1117    ///
1118    /// This avoids a redundant pager-inner lock acquisition on the commit hot
1119    /// path. The precise set remains available via `pending_conflict_pages()`
1120    /// when callers need exact commit-time page synthesis.
1121    fn pending_conflict_pages_conservative(&self) -> Vec<PageNumber> {
1122        self.write_set_page_numbers()
1123    }
1124
1125    /// Sorted page numbers in the current write set, without locking.
1126    /// Default returns empty; pager-backed implementations override.
1127    fn write_set_page_numbers(&self) -> Vec<PageNumber> {
1128        Vec::new()
1129    }
1130
1131    /// Whether page 1 is currently part of this transaction's pending commit
1132    /// surface, including commit-time allocator/header synthesis.
1133    fn page_one_in_pending_commit_surface(&self) -> Result<bool> {
1134        Ok(self.pending_commit_pages()?.contains(&PageNumber::ONE))
1135    }
1136
1137    /// Returns the transaction's effective database page size.
1138    ///
1139    /// Real pager-backed transactions override this so upper layers can
1140    /// normalize owned page buffers before staging them in MVCC state.
1141    fn page_size(&self) -> PageSize {
1142        PageSize::default()
1143    }
1144
1145    /// Whether calling [`allocate_page`](Self::allocate_page) right now must
1146    /// add page 1 to the MVCC conflict surface before the underlying allocator
1147    /// state changes.
1148    ///
1149    /// Real pager-backed transactions override this with exact allocator
1150    /// semantics so upper layers can avoid false page-1 conflicts on net-zero
1151    /// allocator churn or commit-time-only metadata updates. The default
1152    /// remains conservative.
1153    fn allocate_page_requires_page_one_conflict_tracking(&self) -> Result<bool> {
1154        Ok(true)
1155    }
1156
1157    /// Whether calling [`free_page`](Self::free_page) for `page_no` right now
1158    /// must add page 1 to the MVCC conflict surface before the underlying
1159    /// allocator state changes.
1160    ///
1161    /// Real pager-backed transactions override this with exact allocator
1162    /// semantics so upper layers can avoid false page-1 conflicts on net-zero
1163    /// allocator churn or commit-time-only metadata updates. The default
1164    /// remains conservative.
1165    fn free_page_requires_page_one_conflict_tracking(&self, _page_no: PageNumber) -> Result<bool> {
1166        Ok(true)
1167    }
1168
1169    /// Whether calling [`write_page`](Self::write_page) or
1170    /// [`write_page_data`](Self::write_page_data) for `page_no` right now must
1171    /// add page 1 to the MVCC conflict surface before the underlying page
1172    /// state changes.
1173    ///
1174    /// Real pager-backed transactions override this with exact growth
1175    /// semantics so upper layers can defer page-1 tracking until a newly
1176    /// allocated high page actually becomes part of the pending commit
1177    /// surface. The default remains conservative.
1178    fn write_page_requires_page_one_conflict_tracking(&self, _page_no: PageNumber) -> Result<bool> {
1179        Ok(true)
1180    }
1181
1182    /// Roll back this transaction, discarding the write-set.
1183    ///
1184    /// Rollback is infallible in the MVCC model (we simply discard the
1185    /// local write-set and release page locks), but returns `Result` for
1186    /// consistency with the trait surface.
1187    fn rollback<'a>(&'a mut self, cx: &'a Cx) -> impl Future<Output = Result<()>> + 'a;
1188
1189    /// Record a granular write witness for fine-grained SSI bookkeeping.
1190    ///
1191    /// Simple pager-backed transactions may ignore this, but concurrent MVCC
1192    /// implementations can override it to feed witness-plane validation.
1193    fn record_write_witness(&mut self, _cx: &Cx, _key: fsqlite_types::WitnessKey) {}
1194
1195    /// Create a named savepoint, snapshotting the current write-set.
1196    ///
1197    /// Corresponds to SQL `SAVEPOINT name`. The snapshot captures the
1198    /// write-set and freed-pages state at this point so that
1199    /// [`rollback_to_savepoint`](Self::rollback_to_savepoint) can restore it.
1200    fn savepoint(&mut self, cx: &Cx, name: &str) -> Result<()>;
1201
1202    /// Release (collapse) a named savepoint without rolling back.
1203    ///
1204    /// Corresponds to SQL `RELEASE name`. All changes since the savepoint
1205    /// are kept, and the savepoint is removed from the stack. Savepoints
1206    /// created after the named one are also released.
1207    fn release_savepoint(&mut self, cx: &Cx, name: &str) -> Result<()>;
1208
1209    /// Roll back to a named savepoint, restoring the snapshotted state.
1210    ///
1211    /// Corresponds to SQL `ROLLBACK TO name`. The write-set and freed-pages
1212    /// are restored to their state at the time the savepoint was created.
1213    /// The savepoint itself is retained (it can be rolled back to again).
1214    /// Savepoints created after the named one are discarded.
1215    fn rollback_to_savepoint(&mut self, cx: &Cx, name: &str) -> Result<()>;
1216}
1217
1218// ---------------------------------------------------------------------------
1219// CheckpointPageWriter
1220// ---------------------------------------------------------------------------
1221
1222/// A write-back interface used during WAL checkpointing.
1223///
1224/// This trait breaks the `pager ↔ wal` circular dependency: it is
1225/// defined here in `fsqlite-pager` but passed to `fsqlite-wal` at
1226/// runtime from `fsqlite-core`.
1227///
1228/// # Sealed
1229///
1230/// This trait is sealed — only this crate can implement it.
1231pub trait CheckpointPageWriter: sealed::Sealed + Send {
1232    /// Write a page directly to the database file (bypassing the cache).
1233    fn write_page<'a>(
1234        &'a mut self,
1235        cx: &'a Cx,
1236        page_no: PageNumber,
1237        data: &'a [u8],
1238    ) -> WalFuture<'a, ()>;
1239
1240    /// Truncate the database file to `n_pages` pages.
1241    fn truncate<'a>(&'a mut self, cx: &'a Cx, n_pages: u32) -> WalFuture<'a, ()>;
1242
1243    /// Sync the database file to stable storage.
1244    fn sync<'a>(&'a mut self, cx: &'a Cx) -> WalFuture<'a, ()>;
1245}
1246
1247// ---------------------------------------------------------------------------
1248// Exported test mocks (cross-crate)
1249// ---------------------------------------------------------------------------
1250
1251/// Test/mock pager implementation exported for cross-crate tests.
1252#[derive(Debug, Default, Clone, Copy)]
1253pub struct MockMvccPager;
1254
1255impl sealed::Sealed for MockMvccPager {}
1256
1257impl MvccPager for MockMvccPager {
1258    type Txn = MockTransaction;
1259
1260    fn begin<'a>(
1261        &'a self,
1262        _cx: &'a Cx,
1263        _mode: TransactionMode,
1264    ) -> impl Future<Output = Result<Self::Txn>> + 'a {
1265        async {
1266            Ok(MockTransaction {
1267                committed: false,
1268                next_page: 2,
1269                savepoint_names: Vec::new(),
1270            })
1271        }
1272    }
1273
1274    fn journal_mode(&self) -> JournalMode {
1275        JournalMode::Delete
1276    }
1277
1278    fn is_readonly(&self) -> bool {
1279        false
1280    }
1281
1282    fn set_journal_mode<'a>(
1283        &'a self,
1284        _cx: &'a Cx,
1285        mode: JournalMode,
1286    ) -> impl Future<Output = Result<JournalMode>> + 'a {
1287        async move { Ok(mode) }
1288    }
1289
1290    fn set_wal_backend(&self, _backend: Box<dyn WalBackend>) -> Result<()> {
1291        Ok(())
1292    }
1293}
1294
1295/// Test/mock transaction handle exported for cross-crate tests.
1296#[derive(Debug, Clone)]
1297pub struct MockTransaction {
1298    committed: bool,
1299    next_page: u32,
1300    savepoint_names: Vec<String>,
1301}
1302
1303impl sealed::Sealed for MockTransaction {}
1304
1305impl TransactionHandle for MockTransaction {
1306    fn get_page<'a>(
1307        &'a self,
1308        _cx: &'a Cx,
1309        page_no: PageNumber,
1310    ) -> impl Future<Output = Result<PageData>> + 'a {
1311        async move {
1312            let size = fsqlite_types::PageSize::default();
1313            let mut data = PageData::zeroed(size);
1314            data.as_bytes_mut()[..4].copy_from_slice(&page_no.get().to_le_bytes());
1315            Ok(data)
1316        }
1317    }
1318
1319    fn write_page<'a>(
1320        &'a mut self,
1321        _cx: &'a Cx,
1322        _page_no: PageNumber,
1323        _data: &'a [u8],
1324    ) -> impl Future<Output = Result<()>> + 'a {
1325        async { Ok(()) }
1326    }
1327
1328    fn allocate_page<'a>(
1329        &'a mut self,
1330        _cx: &'a Cx,
1331    ) -> impl Future<Output = Result<PageNumber>> + 'a {
1332        async move {
1333            let page = PageNumber::new(self.next_page)
1334                .expect("mock allocator must always produce non-zero page numbers");
1335            self.next_page += 1;
1336            Ok(page)
1337        }
1338    }
1339
1340    fn free_page<'a>(
1341        &'a mut self,
1342        _cx: &'a Cx,
1343        _page_no: PageNumber,
1344    ) -> impl Future<Output = Result<()>> + 'a {
1345        async { Ok(()) }
1346    }
1347
1348    fn commit<'a>(&'a mut self, _cx: &'a Cx) -> impl Future<Output = Result<()>> + 'a {
1349        async move {
1350            self.committed = true;
1351            Ok(())
1352        }
1353    }
1354
1355    fn pager_commit_state(&self) -> PagerCommitState {
1356        if self.committed {
1357            PagerCommitState::Committed
1358        } else {
1359            PagerCommitState::NotCommitted
1360        }
1361    }
1362
1363    fn is_writer(&self) -> bool {
1364        false
1365    }
1366
1367    fn has_pending_writes(&self) -> bool {
1368        false
1369    }
1370
1371    fn pending_commit_pages(&self) -> Result<Vec<PageNumber>> {
1372        Ok(Vec::new())
1373    }
1374
1375    fn rollback<'a>(&'a mut self, _cx: &'a Cx) -> impl Future<Output = Result<()>> + 'a {
1376        async { Ok(()) }
1377    }
1378
1379    fn record_write_witness(&mut self, _cx: &Cx, _key: fsqlite_types::WitnessKey) {}
1380
1381    fn savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
1382        self.savepoint_names.push(name.to_owned());
1383        Ok(())
1384    }
1385
1386    fn release_savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
1387        if let Some(pos) = self.savepoint_names.iter().rposition(|n| n == name) {
1388            self.savepoint_names.truncate(pos);
1389            Ok(())
1390        } else {
1391            Err(fsqlite_error::FrankenError::internal(format!(
1392                "no savepoint named '{name}'"
1393            )))
1394        }
1395    }
1396
1397    fn rollback_to_savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
1398        if let Some(pos) = self.savepoint_names.iter().rposition(|n| n == name) {
1399            self.savepoint_names.truncate(pos + 1);
1400            Ok(())
1401        } else {
1402            Err(fsqlite_error::FrankenError::internal(format!(
1403                "no savepoint named '{name}'"
1404            )))
1405        }
1406    }
1407}
1408
1409/// In-memory pager mock exported for cross-crate tests that need zero-filled
1410/// pages and durable writes within a transaction.
1411#[derive(Debug, Default, Clone, Copy)]
1412pub struct MemoryMockMvccPager;
1413
1414impl sealed::Sealed for MemoryMockMvccPager {}
1415
1416impl MvccPager for MemoryMockMvccPager {
1417    type Txn = MemoryMockTransaction;
1418
1419    fn begin<'a>(
1420        &'a self,
1421        _cx: &'a Cx,
1422        _mode: TransactionMode,
1423    ) -> impl Future<Output = Result<Self::Txn>> + 'a {
1424        async {
1425            Ok(MemoryMockTransaction {
1426                committed: false,
1427                next_page: 2,
1428                pages: HashMap::new(),
1429                savepoints: Vec::new(),
1430            })
1431        }
1432    }
1433
1434    fn journal_mode(&self) -> JournalMode {
1435        JournalMode::Delete
1436    }
1437
1438    fn is_readonly(&self) -> bool {
1439        false
1440    }
1441
1442    fn set_journal_mode<'a>(
1443        &'a self,
1444        _cx: &'a Cx,
1445        mode: JournalMode,
1446    ) -> impl Future<Output = Result<JournalMode>> + 'a {
1447        async move { Ok(mode) }
1448    }
1449
1450    fn set_wal_backend(&self, _backend: Box<dyn WalBackend>) -> Result<()> {
1451        Ok(())
1452    }
1453}
1454
1455#[derive(Debug, Clone)]
1456struct MemoryMockSavepoint {
1457    name: String,
1458    next_page: u32,
1459    pages: HashMap<PageNumber, PageData>,
1460}
1461
1462/// In-memory transaction mock that returns zero-filled pages until written and
1463/// preserves writes for subsequent reads.
1464#[derive(Debug, Clone)]
1465pub struct MemoryMockTransaction {
1466    committed: bool,
1467    next_page: u32,
1468    pages: HashMap<PageNumber, PageData>,
1469    savepoints: Vec<MemoryMockSavepoint>,
1470}
1471
1472impl sealed::Sealed for MemoryMockTransaction {}
1473
1474impl TransactionHandle for MemoryMockTransaction {
1475    fn get_page<'a>(
1476        &'a self,
1477        _cx: &'a Cx,
1478        page_no: PageNumber,
1479    ) -> impl Future<Output = Result<PageData>> + 'a {
1480        async move {
1481            Ok(self
1482                .pages
1483                .get(&page_no)
1484                .cloned()
1485                .unwrap_or_else(|| PageData::zeroed(fsqlite_types::PageSize::default())))
1486        }
1487    }
1488
1489    fn write_page<'a>(
1490        &'a mut self,
1491        _cx: &'a Cx,
1492        page_no: PageNumber,
1493        data: &'a [u8],
1494    ) -> impl Future<Output = Result<()>> + 'a {
1495        async move {
1496            self.committed = false;
1497            let page_size = fsqlite_types::PageSize::default().as_usize();
1498            let mut page = vec![0_u8; page_size];
1499            let copy_len = data.len().min(page_size);
1500            page[..copy_len].copy_from_slice(&data[..copy_len]);
1501            self.pages.insert(page_no, PageData::from_vec(page));
1502            Ok(())
1503        }
1504    }
1505
1506    fn write_page_data<'a>(
1507        &'a mut self,
1508        _cx: &'a Cx,
1509        page_no: PageNumber,
1510        data: PageData,
1511    ) -> impl Future<Output = Result<()>> + 'a {
1512        async move {
1513            self.committed = false;
1514            let page_size = fsqlite_types::PageSize::default().as_usize();
1515            let mut page = vec![0_u8; page_size];
1516            let copy_len = data.len().min(page_size);
1517            page[..copy_len].copy_from_slice(&data.as_bytes()[..copy_len]);
1518            self.pages.insert(page_no, PageData::from_vec(page));
1519            Ok(())
1520        }
1521    }
1522
1523    fn allocate_page<'a>(
1524        &'a mut self,
1525        _cx: &'a Cx,
1526    ) -> impl Future<Output = Result<PageNumber>> + 'a {
1527        async move {
1528            self.committed = false;
1529            let page = PageNumber::new(self.next_page)
1530                .expect("mock allocator must always produce non-zero page numbers");
1531            self.next_page += 1;
1532            self.pages
1533                .entry(page)
1534                .or_insert_with(|| PageData::zeroed(fsqlite_types::PageSize::default()));
1535            Ok(page)
1536        }
1537    }
1538
1539    fn free_page<'a>(
1540        &'a mut self,
1541        _cx: &'a Cx,
1542        page_no: PageNumber,
1543    ) -> impl Future<Output = Result<()>> + 'a {
1544        async move {
1545            self.committed = false;
1546            self.pages.remove(&page_no);
1547            Ok(())
1548        }
1549    }
1550
1551    fn commit<'a>(&'a mut self, _cx: &'a Cx) -> impl Future<Output = Result<()>> + 'a {
1552        async move {
1553            self.committed = true;
1554            Ok(())
1555        }
1556    }
1557
1558    fn pager_commit_state(&self) -> PagerCommitState {
1559        if self.committed {
1560            PagerCommitState::Committed
1561        } else {
1562            PagerCommitState::NotCommitted
1563        }
1564    }
1565
1566    fn is_writer(&self) -> bool {
1567        !self.pages.is_empty()
1568    }
1569
1570    fn has_pending_writes(&self) -> bool {
1571        !self.committed && !self.pages.is_empty()
1572    }
1573
1574    fn pending_commit_pages(&self) -> Result<Vec<PageNumber>> {
1575        let mut pages = self.pages.keys().copied().collect::<Vec<_>>();
1576        pages.sort_unstable();
1577        Ok(pages)
1578    }
1579
1580    fn rollback<'a>(&'a mut self, _cx: &'a Cx) -> impl Future<Output = Result<()>> + 'a {
1581        async move {
1582            self.committed = false;
1583            self.next_page = 2;
1584            self.pages.clear();
1585            self.savepoints.clear();
1586            Ok(())
1587        }
1588    }
1589
1590    fn record_write_witness(&mut self, _cx: &Cx, _key: fsqlite_types::WitnessKey) {}
1591
1592    fn savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
1593        self.savepoints.push(MemoryMockSavepoint {
1594            name: name.to_owned(),
1595            next_page: self.next_page,
1596            pages: self.pages.clone(),
1597        });
1598        Ok(())
1599    }
1600
1601    fn release_savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
1602        if let Some(pos) = self.savepoints.iter().rposition(|sp| sp.name == name) {
1603            self.savepoints.truncate(pos);
1604            Ok(())
1605        } else {
1606            Err(fsqlite_error::FrankenError::internal(format!(
1607                "no savepoint named '{name}'"
1608            )))
1609        }
1610    }
1611
1612    fn rollback_to_savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
1613        if let Some(pos) = self.savepoints.iter().rposition(|sp| sp.name == name) {
1614            let snapshot = self.savepoints[pos].clone();
1615            self.next_page = snapshot.next_page;
1616            self.pages = snapshot.pages;
1617            self.savepoints.truncate(pos + 1);
1618            Ok(())
1619        } else {
1620            Err(fsqlite_error::FrankenError::internal(format!(
1621                "no savepoint named '{name}'"
1622            )))
1623        }
1624    }
1625}
1626
1627/// Stack-allocated transaction wrapper used by upper layers to avoid boxing
1628/// pager transactions behind `dyn TransactionHandle`.
1629#[cfg_attr(
1630    target_arch = "wasm32",
1631    expect(
1632        clippy::large_enum_variant,
1633        reason = "native transaction variants are absent on wasm, making the intentional inline memory transaction an apparent size outlier"
1634    )
1635)]
1636pub enum TransactionKind {
1637    /// In-memory pager transaction (`:memory:` databases).
1638    Memory(SimpleTransaction<MemoryVfs>),
1639    /// Linux io_uring pager transaction.
1640    #[cfg(all(feature = "native", target_os = "linux"))]
1641    IoUring(SimpleTransaction<IoUringVfs>),
1642    /// Unix filesystem pager transaction.
1643    #[cfg(all(feature = "native", unix))]
1644    Unix(SimpleTransaction<UnixVfs>),
1645    /// Windows filesystem pager transaction.
1646    #[cfg(all(feature = "native", target_os = "windows"))]
1647    Windows(SimpleTransaction<WindowsVfs>),
1648    /// Generic mock transaction used by cross-crate tests.
1649    Mock(MockTransaction),
1650    /// In-memory mock transaction used by cross-crate tests.
1651    MemoryMock(MemoryMockTransaction),
1652    /// bd-perf: Sentinel used by SharedTxnPageIo::drain() when the real
1653    /// transaction is extracted while retaining cursor Rc references.
1654    /// Any page read/write through this variant panics — it should only
1655    /// exist transiently between drain and the next refill.
1656    Drained,
1657}
1658
1659impl std::fmt::Debug for TransactionKind {
1660    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1661        match self {
1662            Self::Memory(_) => f.write_str("TransactionKind::Memory"),
1663            #[cfg(all(feature = "native", target_os = "linux"))]
1664            Self::IoUring(_) => f.write_str("TransactionKind::IoUring"),
1665            #[cfg(all(feature = "native", unix))]
1666            Self::Unix(_) => f.write_str("TransactionKind::Unix"),
1667            #[cfg(all(feature = "native", target_os = "windows"))]
1668            Self::Windows(_) => f.write_str("TransactionKind::Windows"),
1669            Self::Mock(_) => f.write_str("TransactionKind::Mock"),
1670            Self::MemoryMock(_) => f.write_str("TransactionKind::MemoryMock"),
1671            Self::Drained => f.write_str("TransactionKind::Drained"),
1672        }
1673    }
1674}
1675
1676impl TransactionKind {
1677    /// The pager's live free-page set for this transaction (see
1678    /// [`SimpleTransaction::live_freelist_pages`]). Used by `PRAGMA
1679    /// integrity_check` (GH#113) to validate page ownership against the
1680    /// authoritative in-transaction freelist rather than the deferred,
1681    /// commit-time on-disk trunk. Mock and drained variants have no freelist
1682    /// projection and return an empty set.
1683    #[must_use]
1684    pub fn live_freelist_pages(&self) -> Vec<PageNumber> {
1685        match self {
1686            Self::Memory(txn) => txn.live_freelist_pages(),
1687            #[cfg(all(feature = "native", target_os = "linux"))]
1688            Self::IoUring(txn) => txn.live_freelist_pages(),
1689            #[cfg(all(feature = "native", unix))]
1690            Self::Unix(txn) => txn.live_freelist_pages(),
1691            #[cfg(all(feature = "native", target_os = "windows"))]
1692            Self::Windows(txn) => txn.live_freelist_pages(),
1693            Self::Mock(_) | Self::MemoryMock(_) | Self::Drained => Vec::new(),
1694        }
1695    }
1696
1697    /// The in-transaction database size in pages (see
1698    /// [`SimpleTransaction::live_db_size`]). Used as the page-extent bound by
1699    /// `PRAGMA integrity_check` (GH#113) so the walk does not flag pages
1700    /// allocated this transaction as past the end of the database. Mock and
1701    /// drained variants return 0 (the caller falls back to the published size).
1702    #[must_use]
1703    pub fn live_db_size(&self) -> u32 {
1704        match self {
1705            Self::Memory(txn) => txn.live_db_size(),
1706            #[cfg(all(feature = "native", target_os = "linux"))]
1707            Self::IoUring(txn) => txn.live_db_size(),
1708            #[cfg(all(feature = "native", unix))]
1709            Self::Unix(txn) => txn.live_db_size(),
1710            #[cfg(all(feature = "native", target_os = "windows"))]
1711            Self::Windows(txn) => txn.live_db_size(),
1712            Self::Mock(_) | Self::MemoryMock(_) | Self::Drained => 0,
1713        }
1714    }
1715
1716    /// The fixed database-size bound captured by this transaction's pager
1717    /// snapshot. Mock and drained variants do not expose a pager snapshot and
1718    /// return 0 so callers can use an explicitly validated fallback.
1719    #[must_use]
1720    pub fn snapshot_db_size(&self) -> u32 {
1721        match self {
1722            Self::Memory(txn) => txn.snapshot_db_size(),
1723            #[cfg(all(feature = "native", target_os = "linux"))]
1724            Self::IoUring(txn) => txn.snapshot_db_size(),
1725            #[cfg(all(feature = "native", unix))]
1726            Self::Unix(txn) => txn.snapshot_db_size(),
1727            #[cfg(all(feature = "native", target_os = "windows"))]
1728            Self::Windows(txn) => txn.snapshot_db_size(),
1729            Self::Mock(_) | Self::MemoryMock(_) | Self::Drained => 0,
1730        }
1731    }
1732
1733    /// Largest page visible through the fixed snapshot plus pages issued or
1734    /// staged by this transaction. Mock and drained variants return 0 so
1735    /// callers can use an explicitly validated fallback.
1736    #[must_use]
1737    pub fn visible_db_size_bound(&self) -> u32 {
1738        match self {
1739            Self::Memory(txn) => txn.visible_db_size_bound(),
1740            #[cfg(all(feature = "native", target_os = "linux"))]
1741            Self::IoUring(txn) => txn.visible_db_size_bound(),
1742            #[cfg(all(feature = "native", unix))]
1743            Self::Unix(txn) => txn.visible_db_size_bound(),
1744            #[cfg(all(feature = "native", target_os = "windows"))]
1745            Self::Windows(txn) => txn.visible_db_size_bound(),
1746            Self::Mock(_) | Self::MemoryMock(_) | Self::Drained => 0,
1747        }
1748    }
1749}
1750
1751macro_rules! dispatch_transaction_kind {
1752    ($value:expr, $txn:ident => $body:expr) => {
1753        match $value {
1754            TransactionKind::Memory($txn) => $body,
1755            #[cfg(all(feature = "native", target_os = "linux"))]
1756            TransactionKind::IoUring($txn) => $body,
1757            #[cfg(all(feature = "native", unix))]
1758            TransactionKind::Unix($txn) => $body,
1759            #[cfg(all(feature = "native", target_os = "windows"))]
1760            TransactionKind::Windows($txn) => $body,
1761            TransactionKind::Mock($txn) => $body,
1762            TransactionKind::MemoryMock($txn) => $body,
1763            TransactionKind::Drained => {
1764                panic!("BUG: TransactionKind::Drained accessed while the transaction was extracted")
1765            }
1766        }
1767    };
1768}
1769
1770impl From<SimpleTransaction<MemoryVfs>> for TransactionKind {
1771    fn from(txn: SimpleTransaction<MemoryVfs>) -> Self {
1772        Self::Memory(txn)
1773    }
1774}
1775
1776#[cfg(all(feature = "native", target_os = "linux"))]
1777impl From<SimpleTransaction<IoUringVfs>> for TransactionKind {
1778    fn from(txn: SimpleTransaction<IoUringVfs>) -> Self {
1779        Self::IoUring(txn)
1780    }
1781}
1782
1783#[cfg(all(feature = "native", unix))]
1784impl From<SimpleTransaction<UnixVfs>> for TransactionKind {
1785    fn from(txn: SimpleTransaction<UnixVfs>) -> Self {
1786        Self::Unix(txn)
1787    }
1788}
1789
1790#[cfg(all(feature = "native", target_os = "windows"))]
1791impl From<SimpleTransaction<WindowsVfs>> for TransactionKind {
1792    fn from(txn: SimpleTransaction<WindowsVfs>) -> Self {
1793        Self::Windows(txn)
1794    }
1795}
1796
1797impl From<MockTransaction> for TransactionKind {
1798    fn from(txn: MockTransaction) -> Self {
1799        Self::Mock(txn)
1800    }
1801}
1802
1803impl From<MemoryMockTransaction> for TransactionKind {
1804    fn from(txn: MemoryMockTransaction) -> Self {
1805        Self::MemoryMock(txn)
1806    }
1807}
1808
1809impl sealed::Sealed for TransactionKind {}
1810
1811impl TransactionHandle for TransactionKind {
1812    // These TransactionKind dispatch sites show up in self-time profiles.
1813    // Routing them through `with_handle` / `with_handle_mut` coerces the
1814    // concrete `&SimpleTransaction<V>` into `&dyn TransactionHandle` inside the
1815    // closure, so every call pays a vtable lookup. Inlining the match here lets
1816    // LLVM see the concrete type and dispatch statically; the rest of
1817    // `with_handle`'s callers are cold or shape-uniform enough to keep sharing
1818    // the smaller helper.
1819    fn get_page<'a>(
1820        &'a self,
1821        cx: &'a Cx,
1822        page_no: PageNumber,
1823    ) -> impl Future<Output = Result<PageData>> + 'a {
1824        async move { dispatch_transaction_kind!(self, txn => txn.get_page(cx, page_no).await) }
1825    }
1826
1827    fn prefetch_page_hint(&self, cx: &Cx, page_no: PageNumber) {
1828        dispatch_transaction_kind!(self, txn => txn.prefetch_page_hint(cx, page_no));
1829    }
1830
1831    fn forget_cached_page(&self, page_no: PageNumber) {
1832        dispatch_transaction_kind!(self, txn => txn.forget_cached_page(page_no));
1833    }
1834
1835    fn write_page<'a>(
1836        &'a mut self,
1837        cx: &'a Cx,
1838        page_no: PageNumber,
1839        data: &'a [u8],
1840    ) -> impl Future<Output = Result<()>> + 'a {
1841        async move { dispatch_transaction_kind!(self, txn => txn.write_page(cx, page_no, data).await) }
1842    }
1843
1844    fn write_page_data<'a>(
1845        &'a mut self,
1846        cx: &'a Cx,
1847        page_no: PageNumber,
1848        data: PageData,
1849    ) -> impl Future<Output = Result<()>> + 'a {
1850        async move {
1851            dispatch_transaction_kind!(self, txn => txn.write_page_data(cx, page_no, data).await)
1852        }
1853    }
1854
1855    fn try_mutate_staged_page_data(
1856        &mut self,
1857        page_no: PageNumber,
1858        f: &mut dyn FnMut(&mut PageData),
1859    ) -> bool {
1860        dispatch_transaction_kind!(self, txn => txn.try_mutate_staged_page_data(page_no, f))
1861    }
1862
1863    fn allocate_page<'a>(
1864        &'a mut self,
1865        cx: &'a Cx,
1866    ) -> impl Future<Output = Result<PageNumber>> + 'a {
1867        async move { dispatch_transaction_kind!(self, txn => txn.allocate_page(cx).await) }
1868    }
1869
1870    fn free_page<'a>(
1871        &'a mut self,
1872        cx: &'a Cx,
1873        page_no: PageNumber,
1874    ) -> impl Future<Output = Result<()>> + 'a {
1875        async move { dispatch_transaction_kind!(self, txn => txn.free_page(cx, page_no).await) }
1876    }
1877
1878    fn commit<'a>(&'a mut self, cx: &'a Cx) -> impl Future<Output = Result<()>> + 'a {
1879        async move { dispatch_transaction_kind!(self, txn => txn.commit(cx).await) }
1880    }
1881
1882    fn pager_commit_state(&self) -> PagerCommitState {
1883        dispatch_transaction_kind!(self, txn => txn.pager_commit_state())
1884    }
1885
1886    fn commit_and_retain<'a>(&'a mut self, cx: &'a Cx) -> impl Future<Output = Result<bool>> + 'a {
1887        async move { dispatch_transaction_kind!(self, txn => txn.commit_and_retain(cx).await) }
1888    }
1889
1890    fn is_writer(&self) -> bool {
1891        dispatch_transaction_kind!(self, txn => txn.is_writer())
1892    }
1893
1894    fn has_pending_writes(&self) -> bool {
1895        dispatch_transaction_kind!(self, txn => txn.has_pending_writes())
1896    }
1897
1898    fn published_visible_commit_seq_hint(&self) -> Option<fsqlite_types::CommitSeq> {
1899        dispatch_transaction_kind!(self, txn => txn.published_visible_commit_seq_hint())
1900    }
1901
1902    fn pending_commit_pages(&self) -> Result<Vec<PageNumber>> {
1903        dispatch_transaction_kind!(self, txn => txn.pending_commit_pages())
1904    }
1905
1906    fn pending_conflict_pages(&self) -> Result<Vec<PageNumber>> {
1907        dispatch_transaction_kind!(self, txn => txn.pending_conflict_pages())
1908    }
1909
1910    fn pending_conflict_pages_conservative(&self) -> Vec<PageNumber> {
1911        dispatch_transaction_kind!(self, txn => txn.pending_conflict_pages_conservative())
1912    }
1913
1914    fn write_set_page_numbers(&self) -> Vec<PageNumber> {
1915        dispatch_transaction_kind!(self, txn => txn.write_set_page_numbers())
1916    }
1917
1918    fn page_one_in_pending_commit_surface(&self) -> Result<bool> {
1919        dispatch_transaction_kind!(self, txn => txn.page_one_in_pending_commit_surface())
1920    }
1921
1922    fn page_size(&self) -> PageSize {
1923        dispatch_transaction_kind!(self, txn => txn.page_size())
1924    }
1925
1926    fn allocate_page_requires_page_one_conflict_tracking(&self) -> Result<bool> {
1927        dispatch_transaction_kind!(self, txn => txn.allocate_page_requires_page_one_conflict_tracking())
1928    }
1929
1930    fn free_page_requires_page_one_conflict_tracking(&self, page_no: PageNumber) -> Result<bool> {
1931        dispatch_transaction_kind!(self, txn => txn.free_page_requires_page_one_conflict_tracking(page_no))
1932    }
1933
1934    fn write_page_requires_page_one_conflict_tracking(&self, page_no: PageNumber) -> Result<bool> {
1935        dispatch_transaction_kind!(self, txn => txn.write_page_requires_page_one_conflict_tracking(page_no))
1936    }
1937
1938    fn rollback<'a>(&'a mut self, cx: &'a Cx) -> impl Future<Output = Result<()>> + 'a {
1939        async move { dispatch_transaction_kind!(self, txn => txn.rollback(cx).await) }
1940    }
1941
1942    fn record_write_witness(&mut self, cx: &Cx, key: fsqlite_types::WitnessKey) {
1943        dispatch_transaction_kind!(self, txn => txn.record_write_witness(cx, key));
1944    }
1945
1946    fn savepoint(&mut self, cx: &Cx, name: &str) -> Result<()> {
1947        dispatch_transaction_kind!(self, txn => txn.savepoint(cx, name))
1948    }
1949
1950    fn release_savepoint(&mut self, cx: &Cx, name: &str) -> Result<()> {
1951        dispatch_transaction_kind!(self, txn => txn.release_savepoint(cx, name))
1952    }
1953
1954    fn rollback_to_savepoint(&mut self, cx: &Cx, name: &str) -> Result<()> {
1955        dispatch_transaction_kind!(self, txn => txn.rollback_to_savepoint(cx, name))
1956    }
1957}
1958
1959/// Test/mock checkpoint writer exported for cross-crate tests.
1960#[derive(Debug, Default, Clone, Copy)]
1961pub struct MockCheckpointPageWriter;
1962
1963impl sealed::Sealed for MockCheckpointPageWriter {}
1964
1965impl CheckpointPageWriter for MockCheckpointPageWriter {
1966    fn write_page<'a>(
1967        &'a mut self,
1968        _cx: &'a Cx,
1969        _page_no: PageNumber,
1970        _data: &'a [u8],
1971    ) -> WalFuture<'a, ()> {
1972        Box::pin(async { Ok(()) })
1973    }
1974
1975    fn truncate<'a>(&'a mut self, _cx: &'a Cx, _n_pages: u32) -> WalFuture<'a, ()> {
1976        Box::pin(async { Ok(()) })
1977    }
1978
1979    fn sync<'a>(&'a mut self, _cx: &'a Cx) -> WalFuture<'a, ()> {
1980        Box::pin(async { Ok(()) })
1981    }
1982}
1983
1984// ---------------------------------------------------------------------------
1985// Tests
1986// ---------------------------------------------------------------------------
1987
1988#[cfg(test)]
1989mod tests {
1990    use super::*;
1991    use fsqlite_vfs::VfsWriteCompletionState;
1992    use std::task::Poll;
1993
1994    // -- Unit tests --
1995
1996    const fn test_wal_generation_identity() -> WalGenerationIdentity {
1997        WalGenerationIdentity {
1998            checkpoint_seq: 0,
1999            salts: fsqlite_wal::checksum::WalSalts { salt1: 0, salt2: 0 },
2000        }
2001    }
2002
2003    struct PendingTrackedWalBackend;
2004
2005    impl WalBackend for PendingTrackedWalBackend {
2006        fn append_frame<'a>(
2007            &'a mut self,
2008            _cx: &'a Cx,
2009            _page_number: u32,
2010            _page_data: &'a [u8],
2011            _db_size_if_commit: u32,
2012        ) -> WalFuture<'a, ()> {
2013            Box::pin(std::future::pending())
2014        }
2015
2016        fn read_page<'a>(
2017            &'a mut self,
2018            _cx: &'a Cx,
2019            _page_number: u32,
2020        ) -> WalFuture<'a, Option<Vec<u8>>> {
2021            Box::pin(async { Ok(None) })
2022        }
2023
2024        fn sync(&mut self, _cx: &Cx) -> Result<()> {
2025            Ok(())
2026        }
2027
2028        fn frame_count(&self) -> usize {
2029            0
2030        }
2031
2032        fn checkpoint<'a>(
2033            &'a mut self,
2034            _cx: &'a Cx,
2035            mode: CheckpointMode,
2036            _writer: &'a mut dyn CheckpointPageWriter,
2037            _backfilled_frames: u32,
2038            _oldest_reader_frame: Option<u32>,
2039        ) -> WalFuture<'a, CheckpointResult> {
2040            Box::pin(async move {
2041                Ok(CheckpointResult {
2042                    total_frames: 0,
2043                    frames_backfilled: 0,
2044                    completed: true,
2045                    wal_was_reset: false,
2046                    requested_mode: mode,
2047                    effective_mode: mode,
2048                })
2049            })
2050        }
2051    }
2052
2053    #[test]
2054    fn tracked_default_marks_unpolled_drop_terminal_error() {
2055        let cx = Cx::new();
2056        let data = [0_u8; 16];
2057        let frames = [WalFrameRef {
2058            page_number: 1,
2059            page_data: &data,
2060            db_size_if_commit: 1,
2061        }];
2062        let completion = VfsWriteCompletion::new();
2063        let mut backend = PendingTrackedWalBackend;
2064
2065        let future = backend.append_frames_tracked(&cx, &frames, completion.clone());
2066        assert_eq!(completion.state(), VfsWriteCompletionState::Pending);
2067        drop(future);
2068        assert_eq!(completion.state(), VfsWriteCompletionState::Error);
2069    }
2070
2071    #[test]
2072    fn tracked_default_marks_polled_drop_terminal_error() {
2073        let cx = Cx::new();
2074        let data = [0_u8; 16];
2075        let frames = [WalFrameRef {
2076            page_number: 1,
2077            page_data: &data,
2078            db_size_if_commit: 1,
2079        }];
2080        let completion = VfsWriteCompletion::new();
2081        let mut backend = PendingTrackedWalBackend;
2082        let mut future = Box::pin(backend.append_frames_tracked(&cx, &frames, completion.clone()));
2083
2084        let polled = std::future::poll_fn(|poll_cx| {
2085            assert!(future.as_mut().poll(poll_cx).is_pending());
2086            Poll::Ready(())
2087        });
2088        let runtime = asupersync::runtime::RuntimeBuilder::current_thread()
2089            .blocking_threads(1, 1)
2090            .build()
2091            .expect("tracked-default test runtime should build");
2092        runtime.block_on(polled);
2093        assert_eq!(completion.state(), VfsWriteCompletionState::Pending);
2094        drop(future);
2095        assert_eq!(completion.state(), VfsWriteCompletionState::Error);
2096    }
2097
2098    #[test]
2099    fn test_pager_trait_is_sealed_mock_impl() {
2100        asupersync::test_utils::run_test(|| async {
2101            // This compiles because MockPager is in the same crate.
2102            // External crates cannot impl Sealed, so they cannot impl MvccPager.
2103            let pager = MockMvccPager;
2104            let cx = Cx::new();
2105            let _txn = pager.begin(&cx, TransactionMode::Deferred).await.unwrap();
2106        });
2107    }
2108
2109    #[test]
2110    fn test_mvccpager_begin_commit_rollback_signatures() {
2111        asupersync::test_utils::run_test(|| async {
2112            let pager = MockMvccPager;
2113            let cx = Cx::new();
2114
2115            // Begin takes &Cx and returns Result.
2116            let mut txn = pager.begin(&cx, TransactionMode::ReadOnly).await.unwrap();
2117
2118            // All blocking/I/O methods take &Cx and return Result.
2119            let page_no = PageNumber::new(1).unwrap();
2120            let data = txn.get_page(&cx, page_no).await.unwrap();
2121            assert_eq!(
2122                u32::from_le_bytes(data.as_bytes()[..4].try_into().unwrap()),
2123                1
2124            );
2125
2126            txn.write_page(&cx, page_no, &[0u8; 4096]).await.unwrap();
2127            let new_page = txn.allocate_page(&cx).await.unwrap();
2128            assert_eq!(new_page.get(), 2);
2129            txn.free_page(&cx, new_page).await.unwrap();
2130
2131            txn.commit(&cx).await.unwrap();
2132        });
2133    }
2134
2135    #[test]
2136    fn test_transaction_rollback_is_infallible() {
2137        asupersync::test_utils::run_test(|| async {
2138            let pager = MockMvccPager;
2139            let cx = Cx::new();
2140            let mut txn = pager.begin(&cx, TransactionMode::Deferred).await.unwrap();
2141            // Rollback should succeed without error.
2142            txn.rollback(&cx).await.unwrap();
2143        });
2144    }
2145
2146    #[test]
2147    fn test_checkpoint_page_writer_signatures() {
2148        asupersync::test_utils::run_test(|| async {
2149            let mut writer = MockCheckpointPageWriter;
2150            let cx = Cx::new();
2151            let page1 = PageNumber::new(1).unwrap();
2152
2153            writer.write_page(&cx, page1, &[0u8; 4096]).await.unwrap();
2154            writer.truncate(&cx, 10).await.unwrap();
2155            writer.sync(&cx).await.unwrap();
2156        });
2157    }
2158
2159    #[test]
2160    fn test_transaction_mode_default_is_deferred() {
2161        assert_eq!(TransactionMode::default(), TransactionMode::Deferred);
2162    }
2163
2164    #[test]
2165    fn test_open_traits_are_extensible() {
2166        // Vfs and VfsFile are open traits — external crates CAN implement them.
2167        // This test is in fsqlite-vfs, but we verify the concept:
2168        // sealed traits CANNOT be implemented externally.
2169        // Open traits CAN be implemented externally.
2170        //
2171        // Since we can't directly test "external crate fails to compile"
2172        // in a unit test, we verify that our mock impls compile and work.
2173        //
2174        // `MvccPager` uses `-> impl Future` in its method signatures, so it is
2175        // not dyn compatible; the bound is asserted generically instead.
2176        fn assert_is_mvcc_pager<P: MvccPager<Txn = MockTransaction>>(_pager: &P) {}
2177        let pager = MockMvccPager;
2178        assert_is_mvcc_pager(&pager);
2179    }
2180
2181    #[test]
2182    fn test_memory_mock_transaction_persists_writes() {
2183        asupersync::test_utils::run_test(|| async {
2184            let pager = MemoryMockMvccPager;
2185            let cx = Cx::new();
2186            let mut txn = pager.begin(&cx, TransactionMode::Immediate).await.unwrap();
2187            let page_no = PageNumber::new(256).unwrap();
2188
2189            let mut bytes = vec![0_u8; fsqlite_types::PageSize::default().as_usize()];
2190            bytes[0] = 0x0A;
2191            txn.write_page(&cx, page_no, &bytes).await.unwrap();
2192
2193            let page = txn.get_page(&cx, page_no).await.unwrap();
2194            assert_eq!(page.as_bytes()[0], 0x0A);
2195            assert!(txn.has_pending_writes());
2196            assert!(txn.is_writer());
2197        });
2198    }
2199
2200    #[test]
2201    fn test_memory_mock_transaction_commit_clears_pending_writes() {
2202        asupersync::test_utils::run_test(|| async {
2203            let pager = MemoryMockMvccPager;
2204            let cx = Cx::new();
2205            let mut txn = pager.begin(&cx, TransactionMode::Immediate).await.unwrap();
2206            let page_no = PageNumber::new(2).unwrap();
2207
2208            txn.write_page(&cx, page_no, &[1_u8; 4096]).await.unwrap();
2209            assert!(txn.has_pending_writes());
2210
2211            txn.commit(&cx).await.unwrap();
2212            assert!(
2213                !txn.has_pending_writes(),
2214                "committed mock transactions must not report pending writes"
2215            );
2216        });
2217    }
2218
2219    #[test]
2220    fn test_memory_mock_transaction_rollback_resets_allocator() {
2221        asupersync::test_utils::run_test(|| async {
2222            let pager = MemoryMockMvccPager;
2223            let cx = Cx::new();
2224            let mut txn = pager.begin(&cx, TransactionMode::Immediate).await.unwrap();
2225
2226            assert_eq!(txn.allocate_page(&cx).await.unwrap().get(), 2);
2227            assert_eq!(txn.allocate_page(&cx).await.unwrap().get(), 3);
2228
2229            txn.rollback(&cx).await.unwrap();
2230
2231            assert_eq!(
2232                txn.allocate_page(&cx).await.unwrap().get(),
2233                2,
2234                "rollback should restore the mock allocator to its initial state"
2235            );
2236        });
2237    }
2238
2239    #[test]
2240    fn test_checkpoint_mode_default_is_passive() {
2241        assert_eq!(CheckpointMode::default(), CheckpointMode::Passive);
2242    }
2243
2244    #[test]
2245    fn test_journal_mode_default_is_delete() {
2246        assert_eq!(JournalMode::default(), JournalMode::Delete);
2247    }
2248
2249    #[test]
2250    fn test_wal_publication_snapshot_authoritative_when_index_full() {
2251        let snap = WalPublicationSnapshot {
2252            publication_seq: 1,
2253            generation: test_wal_generation_identity(),
2254            last_commit_frame: Some(10),
2255            commit_count: 5,
2256            latest_frame_entries: 10,
2257            index_is_partial: false,
2258        };
2259        assert!(
2260            snap.lookup_contract_is_authoritative(),
2261            "full index must be authoritative"
2262        );
2263    }
2264
2265    #[test]
2266    fn test_wal_publication_snapshot_not_authoritative_when_partial() {
2267        let snap = WalPublicationSnapshot {
2268            publication_seq: 1,
2269            generation: test_wal_generation_identity(),
2270            last_commit_frame: None,
2271            commit_count: 0,
2272            latest_frame_entries: 0,
2273            index_is_partial: true,
2274        };
2275        assert!(
2276            !snap.lookup_contract_is_authoritative(),
2277            "partial index must not be authoritative"
2278        );
2279    }
2280
2281    #[test]
2282    fn test_prepared_wal_frame_batch_frame_count_and_page_size() {
2283        let batch = PreparedWalFrameBatch {
2284            frame_size: 4120,
2285            page_data_offset: 24,
2286            big_endian_checksum: false,
2287            frame_metas: vec![
2288                PreparedWalFrameMeta {
2289                    page_number: 1,
2290                    db_size_if_commit: 0,
2291                },
2292                PreparedWalFrameMeta {
2293                    page_number: 2,
2294                    db_size_if_commit: 10,
2295                },
2296            ],
2297            checksum_transforms: Vec::new(),
2298            frame_bytes: vec![0u8; 4120 * 2],
2299            last_commit_frame_offset: Some(4120),
2300            finalized_for: None,
2301            finalized_running_checksum: None,
2302        };
2303        assert_eq!(batch.frame_count(), 2);
2304        assert_eq!(batch.page_size(), 4096);
2305    }
2306
2307    #[test]
2308    fn test_prepared_wal_frame_batch_set_db_size_clears_finalized() {
2309        let mut batch = PreparedWalFrameBatch {
2310            frame_size: 32,
2311            page_data_offset: 8,
2312            big_endian_checksum: false,
2313            frame_metas: vec![PreparedWalFrameMeta {
2314                page_number: 1,
2315                db_size_if_commit: 0,
2316            }],
2317            checksum_transforms: Vec::new(),
2318            frame_bytes: vec![0u8; 32],
2319            last_commit_frame_offset: None,
2320            finalized_for: Some(PreparedWalFinalizationState {
2321                checkpoint_seq: 1,
2322                salt1: 0xAA,
2323                salt2: 0xBB,
2324                start_frame_index: 0,
2325                seed: PreparedWalChecksumSeed::default(),
2326            }),
2327            finalized_running_checksum: Some(PreparedWalChecksumSeed { s1: 1, s2: 2 }),
2328        };
2329
2330        batch.set_db_size_if_commit(0, 42);
2331
2332        assert_eq!(batch.frame_metas[0].db_size_if_commit, 42);
2333        assert!(
2334            batch.finalized_for.is_none(),
2335            "set_db_size_if_commit must invalidate finalized_for"
2336        );
2337        assert!(
2338            batch.finalized_running_checksum.is_none(),
2339            "set_db_size_if_commit must invalidate finalized_running_checksum"
2340        );
2341        let db_bytes = &batch.frame_bytes[4..8];
2342        assert_eq!(u32::from_be_bytes(db_bytes.try_into().unwrap()), 42);
2343    }
2344
2345    #[test]
2346    fn test_mock_release_savepoint_unknown_name_returns_error() {
2347        asupersync::test_utils::run_test(|| async {
2348            let pager = MockMvccPager;
2349            let cx = Cx::new();
2350            let mut txn = pager.begin(&cx, TransactionMode::Deferred).await.unwrap();
2351
2352            let result = txn.release_savepoint(&cx, "nonexistent");
2353            assert!(result.is_err(), "releasing unknown savepoint must fail");
2354        });
2355    }
2356
2357    #[test]
2358    fn test_memory_mock_savepoint_rollback_restores_pages() {
2359        asupersync::test_utils::run_test(|| async {
2360            let pager = MemoryMockMvccPager;
2361            let cx = Cx::new();
2362            let mut txn = pager.begin(&cx, TransactionMode::Immediate).await.unwrap();
2363
2364            let p1 = PageNumber::new(1).unwrap();
2365            let page_size = fsqlite_types::PageSize::default().as_usize();
2366            let mut data_a = vec![0u8; page_size];
2367            data_a[0] = 0xAA;
2368            txn.write_page(&cx, p1, &data_a).await.unwrap();
2369
2370            txn.savepoint(&cx, "sp1").unwrap();
2371
2372            let mut data_b = vec![0u8; page_size];
2373            data_b[0] = 0xBB;
2374            txn.write_page(&cx, p1, &data_b).await.unwrap();
2375            assert_eq!(txn.get_page(&cx, p1).await.unwrap().as_bytes()[0], 0xBB);
2376
2377            txn.rollback_to_savepoint(&cx, "sp1").unwrap();
2378            assert_eq!(
2379                txn.get_page(&cx, p1).await.unwrap().as_bytes()[0],
2380                0xAA,
2381                "rollback_to_savepoint must restore page state"
2382            );
2383        });
2384    }
2385
2386    #[test]
2387    fn test_transaction_mode_default_trait_contract_is_deferred() {
2388        assert_eq!(TransactionMode::default(), TransactionMode::Deferred);
2389    }
2390
2391    #[test]
2392    fn test_checkpoint_result_fields() {
2393        let result = CheckpointResult {
2394            total_frames: 100,
2395            frames_backfilled: 80,
2396            completed: false,
2397            wal_was_reset: false,
2398            requested_mode: CheckpointMode::Full,
2399            effective_mode: CheckpointMode::Passive,
2400        };
2401        assert_eq!(result.total_frames, 100);
2402        assert_eq!(result.frames_backfilled, 80);
2403        assert!(!result.completed);
2404        assert_ne!(result.requested_mode, result.effective_mode);
2405    }
2406
2407    #[test]
2408    fn test_journal_mode_debug_clone_copy_eq() {
2409        let a = JournalMode::Wal;
2410        let b = a;
2411        assert_eq!(a, b);
2412        assert_ne!(JournalMode::Delete, JournalMode::Wal);
2413        let dbg = format!("{a:?}");
2414        assert!(dbg.contains("Wal"));
2415    }
2416
2417    #[test]
2418    fn test_checkpoint_result_clone_debug() {
2419        let result = CheckpointResult {
2420            total_frames: 50,
2421            frames_backfilled: 50,
2422            completed: true,
2423            wal_was_reset: true,
2424            requested_mode: CheckpointMode::Truncate,
2425            effective_mode: CheckpointMode::Truncate,
2426        };
2427        let cloned = result.clone();
2428        assert_eq!(result, cloned);
2429        let dbg = format!("{result:?}");
2430        assert!(dbg.contains("CheckpointResult"));
2431        assert!(dbg.contains("Truncate"));
2432        assert!(dbg.contains("wal_was_reset"));
2433    }
2434
2435    #[test]
2436    fn test_wal_publication_snapshot_clone_copy_debug() {
2437        let snap = WalPublicationSnapshot {
2438            publication_seq: 42,
2439            generation: test_wal_generation_identity(),
2440            last_commit_frame: Some(100),
2441            commit_count: 7,
2442            latest_frame_entries: 50,
2443            index_is_partial: false,
2444        };
2445        let copied = snap;
2446        assert_eq!(copied, snap);
2447        let dbg = format!("{snap:?}");
2448        assert!(dbg.contains("WalPublicationSnapshot"));
2449        assert!(dbg.contains("publication_seq"));
2450        assert!(dbg.contains("42"));
2451    }
2452
2453    #[test]
2454    fn test_checkpoint_mode_all_variants_debug() {
2455        for (mode, expected) in [
2456            (CheckpointMode::Passive, "Passive"),
2457            (CheckpointMode::Full, "Full"),
2458            (CheckpointMode::Restart, "Restart"),
2459            (CheckpointMode::Truncate, "Truncate"),
2460        ] {
2461            let dbg = format!("{mode:?}");
2462            assert!(dbg.contains(expected), "expected {expected} in {dbg}");
2463            let copy = mode;
2464            assert_eq!(mode, copy);
2465        }
2466    }
2467
2468    #[test]
2469    fn test_prepared_wal_frame_batch_page_data_and_frame_slice() {
2470        let frame_size = 32;
2471        let page_data_offset = 8;
2472        let mut frame_bytes = vec![0u8; frame_size * 2];
2473        frame_bytes[8] = 0xAA;
2474        frame_bytes[frame_size + 8] = 0xBB;
2475
2476        let batch = PreparedWalFrameBatch {
2477            frame_size,
2478            page_data_offset,
2479            big_endian_checksum: false,
2480            frame_metas: vec![
2481                PreparedWalFrameMeta {
2482                    page_number: 1,
2483                    db_size_if_commit: 0,
2484                },
2485                PreparedWalFrameMeta {
2486                    page_number: 2,
2487                    db_size_if_commit: 5,
2488                },
2489            ],
2490            checksum_transforms: Vec::new(),
2491            frame_bytes,
2492            last_commit_frame_offset: None,
2493            finalized_for: None,
2494            finalized_running_checksum: None,
2495        };
2496
2497        assert_eq!(batch.page_data(0)[0], 0xAA);
2498        assert_eq!(batch.page_data(1)[0], 0xBB);
2499        assert_eq!(batch.frame_slice(0).len(), frame_size);
2500        assert_eq!(batch.frame_slice(1).len(), frame_size);
2501
2502        let refs = batch.frame_refs();
2503        assert_eq!(refs.len(), 2);
2504        assert_eq!(refs[0].page_number, 1);
2505        assert_eq!(refs[1].db_size_if_commit, 5);
2506        assert_eq!(refs[0].page_data[0], 0xAA);
2507        assert_eq!(refs[1].page_data[0], 0xBB);
2508    }
2509
2510    #[test]
2511    fn prepared_wal_frame_meta_debug_clone_copy_eq() {
2512        let a = PreparedWalFrameMeta {
2513            page_number: 5,
2514            db_size_if_commit: 0,
2515        };
2516        let b = PreparedWalFrameMeta {
2517            page_number: 5,
2518            db_size_if_commit: 10,
2519        };
2520        let copied = a;
2521        assert_eq!(copied, a);
2522        assert_ne!(a, b);
2523        let dbg = format!("{a:?}");
2524        assert!(dbg.contains("PreparedWalFrameMeta"));
2525        assert!(dbg.contains("5"));
2526    }
2527
2528    #[test]
2529    fn prepared_wal_checksum_seed_default_and_eq() {
2530        let def = PreparedWalChecksumSeed::default();
2531        assert_eq!(def.s1, 0);
2532        assert_eq!(def.s2, 0);
2533        let other = PreparedWalChecksumSeed { s1: 1, s2: 2 };
2534        assert_ne!(def, other);
2535        let copied = other;
2536        assert_eq!(copied, other);
2537        let dbg = format!("{def:?}");
2538        assert!(dbg.contains("PreparedWalChecksumSeed"));
2539    }
2540
2541    #[test]
2542    fn prepared_wal_finalization_state_default_and_eq() {
2543        let def = PreparedWalFinalizationState::default();
2544        assert_eq!(def.checkpoint_seq, 0);
2545        assert_eq!(def.salt1, 0);
2546        assert_eq!(def.salt2, 0);
2547        assert_eq!(def.start_frame_index, 0);
2548        assert_eq!(def.seed, PreparedWalChecksumSeed::default());
2549        let other = PreparedWalFinalizationState {
2550            checkpoint_seq: 1,
2551            salt1: 0xAA,
2552            salt2: 0xBB,
2553            start_frame_index: 42,
2554            seed: PreparedWalChecksumSeed { s1: 10, s2: 20 },
2555        };
2556        assert_ne!(def, other);
2557        let copied = other;
2558        assert_eq!(copied, other);
2559        let dbg = format!("{other:?}");
2560        assert!(dbg.contains("PreparedWalFinalizationState"));
2561    }
2562
2563    #[test]
2564    fn transaction_mode_all_variants_debug_copy_eq() {
2565        let variants = [
2566            (TransactionMode::Deferred, "Deferred"),
2567            (TransactionMode::Immediate, "Immediate"),
2568            (TransactionMode::Exclusive, "Exclusive"),
2569            (TransactionMode::Concurrent, "Concurrent"),
2570            (TransactionMode::ReadOnly, "ReadOnly"),
2571        ];
2572        for (mode, expected) in &variants {
2573            let dbg = format!("{mode:?}");
2574            assert!(dbg.contains(expected), "expected {expected} in {dbg}");
2575            let copied = *mode;
2576            assert_eq!(copied, *mode);
2577        }
2578        assert_ne!(TransactionMode::Deferred, TransactionMode::Concurrent);
2579    }
2580
2581    #[test]
2582    fn wal_frame_ref_debug_clone_copy() {
2583        let data = [0xABu8; 16];
2584        let frame = WalFrameRef {
2585            page_number: 3,
2586            page_data: &data,
2587            db_size_if_commit: 0,
2588        };
2589        let copied = frame;
2590        assert_eq!(copied.page_number, 3);
2591        assert_eq!(copied.page_data.len(), 16);
2592        assert_eq!(copied.db_size_if_commit, 0);
2593        let dbg = format!("{frame:?}");
2594        assert!(dbg.contains("WalFrameRef"));
2595    }
2596
2597    #[test]
2598    fn mock_checkpoint_page_writer_default_and_trait_methods() {
2599        asupersync::test_utils::run_test(|| async {
2600            let mut writer = MockCheckpointPageWriter;
2601            let cx = Cx::new();
2602            let page = PageNumber::new(1).unwrap();
2603            writer.write_page(&cx, page, &[0u8; 4096]).await.unwrap();
2604            writer.truncate(&cx, 10).await.unwrap();
2605            writer.sync(&cx).await.unwrap();
2606            let dbg = format!("{writer:?}");
2607            assert!(dbg.contains("MockCheckpointPageWriter"));
2608        });
2609    }
2610
2611    #[test]
2612    fn transaction_kind_drained_debug() {
2613        let kind = TransactionKind::Drained;
2614        let dbg = format!("{kind:?}");
2615        assert!(dbg.contains("Drained"));
2616    }
2617
2618    #[test]
2619    fn wal_publication_snapshot_authoritative_boundary() {
2620        let base = WalPublicationSnapshot {
2621            publication_seq: 1,
2622            generation: test_wal_generation_identity(),
2623            last_commit_frame: Some(10),
2624            commit_count: 5,
2625            latest_frame_entries: 10,
2626            index_is_partial: false,
2627        };
2628        assert!(base.lookup_contract_is_authoritative());
2629        let partial = WalPublicationSnapshot {
2630            index_is_partial: true,
2631            ..base
2632        };
2633        assert!(!partial.lookup_contract_is_authoritative());
2634    }
2635}