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