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