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