Skip to main content

clt_database/io/
mod.rs

1use crate::storage::buffer_pool::ArenaBuffer;
2use crate::storage::sqlite3_ondisk::WAL_FRAME_HEADER_SIZE;
3use crate::sync::Arc;
4use crate::turso_assert;
5use crate::{BufferPool, Result};
6use bitflags::bitflags;
7use cfg_block::cfg_block;
8use rand::{Rng, RngCore};
9use std::cell::RefCell;
10use std::collections::HashMap;
11use std::fmt;
12use std::ptr::NonNull;
13use std::sync::LazyLock;
14use std::{fmt::Debug, pin::Pin};
15use turso_macros::AtomicEnum;
16
17cfg_block! {
18    #[cfg(all(target_os = "linux", clt_turso_feature = "io_uring", not(miri)))] {
19        mod io_uring;
20        #[cfg(clt_turso_feature = "fs")]
21        pub use io_uring::UringIO;
22    }
23
24    #[cfg(all(target_family = "unix", not(miri)))] {
25        mod unix;
26        #[cfg(clt_turso_feature = "fs")]
27        pub use unix::UnixIO;
28        pub use unix::UnixIO as PlatformIO;
29        pub use PlatformIO as SyscallIO;
30    }
31
32    #[cfg(all(target_os = "windows", not(miri)))] {
33        mod windows;
34        #[cfg(clt_turso_feature = "fs")]
35        pub use windows::WindowsIO;
36        pub use windows::WindowsIO as PlatformIO;
37        pub use PlatformIO as SyscallIO;
38    }
39
40    #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp", not(miri)))] {
41        mod win_iocp;
42        #[cfg(clt_turso_feature = "fs")]
43        pub use win_iocp::WindowsIOCP;
44    }
45
46    #[cfg(any(not(any(target_family = "unix", target_os = "windows")), miri))] {
47        mod generic;
48        pub use generic::GenericIO as PlatformIO;
49        pub use PlatformIO as SyscallIO;
50    }
51}
52
53mod memory;
54#[cfg(clt_turso_feature = "io_memory_yield")]
55mod memory_yield;
56#[cfg(clt_turso_feature = "fs")]
57mod vfs;
58pub use memory::MemoryIO;
59#[cfg(clt_turso_feature = "io_memory_yield")]
60pub use memory_yield::MemoryYieldIO;
61pub mod clock;
62mod common;
63mod completions;
64pub use clock::Clock;
65pub use completions::*;
66
67/// Platform-independent file identity, analogous to SQLite's `struct unixFileId`.
68/// On Unix: (st_dev, st_ino). On Windows: (dwVolumeSerialNumber, nFileIndex).
69/// On non-filesystem backends: synthetic hash-based identity.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub struct FileId {
72    pub dev: u64,
73    pub ino: u64,
74}
75
76impl FileId {
77    /// Synthetic identity from a path hash, for backends without real inodes
78    /// (MemoryIO, OPFS, simulators).
79    pub fn from_path_hash(path: &str) -> Self {
80        use std::hash::{Hash, Hasher};
81        let mut hasher = std::collections::hash_map::DefaultHasher::new();
82        path.hash(&mut hasher);
83        FileId {
84            dev: 0,
85            ino: hasher.finish(),
86        }
87    }
88}
89
90/// Return the OS-level file identity for a path.
91#[cfg(unix)]
92pub fn get_file_id(path: &str) -> Result<FileId, std::io::Error> {
93    use std::os::unix::fs::MetadataExt;
94    let m = std::fs::metadata(path)?;
95    Ok(FileId {
96        dev: m.dev(),
97        ino: m.ino(),
98    })
99}
100
101#[cfg(windows)]
102pub fn get_file_id(path: &str) -> Result<FileId, std::io::Error> {
103    use std::os::windows::io::AsRawHandle;
104    use windows_sys::Win32::Storage::FileSystem::{
105        GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
106    };
107    let file = std::fs::File::open(path)?;
108    let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
109    let ret = unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut info) };
110    if ret == 0 {
111        return Err(std::io::Error::last_os_error());
112    }
113    Ok(FileId {
114        dev: info.dwVolumeSerialNumber as u64,
115        ino: (info.nFileIndexHigh as u64) << 32 | info.nFileIndexLow as u64,
116    })
117}
118
119#[cfg(not(any(unix, windows)))]
120pub fn get_file_id(path: &str) -> Result<FileId, std::io::Error> {
121    Ok(FileId::from_path_hash(path))
122}
123
124/// Controls which sync mechanism to use for durability.
125/// `FullFsync` only has effect on Apple platforms (uses F_FULLFSYNC fcntl).
126/// On other platforms, both variants behave the same (regular fsync).
127#[derive(Debug, Clone, Copy, PartialEq, Eq, AtomicEnum)]
128pub enum FileSyncType {
129    /// Regular fsync - flushes to disk but may not flush disk write cache on macOS.
130    Fsync,
131    /// Full fsync - on macOS uses F_FULLFSYNC to flush disk write cache.
132    /// On other platforms, behaves the same as Fsync.
133    FullFsync,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum SharedWalLockKind {
138    LinuxOfd,
139    ProcessScopedFcntl,
140}
141
142pub trait SharedWalMappedRegion: Send + Sync {
143    fn ptr(&self) -> NonNull<u8>;
144    fn len(&self) -> usize;
145
146    fn is_empty(&self) -> bool {
147        self.len() == 0
148    }
149}
150
151pub trait File: Send + Sync {
152    fn lock_file(&self, exclusive: bool) -> Result<()>;
153    fn unlock_file(&self) -> Result<()>;
154    fn pread(&self, pos: u64, c: Completion) -> Result<Completion>;
155    fn pwrite(&self, pos: u64, buffer: Arc<Buffer>, c: Completion) -> Result<Completion>;
156    /// Sync file data&metadata to disk.
157    fn sync(&self, c: Completion, sync_type: FileSyncType) -> Result<Completion>;
158    fn pwritev(&self, pos: u64, buffers: Vec<Arc<Buffer>>, c: Completion) -> Result<Completion> {
159        use crate::sync::atomic::{AtomicUsize, Ordering};
160        if buffers.is_empty() {
161            c.complete(0);
162            return Ok(c);
163        }
164        if buffers.len() == 1 {
165            return self.pwrite(pos, buffers[0].clone(), c);
166        }
167        // naive default implementation can be overridden on backends where it makes sense to
168        let mut pos = pos;
169        let outstanding = Arc::new(AtomicUsize::new(buffers.len()));
170        let total_written = Arc::new(AtomicUsize::new(0));
171
172        for buf in buffers {
173            let len = buf.len();
174            let child_c = {
175                let c_main = c.clone();
176                let outstanding = outstanding.clone();
177                let total_written = total_written.clone();
178                Completion::new_write(move |n| {
179                    if let Ok(n) = n {
180                        // accumulate bytes actually reported by the backend
181                        total_written.fetch_add(n as usize, Ordering::SeqCst);
182                        if outstanding.fetch_sub(1, Ordering::AcqRel) == 1 {
183                            // last one finished
184                            c_main.complete(total_written.load(Ordering::Acquire) as i32);
185                        }
186                    }
187                })
188            };
189            if let Err(e) = self.pwrite(pos, buf.clone(), child_c) {
190                c.abort();
191                return Err(e);
192            }
193            pos += len as u64;
194        }
195        Ok(c)
196    }
197    fn size(&self) -> Result<u64>;
198    fn truncate(&self, len: u64, c: Completion) -> Result<Completion>;
199
200    /// Optional method implemented by the IO which supports "partial" files (e.g. file with "holes")
201    /// This method is used in sync engine only for now (in partial sync mode) and never used in the core database code
202    ///
203    /// The hole is the contiguous file region which is not allocated by the file-system
204    /// If there is a single byte which is allocated within a given range - method must return false in this case
205    // todo: need to add custom completion type?
206    fn has_hole(&self, _pos: usize, _len: usize) -> Result<bool> {
207        panic!("has_hole is not supported for the given IO implementation")
208    }
209    /// Optional method implemented by the IO which supports "partial" files (e.g. file with "holes")
210    /// This method is used in sync engine only for now (in partial sync mode) and never used in the core database code
211    // todo: need to add custom completion type?
212    fn punch_hole(&self, _pos: usize, _len: usize) -> Result<()> {
213        panic!("punch_hole is not supported for the given IO implementation")
214    }
215
216    fn shared_wal_lock_byte(
217        &self,
218        _offset: u64,
219        _exclusive: bool,
220        _kind: SharedWalLockKind,
221    ) -> Result<()> {
222        Err(crate::LimboError::InternalError(
223            "shared WAL coordination byte locking is not supported for this file".into(),
224        ))
225    }
226
227    fn shared_wal_try_lock_byte(
228        &self,
229        _offset: u64,
230        _exclusive: bool,
231        _kind: SharedWalLockKind,
232    ) -> Result<bool> {
233        Err(crate::LimboError::InternalError(
234            "shared WAL coordination byte locking is not supported for this file".into(),
235        ))
236    }
237
238    fn shared_wal_unlock_byte(&self, _offset: u64, _kind: SharedWalLockKind) -> Result<()> {
239        Err(crate::LimboError::InternalError(
240            "shared WAL coordination byte unlocking is not supported for this file".into(),
241        ))
242    }
243
244    fn shared_wal_set_len(&self, _len: u64) -> Result<()> {
245        Err(crate::LimboError::InternalError(
246            "shared WAL coordination resizing is not supported for this file".into(),
247        ))
248    }
249
250    fn shared_wal_map(&self, _offset: u64, _len: usize) -> Result<Box<dyn SharedWalMappedRegion>> {
251        Err(crate::LimboError::InternalError(
252            "shared WAL coordination memory mapping is not supported for this file".into(),
253        ))
254    }
255}
256
257pub struct TempFile {
258    /// When temp_dir is dropped the folder is deleted
259    /// set to None if tempfile allocated in memory (for example, in case of WASM target)
260    _temp_dir: Option<tempfile::TempDir>,
261    pub(crate) file: Arc<dyn File>,
262}
263
264impl TempFile {
265    pub fn new(io: &Arc<dyn IO>) -> Result<Self> {
266        #[cfg(not(target_family = "wasm"))]
267        {
268            let temp_dir = tempfile::tempdir().map_err(|e| crate::error::io_error(e, "tempdir"))?;
269            let chunk_file_path = temp_dir.as_ref().join("tursodb_temp_file");
270            let chunk_file_path_str = chunk_file_path.to_str().ok_or_else(|| {
271                crate::LimboError::InternalError("temp file path is not valid UTF-8".to_string())
272            })?;
273            let chunk_file = io.open_file(chunk_file_path_str, OpenFlags::Create, false)?;
274            Ok(TempFile {
275                _temp_dir: Some(temp_dir),
276                file: chunk_file.clone(),
277            })
278        }
279        // on WASM in browser we do not support temp files (as we pre-register db files in advance and can't easily create a new one)
280        // so, for now, we use in-memory IO for tempfiles in WASM
281        #[cfg(target_family = "wasm")]
282        {
283            use crate::MemoryIO;
284
285            let memory_io = Arc::new(MemoryIO::new());
286            let memory_file = memory_io.open_file("tursodb_temp_file", OpenFlags::Create, false)?;
287            Ok(TempFile {
288                _temp_dir: None,
289                file: memory_file,
290            })
291        }
292    }
293
294    /// Creates a TempFile respecting the temp_store setting.
295    /// When temp_store is Memory, uses in-memory storage.
296    /// When temp_store is Default or File, uses file-based storage when
297    /// available. In `no-fs` builds, temp storage always falls back to memory.
298    pub fn with_temp_store(io: &Arc<dyn IO>, temp_store: crate::TempStore) -> Result<Self> {
299        #[cfg(not(target_family = "wasm"))]
300        {
301            #[cfg(not(clt_turso_feature = "fs"))]
302            {
303                let _ = (io, temp_store);
304                let memory_io = Arc::new(MemoryIO::new());
305                let memory_file =
306                    memory_io.open_file("tursodb_temp_file", OpenFlags::Create, false)?;
307                Ok(TempFile {
308                    _temp_dir: None,
309                    file: memory_file,
310                })
311            }
312            #[cfg(clt_turso_feature = "fs")]
313            {
314                if matches!(temp_store, crate::TempStore::Memory) {
315                    let memory_io = Arc::new(MemoryIO::new());
316                    let memory_file =
317                        memory_io.open_file("tursodb_temp_file", OpenFlags::Create, false)?;
318                    return Ok(TempFile {
319                        _temp_dir: None,
320                        file: memory_file,
321                    });
322                }
323                // Fall through to file-based for Default and File modes
324                Self::new(io)
325            }
326        }
327        #[cfg(target_family = "wasm")]
328        {
329            // WASM always uses memory, ignore temp_store setting
330            let _ = temp_store;
331            Self::new(io)
332        }
333    }
334}
335
336impl core::ops::Deref for TempFile {
337    type Target = Arc<dyn File>;
338
339    fn deref(&self) -> &Self::Target {
340        &self.file
341    }
342}
343
344#[derive(Debug, Copy, Clone, PartialEq)]
345pub struct OpenFlags(i32);
346
347// OpenFlags is a newtype over i32, which is inherently Send+Sync.
348// The assertion below verifies this at compile time.
349crate::assert::assert_send_sync!(OpenFlags);
350
351bitflags! {
352    impl OpenFlags: i32 {
353        const None = 0b00000000;
354        const Create = 0b0000001;
355        const ReadOnly = 0b0000010;
356        const NoLock = 0b0000100;
357    }
358}
359
360impl Default for OpenFlags {
361    fn default() -> Self {
362        Self::Create
363    }
364}
365
366pub trait IO: Clock + Send + Sync {
367    fn open_file(&self, path: &str, flags: OpenFlags, direct: bool) -> Result<Arc<dyn File>>;
368
369    fn open_shared_wal_file(&self, path: &str) -> Result<Arc<dyn File>> {
370        self.open_file(path, OpenFlags::Create | OpenFlags::NoLock, false)
371    }
372
373    // remove_file is used in the sync-engine
374    fn remove_file(&self, path: &str) -> Result<()>;
375
376    /// Whether this IO backend can back host-filesystem shared WAL coordination.
377    fn supports_shared_wal_coordination(&self) -> bool {
378        false
379    }
380
381    fn step(&self) -> Result<()> {
382        Ok(())
383    }
384
385    fn cancel(&self, c: &[Completion]) -> Result<()> {
386        c.iter().for_each(|c| c.abort());
387        Ok(())
388    }
389
390    /// Drive the IO backend until each completion in `completions` is
391    /// `finished()`. Used after `cancel()` (so cancelled ops actually
392    /// release their buffers before the caller returns) and after a
393    /// single `pwrite`/`pwritev`/`sync` that the caller wants to await
394    /// synchronously.
395    ///
396    /// Unlike a global "drain the ring" barrier, this only waits on the
397    /// completions the caller passes in. Other threads can keep
398    /// submitting concurrently — their work doesn't extend or interfere
399    /// with this call. `Completion::finished()` is monotonic
400    /// (`OnceLock`-backed), so the loop will terminate as soon as every
401    /// caller-owned completion has had its CQE processed.
402    fn drain_completions(&self, completions: &[Completion]) -> Result<()> {
403        while completions.iter().any(|c| !c.finished()) {
404            self.step()?;
405        }
406        Ok(())
407    }
408
409    fn wait_for_completion(&self, c: Completion) -> Result<()> {
410        while !c.finished() {
411            self.step()?
412        }
413        if let Some(inner) = &c.inner {
414            if let Some(Some(err)) = inner.result.get().copied() {
415                return Err(err.into());
416            }
417        }
418        Ok(())
419    }
420
421    fn generate_random_number(&self) -> i64 {
422        rand::rng().random()
423    }
424
425    /// Fill `dest` with random data.
426    fn fill_bytes(&self, dest: &mut [u8]) {
427        rand::rng().fill_bytes(dest);
428    }
429
430    fn get_memory_io(&self) -> Arc<MemoryIO> {
431        Arc::new(MemoryIO::new())
432    }
433
434    fn register_fixed_buffer(&self, _ptr: NonNull<u8>, _len: usize) -> Result<u32> {
435        Err(crate::LimboError::InternalError(
436            "unsupported operation".to_string(),
437        ))
438    }
439
440    /// Yield the current thread to the scheduler.
441    /// Used for backoff in contended lock acquisition.
442    fn yield_now(&self) {
443        crate::thread::yield_now();
444    }
445
446    /// Sleep for the specified duration.
447    /// Used for progressive backoff in contended lock acquisition.
448    fn sleep(&self, duration: std::time::Duration) {
449        crate::thread::sleep(duration);
450    }
451
452    /// Return the file identity for the given path.
453    /// Default uses OS-level metadata; non-filesystem backends override
454    /// with synthetic hash-based identity.
455    fn file_id(&self, path: &str) -> Result<FileId> {
456        get_file_id(path).map_err(|e| {
457            crate::LimboError::InternalError(format!(
458                "failed to get file identity for '{path}': {e}"
459            ))
460        })
461    }
462}
463
464/// Batches multiple vectored writes for submission.
465pub struct WriteBatch<'a> {
466    file: Arc<dyn File>,
467    ops: Vec<WriteOp<'a>>,
468}
469
470struct WriteOp<'a> {
471    pos: u64,
472    bufs: &'a [Arc<Buffer>],
473}
474
475impl<'a> WriteBatch<'a> {
476    pub fn new(file: Arc<dyn File>) -> Self {
477        Self {
478            file,
479            ops: Vec::new(),
480        }
481    }
482
483    #[inline]
484    pub fn writev(&mut self, pos: u64, bufs: &'a [Arc<Buffer>]) {
485        if !bufs.is_empty() {
486            self.ops.push(WriteOp { pos, bufs });
487        }
488    }
489
490    /// Total bytes across all operations.
491    #[inline]
492    pub fn total_bytes(&self) -> usize {
493        self.ops
494            .iter()
495            .map(|op| op.bufs.iter().map(|b| b.len()).sum::<usize>())
496            .sum()
497    }
498
499    /// Submit all writes. Returns completions caller must wait on.
500    #[inline]
501    pub fn submit(self) -> Result<Vec<Completion>> {
502        let mut completions = Vec::with_capacity(self.ops.len());
503        for WriteOp { pos, bufs } in self.ops {
504            let total_len = bufs.iter().map(|b| b.len()).sum::<usize>() as i32;
505            let c = Completion::new_write(move |res| {
506                let Ok(bytes_written) = res else {
507                    return;
508                };
509                turso_assert!(
510                    bytes_written == total_len,
511                    "pwritev wrote {bytes_written} bytes, expected {total_len}"
512                );
513            });
514            completions.push(self.file.pwritev(pos, bufs.to_vec(), c)?);
515        }
516        Ok(completions)
517    }
518
519    /// Returns the file for fsync after writes complete.
520    #[inline]
521    pub const fn file(&self) -> &Arc<dyn File> {
522        &self.file
523    }
524}
525
526pub type BufferData = Pin<Box<[u8]>>;
527
528#[derive(Clone)]
529pub enum SharedBufferData {
530    Full(Arc<Box<[u8]>>),
531    View(SharedBufferView),
532}
533
534#[derive(Clone)]
535pub struct SharedBufferView {
536    data: Arc<Box<[u8]>>,
537    start: usize,
538}
539
540impl SharedBufferView {
541    fn new(data: Arc<Box<[u8]>>, start: usize) -> Self {
542        assert!(
543            start <= data.len(),
544            "SharedBufferData::new_view: start ({start}) > data.len() ({})",
545            data.len()
546        );
547        Self { data, start }
548    }
549
550    pub fn len(&self) -> usize {
551        self.data.len() - self.start
552    }
553
554    pub fn is_empty(&self) -> bool {
555        self.len() == 0
556    }
557
558    pub fn as_slice(&self) -> &[u8] {
559        &self.data.as_ref().as_ref()[self.start..]
560    }
561
562    pub fn as_ptr(&self) -> *const u8 {
563        unsafe { self.data.as_ref().as_ptr().add(self.start) }
564    }
565}
566
567impl SharedBufferData {
568    pub fn new(data: Arc<Box<[u8]>>) -> Self {
569        Self::Full(data)
570    }
571
572    pub fn new_view(data: Arc<Box<[u8]>>, start: usize) -> Self {
573        Self::View(SharedBufferView::new(data, start))
574    }
575
576    pub fn len(&self) -> usize {
577        match self {
578            Self::Full(data) => data.len(),
579            Self::View(view) => view.len(),
580        }
581    }
582
583    pub fn is_empty(&self) -> bool {
584        self.len() == 0
585    }
586
587    pub fn as_slice(&self) -> &[u8] {
588        match self {
589            Self::Full(data) => data.as_ref().as_ref(),
590            Self::View(view) => view.as_slice(),
591        }
592    }
593
594    pub fn as_ptr(&self) -> *const u8 {
595        match self {
596            Self::Full(data) => data.as_ref().as_ptr(),
597            Self::View(view) => view.as_ptr(),
598        }
599    }
600}
601
602pub enum Buffer {
603    Heap(BufferData),
604    Shared(SharedBufferData),
605    /// A heap buffer with a logical start offset: only `data[start..]` is
606    /// exposed via [`Buffer::as_slice`] / [`Buffer::len`]. Used to skip a
607    /// pre-allocated prefix without shifting bytes in memory before I/O.
608    HeapView {
609        data: BufferData,
610        start: usize,
611    },
612    Pooled(ArenaBuffer),
613}
614
615impl Debug for Buffer {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        match self {
618            Self::Pooled(p) => write!(f, "Pooled(len={})", p.logical_len()),
619            Self::Heap(buf) => write!(f, "{buf:?}: {}", buf.len()),
620            Self::Shared(buf) => write!(f, "Shared(len={})", buf.len()),
621            Self::HeapView { data, start } => {
622                write!(
623                    f,
624                    "HeapView({start}..{}, view_len={})",
625                    data.len(),
626                    data.len() - start
627                )
628            }
629        }
630    }
631}
632
633impl Drop for Buffer {
634    fn drop(&mut self) {
635        match self {
636            Self::Heap(buf) | Self::HeapView { data: buf, .. } => {
637                let underlying_len = buf.len();
638                TEMP_BUFFER_CACHE.with(|cache| {
639                    let mut cache = cache.borrow_mut();
640                    // take ownership of the buffer by swapping it with a dummy
641                    let buffer = std::mem::replace(buf, Pin::new(vec![].into_boxed_slice()));
642                    cache.return_buffer(buffer, underlying_len);
643                });
644            }
645            Self::Pooled(_) | Self::Shared(_) => {}
646        }
647    }
648}
649
650impl Buffer {
651    pub fn new(data: Vec<u8>) -> Self {
652        tracing::trace!("buffer::new({:?})", data);
653        Self::Heap(Pin::new(data.into_boxed_slice()))
654    }
655
656    pub fn new_shared(data: Arc<Box<[u8]>>) -> Self {
657        Self::Shared(SharedBufferData::new(data))
658    }
659
660    pub fn new_shared_data(data: SharedBufferData) -> Self {
661        Self::Shared(data)
662    }
663
664    /// Wraps `data` so that only bytes `[start..]` are visible via
665    /// [`Buffer::as_slice`] / [`Buffer::len`]. The skipped prefix lives in
666    /// memory but is never read by the I/O layer — useful when a caller
667    /// has pre-allocated optional framing room at the front of a buffer
668    /// and wants to elide it on a particular write without a memmove.
669    pub fn new_with_start(data: Vec<u8>, start: usize) -> Self {
670        assert!(
671            start <= data.len(),
672            "Buffer::new_with_start: start ({start}) > data.len() ({})",
673            data.len()
674        );
675        Self::HeapView {
676            data: Pin::new(data.into_boxed_slice()),
677            start,
678        }
679    }
680
681    /// Returns the index of the underlying `Arena` if it was registered with
682    /// io_uring. Only for use with `UringIO` backend.
683    pub fn fixed_id(&self) -> Option<u32> {
684        match self {
685            Self::Heap(..) | Self::HeapView { .. } | Self::Shared(..) => None,
686            Self::Pooled(buf) => buf.fixed_id(),
687        }
688    }
689
690    pub fn new_pooled(buf: ArenaBuffer) -> Self {
691        Self::Pooled(buf)
692    }
693
694    pub fn new_temporary(size: usize) -> Self {
695        TEMP_BUFFER_CACHE.with(|cache| {
696            if let Some(buffer) = cache.borrow_mut().get_buffer(size) {
697                Self::Heap(buffer)
698            } else {
699                Self::Heap(Pin::new(vec![0; size].into_boxed_slice()))
700            }
701        })
702    }
703
704    pub fn len(&self) -> usize {
705        match self {
706            Self::Heap(buf) => buf.len(),
707            Self::Shared(buf) => buf.len(),
708            Self::HeapView { data, start } => data.len() - *start,
709            Self::Pooled(buf) => buf.logical_len(),
710        }
711    }
712
713    pub fn is_empty(&self) -> bool {
714        self.len() == 0
715    }
716
717    pub fn as_slice(&self) -> &[u8] {
718        match self {
719            Self::Heap(buf) => {
720                // SAFETY: The buffer is guaranteed to be valid for the lifetime of the slice
721                unsafe { std::slice::from_raw_parts(buf.as_ptr(), buf.len()) }
722            }
723            Self::Shared(buf) => buf.as_slice(),
724            Self::HeapView { data, start } => {
725                // SAFETY: `start` was bounds-checked at construction; the buffer
726                // is valid for the lifetime of the returned slice.
727                unsafe {
728                    std::slice::from_raw_parts(data.as_ptr().add(*start), data.len() - *start)
729                }
730            }
731            Self::Pooled(buf) => buf,
732        }
733    }
734
735    #[allow(clippy::mut_from_ref)]
736    pub fn as_mut_slice(&self) -> &mut [u8] {
737        unsafe { std::slice::from_raw_parts_mut(self.as_mut_ptr(), self.len()) }
738    }
739    #[inline]
740    pub fn as_ptr(&self) -> *const u8 {
741        match self {
742            Self::Heap(buf) => buf.as_ptr(),
743            Self::Shared(buf) => buf.as_ptr(),
744            Self::HeapView { data, start } => unsafe { data.as_ptr().add(*start) },
745            Self::Pooled(buf) => buf.as_ptr(),
746        }
747    }
748    #[inline]
749    pub fn as_mut_ptr(&self) -> *mut u8 {
750        match self {
751            Self::Heap(buf) => buf.as_ptr() as *mut u8,
752            Self::Shared(_) => panic!("Buffer::Shared is immutable"),
753            Self::HeapView { data, start } => unsafe { (data.as_ptr() as *mut u8).add(*start) },
754            Self::Pooled(buf) => buf.as_ptr() as *mut u8,
755        }
756    }
757
758    #[inline]
759    pub fn is_pooled(&self) -> bool {
760        matches!(self, Self::Pooled(..))
761    }
762
763    #[inline]
764    pub fn is_heap(&self) -> bool {
765        matches!(self, Self::Heap(..) | Self::HeapView { .. })
766    }
767}
768
769crate::thread::thread_local! {
770    /// thread local cache to re-use temporary buffers to prevent churn when pool overflows
771    pub static TEMP_BUFFER_CACHE: RefCell<TempBufferCache> = RefCell::new(TempBufferCache::new());
772}
773
774#[cfg(clt_turso_tests)]
775mod buffer_tests {
776    use super::*;
777
778    #[test]
779    fn shared_buffer_exposes_arc_bytes() {
780        let data = Arc::new(vec![1, 2, 3, 4].into_boxed_slice());
781        let buffer = Buffer::new_shared(data.clone());
782
783        assert_eq!(buffer.len(), 4);
784        assert_eq!(buffer.as_slice(), &[1, 2, 3, 4]);
785        assert_eq!(buffer.as_ptr(), data.as_ref().as_ptr());
786        assert!(!buffer.is_heap());
787        assert!(!buffer.is_pooled());
788    }
789
790    #[test]
791    fn shared_buffer_view_exposes_tail_without_copying() {
792        let data = Arc::new(vec![0, 1, 2, 3, 4].into_boxed_slice());
793        let shared = SharedBufferData::new_view(data.clone(), 2);
794        let buffer = Buffer::new_shared_data(shared.clone());
795
796        assert_eq!(shared.len(), 3);
797        assert_eq!(shared.as_slice(), &[2, 3, 4]);
798        assert_eq!(shared.as_ptr(), unsafe { data.as_ref().as_ptr().add(2) });
799        assert_eq!(buffer.len(), 3);
800        assert_eq!(buffer.as_slice(), &[2, 3, 4]);
801        assert_eq!(buffer.as_ptr(), shared.as_ptr());
802    }
803}
804
805/// A cache for temporary or any additional `Buffer` allocations beyond
806/// what the `BufferPool` has room for, or for use before the pool is
807/// fully initialized.
808pub(crate) struct TempBufferCache {
809    /// The `[Database::page_size]` at the time the cache is initiated.
810    page_size: usize,
811    /// Cache of buffers of size `self.page_size`.
812    page_buffers: Vec<BufferData>,
813    /// Cache of buffers of size `self.page_size` + WAL_FRAME_HEADER_SIZE.
814    wal_frame_buffers: Vec<BufferData>,
815    /// Maximum number of buffers that will live in each cache.
816    max_cached: usize,
817}
818
819impl TempBufferCache {
820    const DEFAULT_MAX_CACHE_SIZE: usize = 256;
821
822    fn new() -> Self {
823        Self {
824            page_size: BufferPool::DEFAULT_PAGE_SIZE,
825            page_buffers: Vec::with_capacity(8),
826            wal_frame_buffers: Vec::with_capacity(8),
827            max_cached: Self::DEFAULT_MAX_CACHE_SIZE,
828        }
829    }
830
831    /// If the `[Database::page_size]` is set, any temporary buffers that might
832    /// exist prior need to be cleared and new `page_size` needs to be saved.
833    pub fn reinit_cache(&mut self, page_size: usize) {
834        self.page_buffers.clear();
835        self.wal_frame_buffers.clear();
836        self.page_size = page_size;
837    }
838
839    fn get_buffer(&mut self, size: usize) -> Option<BufferData> {
840        match size {
841            sz if sz == self.page_size => self.page_buffers.pop(),
842            sz if sz == (self.page_size + WAL_FRAME_HEADER_SIZE) => self.wal_frame_buffers.pop(),
843            _ => None,
844        }
845    }
846
847    fn return_buffer(&mut self, buff: BufferData, len: usize) {
848        let sz = self.page_size;
849        let cache = match len {
850            n if n.eq(&sz) => &mut self.page_buffers,
851            n if n.eq(&(sz + WAL_FRAME_HEADER_SIZE)) => &mut self.wal_frame_buffers,
852            _ => return,
853        };
854        if self.max_cached > cache.len() {
855            cache.push(buff);
856        }
857    }
858}
859
860// Runtime-registrable Rust IO backends, resolved by `Database::io_for_vfs`.
861#[allow(clippy::type_complexity)]
862static IO_REGISTRY: LazyLock<parking_lot::Mutex<HashMap<String, Arc<dyn IO>>>> =
863    LazyLock::new(|| parking_lot::Mutex::new(HashMap::new()));
864
865const BUILTIN_VFS_NAMES: &[&str] = &["memory", "syscall", "io_uring", "experimental_win_iocp"];
866
867/// Register a named Rust IO backend.
868///
869/// Once registered, it can be used via [`Database::io_for_vfs`] or through
870/// any language binding's `vfs=` parameter (Go DSN, Python kwarg, etc.).
871///
872/// Re-registering the same name replaces the previous backend. Registered
873/// names take precedence over C VFS extensions and built-in backends
874/// (`"memory"`, `"syscall"`, `"io_uring"`), so registering a built-in name
875/// will shadow the default implementation.
876///
877/// # Errors
878///
879/// Returns [`LimboError::InvalidArgument`] if `name` is empty.
880pub fn register_io(name: &str, io: Arc<dyn IO>) -> crate::Result<()> {
881    if name.is_empty() {
882        return Err(crate::LimboError::InvalidArgument(
883            "IO backend name must not be empty".into(),
884        ));
885    }
886    if BUILTIN_VFS_NAMES.contains(&name) {
887        tracing::warn!("registered IO backend \"{name}\" shadows a built-in VFS");
888    }
889    IO_REGISTRY.lock().insert(name.to_string(), io);
890    Ok(())
891}
892
893/// Remove a registered Rust IO backend by name.
894///
895/// Returns `true` if an entry was removed, `false` if the name was not found.
896pub fn unregister_io(name: &str) -> bool {
897    IO_REGISTRY.lock().remove(name).is_some()
898}
899
900/// Look up a registered Rust IO backend by name.
901pub fn get_registered_io(name: &str) -> Option<Arc<dyn IO>> {
902    IO_REGISTRY.lock().get(name).cloned()
903}
904
905/// List all registered Rust IO backend names.
906pub fn list_registered_io() -> Vec<String> {
907    IO_REGISTRY.lock().keys().cloned().collect()
908}
909
910#[cfg(clt_turso_tests)]
911mod io_registry_tests {
912    use super::*;
913
914    #[test]
915    fn register_and_retrieve() {
916        let io = Arc::new(MemoryIO::new());
917        register_io("ioreg::retrieve", io).unwrap();
918        assert!(get_registered_io("ioreg::retrieve").is_some());
919        assert!(get_registered_io("nonexistent").is_none());
920        unregister_io("ioreg::retrieve");
921    }
922
923    #[test]
924    fn re_register_replaces() {
925        let io1 = Arc::new(MemoryIO::new());
926        let io2 = Arc::new(MemoryIO::new());
927        register_io("ioreg::replace", io1).unwrap();
928        register_io("ioreg::replace", io2).unwrap();
929        let count = list_registered_io()
930            .into_iter()
931            .filter(|n| n == "ioreg::replace")
932            .count();
933        assert_eq!(count, 1, "should not duplicate entries");
934        unregister_io("ioreg::replace");
935    }
936
937    #[test]
938    fn unregister_returns_false_for_missing() {
939        assert!(!unregister_io("ioreg::never_registered"));
940    }
941
942    #[test]
943    fn unregister_removes() {
944        let io = Arc::new(MemoryIO::new());
945        register_io("ioreg::removable", io).unwrap();
946        assert!(unregister_io("ioreg::removable"));
947        assert!(get_registered_io("ioreg::removable").is_none());
948    }
949
950    #[test]
951    fn list_includes_registered() {
952        let io = Arc::new(MemoryIO::new());
953        register_io("ioreg::listed", io).unwrap();
954        assert!(list_registered_io().contains(&"ioreg::listed".to_string()));
955        unregister_io("ioreg::listed");
956    }
957
958    #[test]
959    fn empty_name_returns_error() {
960        let result = register_io("", Arc::new(MemoryIO::new()));
961        assert!(result.is_err());
962    }
963}
964
965#[cfg(all(shuttle, clt_turso_tests))]
966mod shuttle_tests {
967    use std::path::PathBuf;
968
969    use super::*;
970    use crate::io::{Buffer, Completion, OpenFlags, IO};
971    use crate::sync::atomic::{AtomicUsize, Ordering};
972    use crate::sync::Arc;
973    use crate::thread;
974
975    /// Factory trait for creating IO implementations in tests.
976    /// Allows the same test logic to run against different IO backends.
977    trait IOFactory: Send + Sync + 'static {
978        fn create(&self) -> Arc<dyn IO>;
979        /// Returns a unique temp directory path for this factory instance.
980        fn temp_dir(&self) -> PathBuf;
981    }
982
983    struct MemoryIOFactory {
984        id: u64,
985    }
986
987    impl MemoryIOFactory {
988        fn new() -> Self {
989            use crate::sync::atomic::AtomicU64;
990            static COUNTER: AtomicU64 = AtomicU64::new(0);
991            Self {
992                id: COUNTER.fetch_add(1, Ordering::SeqCst),
993            }
994        }
995    }
996
997    impl IOFactory for MemoryIOFactory {
998        fn create(&self) -> Arc<dyn IO> {
999            Arc::new(MemoryIO::new())
1000        }
1001        fn temp_dir(&self) -> PathBuf {
1002            format!("mem_{}", self.id).into()
1003        }
1004    }
1005
1006    #[cfg(all(target_family = "unix", clt_turso_feature = "fs", not(miri)))]
1007    struct PlatformIOFactory {
1008        temp_dir: tempfile::TempDir,
1009    }
1010
1011    #[cfg(all(target_family = "unix", clt_turso_feature = "fs", not(miri)))]
1012    impl PlatformIOFactory {
1013        fn new() -> Self {
1014            Self {
1015                temp_dir: tempfile::tempdir().unwrap(),
1016            }
1017        }
1018    }
1019
1020    #[cfg(all(target_family = "unix", clt_turso_feature = "fs", not(miri)))]
1021    impl IOFactory for PlatformIOFactory {
1022        fn create(&self) -> Arc<dyn IO> {
1023            Arc::new(PlatformIO::new().unwrap())
1024        }
1025        fn temp_dir(&self) -> PathBuf {
1026            self.temp_dir.path().to_path_buf()
1027        }
1028    }
1029
1030    #[cfg(all(
1031        target_os = "linux",
1032        clt_turso_feature = "io_uring",
1033        clt_turso_feature = "fs",
1034        not(miri)
1035    ))]
1036    struct UringIOFactory {
1037        temp_dir: tempfile::TempDir,
1038    }
1039
1040    #[cfg(all(
1041        target_os = "linux",
1042        clt_turso_feature = "io_uring",
1043        clt_turso_feature = "fs",
1044        not(miri)
1045    ))]
1046    impl UringIOFactory {
1047        fn new() -> Self {
1048            Self {
1049                temp_dir: tempfile::tempdir().unwrap(),
1050            }
1051        }
1052    }
1053
1054    #[cfg(all(
1055        target_os = "linux",
1056        clt_turso_feature = "io_uring",
1057        clt_turso_feature = "fs",
1058        not(miri)
1059    ))]
1060    impl IOFactory for UringIOFactory {
1061        fn create(&self) -> Arc<dyn IO> {
1062            Arc::new(UringIO::new().unwrap())
1063        }
1064        fn temp_dir(&self) -> PathBuf {
1065            self.temp_dir.path().to_path_buf()
1066        }
1067    }
1068
1069    #[cfg(all(
1070        target_os = "windows",
1071        clt_turso_feature = "experimental_win_iocp",
1072        clt_turso_feature = "fs",
1073        not(miri)
1074    ))]
1075    struct WinIOCPFactory {
1076        temp_dir: tempfile::TempDir,
1077    }
1078
1079    #[cfg(all(
1080        target_os = "windows",
1081        clt_turso_feature = "experimental_win_iocp",
1082        clt_turso_feature = "fs",
1083        not(miri)
1084    ))]
1085    impl WinIOCPFactory {
1086        fn new() -> Self {
1087            Self {
1088                temp_dir: tempfile::tempdir().unwrap(),
1089            }
1090        }
1091    }
1092
1093    #[cfg(all(
1094        target_os = "windows",
1095        clt_turso_feature = "experimental_win_iocp",
1096        clt_turso_feature = "fs",
1097        not(miri)
1098    ))]
1099    impl IOFactory for WinIOCPFactory {
1100        fn create(&self) -> Arc<dyn IO> {
1101            Arc::new(WindowsIOCP::new().unwrap())
1102        }
1103        fn temp_dir(&self) -> PathBuf {
1104            self.temp_dir.path().to_path_buf()
1105        }
1106    }
1107
1108    /// Macro to generate shuttle tests for all IO implementations.
1109    /// Creates a test for MemoryIO, and conditionally for PlatformIO and UringIO.
1110    macro_rules! shuttle_io_test {
1111        ($test_name:ident, $test_impl:ident) => {
1112            pastey::paste! {
1113                #[test]
1114                fn [<shuttle_ $test_name _memory>]() {
1115                    shuttle::check_random(|| $test_impl(MemoryIOFactory::new()), 1000);
1116                }
1117
1118                #[cfg(all(target_family = "unix", clt_turso_feature = "fs", not(miri)))]
1119                #[test]
1120                fn [<shuttle_ $test_name _platform>]() {
1121                    shuttle::check_random(|| $test_impl(PlatformIOFactory::new()), 1000);
1122                }
1123
1124                #[cfg(all(target_os = "linux", clt_turso_feature = "io_uring", clt_turso_feature = "fs", not(miri)))]
1125                #[test]
1126                fn [<shuttle_ $test_name _uring>]() {
1127                    shuttle::check_random(|| $test_impl(UringIOFactory::new()), 1000);
1128                }
1129
1130                #[cfg(all(target_os = "windows", clt_turso_feature = "experimental_win_iocp", clt_turso_feature = "fs", not(miri)))]
1131                #[test]
1132                fn [<shuttle_ $test_name _win_iocp>]() {
1133                    shuttle::check_random(|| $test_impl(WinIOCPFactory::new()), 1000);
1134                }
1135
1136            }
1137        };
1138    }
1139
1140    /// Helper to wait for a completion synchronously and assert it succeeded.
1141    fn wait_completion_ok(io: &dyn IO, c: &Completion) {
1142        io.wait_for_completion(c.clone()).unwrap();
1143        assert!(c.succeeded(), "completion failed: {:?}", c.get_error());
1144        assert!(!c.failed());
1145        assert!(c.finished());
1146        assert!(c.get_error().is_none());
1147    }
1148
1149    /// Helper to wait for a completion synchronously without asserting success.
1150    #[allow(dead_code)]
1151    fn wait_completion(io: &dyn IO, c: &Completion) {
1152        io.wait_for_completion(c.clone()).unwrap();
1153        assert!(c.finished());
1154    }
1155
1156    /// Test concurrent file creation from multiple threads.
1157    fn test_concurrent_file_creation_impl<F: IOFactory>(factory: F) {
1158        let io = factory.create();
1159        let base = factory.temp_dir();
1160        let mut handles = vec![];
1161        const NUM_THREADS: usize = 3;
1162
1163        for i in 0..NUM_THREADS {
1164            let io = io.clone();
1165            let base = base.clone();
1166            handles.push(thread::spawn(move || {
1167                let path = base.join(format!("test_file_{}.db", i));
1168                let file = io
1169                    .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1170                    .unwrap();
1171                assert!(file.size().unwrap() == 0);
1172            }));
1173        }
1174
1175        for h in handles {
1176            h.join().unwrap();
1177        }
1178    }
1179
1180    shuttle_io_test!(concurrent_file_creation, test_concurrent_file_creation_impl);
1181
1182    /// Test concurrent writes to different offsets in the same file.
1183    fn test_concurrent_writes_different_offsets_impl<F: IOFactory>(factory: F) {
1184        let io = factory.create();
1185        let path = factory.temp_dir().join("test.db");
1186        let file = io
1187            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1188            .unwrap();
1189
1190        let mut handles = vec![];
1191        const NUM_THREADS: usize = 3;
1192
1193        for i in 0..NUM_THREADS {
1194            let file = file.clone();
1195            let io = io.clone();
1196            handles.push(thread::spawn(move || {
1197                let data = vec![i as u8; 100];
1198                let buf = Arc::new(Buffer::new(data));
1199                let pos = (i * 100) as u64;
1200
1201                let c = Completion::new_write(|_| {});
1202                let c = file.pwrite(pos, buf, c).unwrap();
1203                wait_completion_ok(io.as_ref(), &c);
1204            }));
1205        }
1206
1207        for h in handles {
1208            h.join().unwrap();
1209        }
1210
1211        // Verify file size accounts for all writes
1212        let expected_size = (NUM_THREADS * 100) as u64;
1213        assert_eq!(file.size().unwrap(), expected_size);
1214
1215        // Read back and verify each segment contains correct data
1216        for i in 0..NUM_THREADS {
1217            let read_buf = Arc::new(Buffer::new_temporary(100));
1218            let pos = (i * 100) as u64;
1219            let c = Completion::new_read(read_buf.clone(), |_| None);
1220            let c = file.pread(pos, c).unwrap();
1221            wait_completion_ok(io.as_ref(), &c);
1222
1223            let expected = vec![i as u8; 100];
1224            assert_eq!(
1225                read_buf.as_slice(),
1226                expected.as_slice(),
1227                "data mismatch at offset {}",
1228                pos
1229            );
1230        }
1231    }
1232
1233    shuttle_io_test!(
1234        concurrent_writes_different_offsets,
1235        test_concurrent_writes_different_offsets_impl
1236    );
1237
1238    /// Test concurrent reads and writes to the same file.
1239    fn test_concurrent_read_write_impl<F: IOFactory>(factory: F) {
1240        let io = factory.create();
1241        let path = factory.temp_dir().join("test.db");
1242        let file = io
1243            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1244            .unwrap();
1245
1246        // First write some initial data
1247        let initial_data = vec![0xAA; 1000];
1248        let buf = Arc::new(Buffer::new(initial_data));
1249        let c = Completion::new_write(|_| {});
1250        let c = file.pwrite(0, buf, c).unwrap();
1251        wait_completion_ok(io.as_ref(), &c);
1252
1253        let mut handles = vec![];
1254
1255        // Spawn readers
1256        for _ in 0..2 {
1257            let file = file.clone();
1258            let io = io.clone();
1259            handles.push(thread::spawn(move || {
1260                let read_buf = Arc::new(Buffer::new_temporary(100));
1261                let c = Completion::new_read(read_buf.clone(), |_| None);
1262                let c = file.pread(0, c).unwrap();
1263                wait_completion_ok(io.as_ref(), &c);
1264
1265                // All bytes read should be 0xAA (initial data at offset 0)
1266                assert!(
1267                    read_buf.as_slice().iter().all(|&b| b == 0xAA),
1268                    "read buffer should contain initial data 0xAA"
1269                );
1270            }));
1271        }
1272
1273        // Spawn a writer
1274        {
1275            let file = file.clone();
1276            let io = io.clone();
1277            handles.push(thread::spawn(move || {
1278                let data = vec![0xBB; 100];
1279                let buf = Arc::new(Buffer::new(data));
1280                let c = Completion::new_write(|_| {});
1281                let c = file.pwrite(500, buf, c).unwrap();
1282                wait_completion_ok(io.as_ref(), &c);
1283            }));
1284        }
1285
1286        for h in handles {
1287            h.join().unwrap();
1288        }
1289
1290        // Verify the write at offset 500 succeeded
1291        let read_buf = Arc::new(Buffer::new_temporary(100));
1292        let c = Completion::new_read(read_buf.clone(), |_| None);
1293        let c = file.pread(500, c).unwrap();
1294        wait_completion_ok(io.as_ref(), &c);
1295        assert!(
1296            read_buf.as_slice().iter().all(|&b| b == 0xBB),
1297            "data at offset 500 should be 0xBB"
1298        );
1299    }
1300
1301    shuttle_io_test!(concurrent_read_write, test_concurrent_read_write_impl);
1302
1303    /// Test that completion callbacks are invoked correctly under concurrency.
1304    fn test_completion_callbacks_concurrent_impl<F: IOFactory>(factory: F) {
1305        let io = factory.create();
1306        let path = factory.temp_dir().join("test.db");
1307        let file = io
1308            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1309            .unwrap();
1310
1311        let callback_count = Arc::new(AtomicUsize::new(0));
1312        let mut handles = vec![];
1313        const NUM_WRITES: usize = 3;
1314
1315        for i in 0..NUM_WRITES {
1316            let file = file.clone();
1317            let io = io.clone();
1318            let count = callback_count.clone();
1319            handles.push(thread::spawn(move || {
1320                let data = vec![i as u8; 50];
1321                let buf = Arc::new(Buffer::new(data));
1322                let count_clone = count.clone();
1323                let c = Completion::new_write(move |res| {
1324                    assert!(res.is_ok());
1325                    count_clone.fetch_add(1, Ordering::SeqCst);
1326                });
1327                let c = file.pwrite((i * 50) as u64, buf, c).unwrap();
1328                wait_completion_ok(io.as_ref(), &c);
1329            }));
1330        }
1331
1332        for h in handles {
1333            h.join().unwrap();
1334        }
1335
1336        assert_eq!(callback_count.load(Ordering::SeqCst), NUM_WRITES);
1337    }
1338
1339    shuttle_io_test!(
1340        completion_callbacks_concurrent,
1341        test_completion_callbacks_concurrent_impl
1342    );
1343
1344    /// Test concurrent truncate operations.
1345    fn test_concurrent_truncate_impl<F: IOFactory>(factory: F) {
1346        let io = factory.create();
1347        let path = factory.temp_dir().join("test.db");
1348        let file = io
1349            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1350            .unwrap();
1351
1352        // Write initial data
1353        let initial = vec![0xFF; 5000];
1354        let buf = Arc::new(Buffer::new(initial));
1355        let c = Completion::new_write(|_| {});
1356        let c = file.pwrite(0, buf, c).unwrap();
1357        wait_completion_ok(io.as_ref(), &c);
1358
1359        let mut handles = vec![];
1360
1361        // Spawn threads that truncate to different sizes
1362        for i in 0..3 {
1363            let file = file.clone();
1364            let io = io.clone();
1365            handles.push(thread::spawn(move || {
1366                let truncate_size = ((i + 1) * 1000) as u64;
1367                let c = Completion::new_trunc(|_| {});
1368                let c = file.truncate(truncate_size, c).unwrap();
1369                wait_completion_ok(io.as_ref(), &c);
1370            }));
1371        }
1372
1373        for h in handles {
1374            h.join().unwrap();
1375        }
1376
1377        // Size should be one of the truncate values
1378        let final_size = file.size().unwrap();
1379        assert!(final_size == 1000 || final_size == 2000 || final_size == 3000);
1380    }
1381
1382    shuttle_io_test!(concurrent_truncate, test_concurrent_truncate_impl);
1383
1384    /// Test pwritev with concurrent reads.
1385    fn test_pwritev_with_concurrent_reads_impl<F: IOFactory>(factory: F) {
1386        let io = factory.create();
1387        let path = factory.temp_dir().join("test.db");
1388        let file = io
1389            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1390            .unwrap();
1391
1392        // Write initial data so reads have something to return
1393        let initial = vec![0x11; 2000];
1394        let buf = Arc::new(Buffer::new(initial));
1395        let c = Completion::new_write(|_| {});
1396        let c = file.pwrite(0, buf, c).unwrap();
1397        wait_completion_ok(io.as_ref(), &c);
1398
1399        let mut handles = vec![];
1400
1401        // Spawn a pwritev thread
1402        {
1403            let file = file.clone();
1404            let io = io.clone();
1405            handles.push(thread::spawn(move || {
1406                let bufs = vec![
1407                    Arc::new(Buffer::new(vec![0x22; 100])),
1408                    Arc::new(Buffer::new(vec![0x33; 100])),
1409                    Arc::new(Buffer::new(vec![0x44; 100])),
1410                ];
1411                let c = Completion::new_write(|_| {});
1412                let c = file.pwritev(0, bufs, c).unwrap();
1413                wait_completion_ok(io.as_ref(), &c);
1414            }));
1415        }
1416
1417        // Spawn reader threads
1418        for _ in 0..2 {
1419            let file = file.clone();
1420            let io = io.clone();
1421            handles.push(thread::spawn(move || {
1422                let buf = Arc::new(Buffer::new_temporary(100));
1423                let c = Completion::new_read(buf.clone(), |_| None);
1424                let c = file.pread(0, c).unwrap();
1425                wait_completion_ok(io.as_ref(), &c);
1426
1427                // Data should be either initial (0x11) or from pwritev (0x22)
1428                // depending on race ordering
1429                let first_byte = buf.as_slice()[0];
1430                assert!(
1431                    first_byte == 0x11 || first_byte == 0x22,
1432                    "first byte should be 0x11 or 0x22, got {:#x}",
1433                    first_byte
1434                );
1435                // All 100 bytes should be consistent
1436                assert!(
1437                    buf.as_slice().iter().all(|&b| b == first_byte),
1438                    "all bytes should be the same value"
1439                );
1440            }));
1441        }
1442
1443        for h in handles {
1444            h.join().unwrap();
1445        }
1446
1447        // After all threads complete, verify pwritev data is present
1448        let read_buf = Arc::new(Buffer::new_temporary(300));
1449        let c = Completion::new_read(read_buf.clone(), |_| None);
1450        let c = file.pread(0, c).unwrap();
1451        wait_completion_ok(io.as_ref(), &c);
1452
1453        // Should have 0x22 for first 100, 0x33 for next 100, 0x44 for last 100
1454        assert!(
1455            read_buf.as_slice()[..100].iter().all(|&b| b == 0x22),
1456            "bytes 0-99 should be 0x22"
1457        );
1458        assert!(
1459            read_buf.as_slice()[100..200].iter().all(|&b| b == 0x33),
1460            "bytes 100-199 should be 0x33"
1461        );
1462        assert!(
1463            read_buf.as_slice()[200..300].iter().all(|&b| b == 0x44),
1464            "bytes 200-299 should be 0x44"
1465        );
1466    }
1467
1468    shuttle_io_test!(
1469        pwritev_with_concurrent_reads,
1470        test_pwritev_with_concurrent_reads_impl
1471    );
1472
1473    /// Test concurrent access to multiple files.
1474    fn test_concurrent_multifile_access_impl<F: IOFactory>(factory: F) {
1475        let io = factory.create();
1476        let base = factory.temp_dir();
1477
1478        let mut handles = vec![];
1479        const NUM_FILES: usize = 3;
1480
1481        for i in 0..NUM_FILES {
1482            let io = io.clone();
1483            let base = base.clone();
1484            handles.push(thread::spawn(move || {
1485                let path = base.join(format!("file_{}.db", i));
1486                let file = io
1487                    .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1488                    .unwrap();
1489
1490                // Write to file
1491                let data = vec![i as u8; 200];
1492                let buf = Arc::new(Buffer::new(data.clone()));
1493                let c = Completion::new_write(|_| {});
1494                let c = file.pwrite(0, buf, c).unwrap();
1495                wait_completion_ok(io.as_ref(), &c);
1496
1497                // Read back and verify
1498                let read_buf = Arc::new(Buffer::new_temporary(200));
1499                let c = Completion::new_read(read_buf.clone(), |_| None);
1500                let c = file.pread(0, c).unwrap();
1501                wait_completion_ok(io.as_ref(), &c);
1502
1503                assert_eq!(read_buf.as_slice(), data.as_slice());
1504            }));
1505        }
1506
1507        for h in handles {
1508            h.join().unwrap();
1509        }
1510    }
1511
1512    shuttle_io_test!(
1513        concurrent_multifile_access,
1514        test_concurrent_multifile_access_impl
1515    );
1516
1517    /// Test file locking under concurrent access.
1518    fn test_file_locking_concurrent_impl<F: IOFactory>(factory: F) {
1519        let io = factory.create();
1520        let path = factory.temp_dir().join("test.db");
1521        let file = io
1522            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1523            .unwrap();
1524
1525        let mut handles = vec![];
1526
1527        // Multiple threads try to lock/unlock
1528        for _ in 0..3 {
1529            let file = file.clone();
1530            handles.push(thread::spawn(move || {
1531                // Exclusive lock
1532                file.lock_file(true).unwrap();
1533                thread::yield_now();
1534                file.unlock_file().unwrap();
1535
1536                // Shared lock
1537                file.lock_file(false).unwrap();
1538                thread::yield_now();
1539                file.unlock_file().unwrap();
1540            }));
1541        }
1542
1543        for h in handles {
1544            h.join().unwrap();
1545        }
1546    }
1547
1548    shuttle_io_test!(file_locking_concurrent, test_file_locking_concurrent_impl);
1549
1550    /// Test reading past end of file returns zero bytes.
1551    fn test_read_past_eof_impl<F: IOFactory>(factory: F) {
1552        let io = factory.create();
1553        let path = factory.temp_dir().join("test.db");
1554        let file = io
1555            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1556            .unwrap();
1557
1558        // Write 100 bytes
1559        let data = vec![0xAA; 100];
1560        let buf = Arc::new(Buffer::new(data));
1561        let c = Completion::new_write(|_| {});
1562        let c = file.pwrite(0, buf, c).unwrap();
1563        wait_completion_ok(io.as_ref(), &c);
1564
1565        let mut handles = vec![];
1566
1567        // Multiple threads try to read past EOF
1568        for _ in 0..3 {
1569            let file = file.clone();
1570            let io = io.clone();
1571            handles.push(thread::spawn(move || {
1572                let read_buf = Arc::new(Buffer::new_temporary(100));
1573                let bytes_read = Arc::new(AtomicUsize::new(999));
1574                let bytes_read_clone = bytes_read.clone();
1575                let c = Completion::new_read(read_buf, move |res| {
1576                    if let Ok((_, n)) = res {
1577                        bytes_read_clone.store(n as usize, Ordering::SeqCst);
1578                    }
1579                    None
1580                });
1581                let c = file.pread(200, c).unwrap(); // Past EOF
1582                                                     // Reading past EOF succeeds with 0 bytes read
1583                wait_completion_ok(io.as_ref(), &c);
1584                assert_eq!(bytes_read.load(Ordering::SeqCst), 0);
1585            }));
1586        }
1587
1588        for h in handles {
1589            h.join().unwrap();
1590        }
1591    }
1592
1593    shuttle_io_test!(read_past_eof, test_read_past_eof_impl);
1594
1595    /// Test empty write operations.
1596    fn test_empty_write_impl<F: IOFactory>(factory: F) {
1597        let io = factory.create();
1598        let path = factory.temp_dir().join("test.db");
1599        let file = io
1600            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1601            .unwrap();
1602
1603        let mut handles = vec![];
1604
1605        for _ in 0..3 {
1606            let file = file.clone();
1607            let io = io.clone();
1608            handles.push(thread::spawn(move || {
1609                // Empty buffer write
1610                let buf = Arc::new(Buffer::new(vec![]));
1611                let c = Completion::new_write(|_| {});
1612                let c = file.pwrite(0, buf, c).unwrap();
1613                wait_completion_ok(io.as_ref(), &c);
1614            }));
1615        }
1616
1617        for h in handles {
1618            h.join().unwrap();
1619        }
1620
1621        assert_eq!(file.size().unwrap(), 0);
1622    }
1623
1624    shuttle_io_test!(empty_write, test_empty_write_impl);
1625
1626    /// Test sync operations under concurrency.
1627    fn test_concurrent_sync_impl<F: IOFactory>(factory: F) {
1628        let io = factory.create();
1629        let path = factory.temp_dir().join("test.db");
1630        let file = io
1631            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1632            .unwrap();
1633
1634        // Write some data first
1635        let data = vec![0xFF; 1000];
1636        let buf = Arc::new(Buffer::new(data));
1637        let c = Completion::new_write(|_| {});
1638        let c = file.pwrite(0, buf, c).unwrap();
1639        wait_completion_ok(io.as_ref(), &c);
1640
1641        let mut handles = vec![];
1642
1643        // Multiple sync calls concurrently
1644        for _ in 0..3 {
1645            let file = file.clone();
1646            let io = io.clone();
1647            handles.push(thread::spawn(move || {
1648                let c = Completion::new_sync(|_| {});
1649                let c = file.sync(c, FileSyncType::Fsync).unwrap();
1650                wait_completion_ok(io.as_ref(), &c);
1651            }));
1652        }
1653
1654        for h in handles {
1655            h.join().unwrap();
1656        }
1657    }
1658
1659    shuttle_io_test!(concurrent_sync, test_concurrent_sync_impl);
1660
1661    /// Test concurrent open of the same file returns same file instance.
1662    fn test_concurrent_open_same_file_impl<F: IOFactory>(factory: F) {
1663        let io = factory.create();
1664        let path = factory.temp_dir().join("shared.db");
1665
1666        // Create file first
1667        let _ = io
1668            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1669            .unwrap();
1670
1671        let mut handles = vec![];
1672
1673        for _ in 0..3 {
1674            let io = io.clone();
1675            let path = path.clone();
1676            handles.push(thread::spawn(move || {
1677                let file = io
1678                    .open_file(path.to_str().unwrap(), OpenFlags::None, false)
1679                    .unwrap();
1680                thread::yield_now();
1681                // Write a byte to prove we got a valid file
1682                let buf = Arc::new(Buffer::new(vec![0xAA]));
1683                let c = Completion::new_write(|_| {});
1684                let c = file.pwrite(0, buf, c).unwrap();
1685                wait_completion_ok(io.as_ref(), &c);
1686            }));
1687        }
1688
1689        for h in handles {
1690            h.join().unwrap();
1691        }
1692    }
1693
1694    shuttle_io_test!(
1695        concurrent_open_same_file,
1696        test_concurrent_open_same_file_impl
1697    );
1698
1699    /// Test file removal while concurrent access.
1700    fn test_file_remove_concurrent_impl<F: IOFactory>(factory: F) {
1701        let io = factory.create();
1702        let base = factory.temp_dir();
1703
1704        // Create multiple files
1705        for i in 0..3 {
1706            let path = base.join(format!("remove_{}.db", i));
1707            let file = io
1708                .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1709                .unwrap();
1710            let buf = Arc::new(Buffer::new(vec![0xFF; 100]));
1711            let c = Completion::new_write(|_| {});
1712            let c = file.pwrite(0, buf, c).unwrap();
1713            wait_completion_ok(io.as_ref(), &c);
1714        }
1715
1716        let mut handles = vec![];
1717
1718        // Remove files concurrently
1719        for i in 0..3 {
1720            let io = io.clone();
1721            let base = base.clone();
1722            handles.push(thread::spawn(move || {
1723                let path = base.join(format!("remove_{}.db", i));
1724                io.remove_file(path.to_str().unwrap()).unwrap();
1725            }));
1726        }
1727
1728        for h in handles {
1729            h.join().unwrap();
1730        }
1731    }
1732
1733    shuttle_io_test!(file_remove_concurrent, test_file_remove_concurrent_impl);
1734
1735    /// Test write spanning multiple internal pages.
1736    fn test_large_write_concurrent_impl<F: IOFactory>(factory: F) {
1737        let io = factory.create();
1738        let path = factory.temp_dir().join("test.db");
1739        let file = io
1740            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1741            .unwrap();
1742
1743        let mut handles = vec![];
1744
1745        // Multiple threads write large buffers that span multiple pages
1746        for i in 0..2 {
1747            let file = file.clone();
1748            let io = io.clone();
1749            handles.push(thread::spawn(move || {
1750                // Write 10000 bytes (spans multiple 4096-byte pages)
1751                let data = vec![(i + 1) as u8; 10000];
1752                let buf = Arc::new(Buffer::new(data));
1753                let c = Completion::new_write(|_| {});
1754                let c = file.pwrite((i * 10000) as u64, buf, c).unwrap();
1755                wait_completion_ok(io.as_ref(), &c);
1756            }));
1757        }
1758
1759        for h in handles {
1760            h.join().unwrap();
1761        }
1762
1763        assert_eq!(file.size().unwrap(), 20000);
1764
1765        // Read back and verify each segment contains correct data
1766        for i in 0..2 {
1767            let read_buf = Arc::new(Buffer::new_temporary(10000));
1768            let pos = (i * 10000) as u64;
1769            let c = Completion::new_read(read_buf.clone(), |_| None);
1770            let c = file.pread(pos, c).unwrap();
1771            wait_completion_ok(io.as_ref(), &c);
1772
1773            let expected_byte = (i + 1) as u8;
1774            assert!(
1775                read_buf.as_slice().iter().all(|&b| b == expected_byte),
1776                "all bytes at offset {} should be {:#x}",
1777                pos,
1778                expected_byte
1779            );
1780        }
1781    }
1782
1783    shuttle_io_test!(large_write_concurrent, test_large_write_concurrent_impl);
1784
1785    /// Test has_hole and punch_hole under concurrency.
1786    /// Note: Only runs on MemoryIO as hole operations are not supported on all backends.
1787    fn test_hole_operations_concurrent_impl<F: IOFactory>(factory: F) {
1788        let io = factory.create();
1789        let path = factory.temp_dir().join("test.db");
1790        let file = io
1791            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1792            .unwrap();
1793
1794        // Write data spanning multiple pages (at least 3 pages = 12288 bytes)
1795        let data = vec![0xFF; 16384];
1796        let buf = Arc::new(Buffer::new(data));
1797        let c = Completion::new_write(|_| {});
1798        let c = file.pwrite(0, buf, c).unwrap();
1799        wait_completion_ok(io.as_ref(), &c);
1800
1801        let mut handles = vec![];
1802
1803        // Thread 1: punch holes
1804        {
1805            let file = file.clone();
1806            handles.push(thread::spawn(move || {
1807                // Punch hole in middle page (page-aligned)
1808                file.punch_hole(4096, 4096).unwrap();
1809            }));
1810        }
1811
1812        // Thread 2: check for holes
1813        {
1814            let file = file.clone();
1815            handles.push(thread::spawn(move || {
1816                // Check various regions
1817                let has_hole = file.has_hole(0, 4096).unwrap();
1818                assert!(!has_hole);
1819                let _ = file.has_hole(4096, 4096).unwrap();
1820                let has_hole = file.has_hole(8192, 4096).unwrap();
1821                assert!(!has_hole);
1822            }));
1823        }
1824
1825        for h in handles {
1826            h.join().unwrap();
1827        }
1828    }
1829
1830    // hole_operations only runs on MemoryIO since not all backends support holes
1831    #[test]
1832    fn shuttle_hole_operations_concurrent_memory() {
1833        shuttle::check_random(
1834            || test_hole_operations_concurrent_impl(MemoryIOFactory::new()),
1835            1000,
1836        );
1837    }
1838
1839    /// Test that partial reads work correctly at EOF boundary.
1840    fn test_partial_read_at_eof_impl<F: IOFactory>(factory: F) {
1841        let io = factory.create();
1842        let path = factory.temp_dir().join("test.db");
1843        let file = io
1844            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1845            .unwrap();
1846
1847        // Write exactly 150 bytes
1848        let data = vec![0xAB; 150];
1849        let buf = Arc::new(Buffer::new(data));
1850        let c = Completion::new_write(|_| {});
1851        let c = file.pwrite(0, buf, c).unwrap();
1852        wait_completion_ok(io.as_ref(), &c);
1853
1854        let mut handles = vec![];
1855
1856        // Multiple threads try to read 100 bytes starting at offset 100
1857        // Should only get 50 bytes back
1858        for _ in 0..3 {
1859            let file = file.clone();
1860            let io = io.clone();
1861            handles.push(thread::spawn(move || {
1862                let read_buf = Arc::new(Buffer::new_temporary(100));
1863                let bytes_read = Arc::new(AtomicUsize::new(999));
1864                let bytes_read_clone = bytes_read.clone();
1865                let c = Completion::new_read(read_buf.clone(), move |res| {
1866                    if let Ok((_, n)) = res {
1867                        bytes_read_clone.store(n as usize, Ordering::SeqCst);
1868                    }
1869                    None
1870                });
1871                let c = file.pread(100, c).unwrap();
1872                wait_completion_ok(io.as_ref(), &c);
1873
1874                // Should read exactly 50 bytes (150 - 100)
1875                assert_eq!(bytes_read.load(Ordering::SeqCst), 50);
1876                // Verify the bytes read are correct
1877                assert_eq!(&read_buf.as_slice()[..50], &[0xAB; 50]);
1878            }));
1879        }
1880
1881        for h in handles {
1882            h.join().unwrap();
1883        }
1884    }
1885
1886    shuttle_io_test!(partial_read_at_eof, test_partial_read_at_eof_impl);
1887
1888    /// Test empty pwritev.
1889    fn test_empty_pwritev_impl<F: IOFactory>(factory: F) {
1890        let io = factory.create();
1891        let path = factory.temp_dir().join("test.db");
1892        let file = io
1893            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1894            .unwrap();
1895
1896        let mut handles = vec![];
1897
1898        for _ in 0..3 {
1899            let file = file.clone();
1900            let io = io.clone();
1901            handles.push(thread::spawn(move || {
1902                let bufs: Vec<Arc<Buffer>> = vec![];
1903                let c = Completion::new_write(|_| {});
1904                let c = file.pwritev(0, bufs, c).unwrap();
1905                wait_completion_ok(io.as_ref(), &c);
1906            }));
1907        }
1908
1909        for h in handles {
1910            h.join().unwrap();
1911        }
1912    }
1913
1914    shuttle_io_test!(empty_pwritev, test_empty_pwritev_impl);
1915
1916    /// Test error case: opening non-existent file without Create flag.
1917    fn test_open_nonexistent_without_create_impl<F: IOFactory>(factory: F) {
1918        let io = factory.create();
1919        let base = factory.temp_dir();
1920
1921        let mut handles = vec![];
1922
1923        for i in 0..3 {
1924            let io = io.clone();
1925            let base = base.clone();
1926            handles.push(thread::spawn(move || {
1927                let path = base.join(format!("nonexistent_{}.db", i));
1928                let result = io.open_file(path.to_str().unwrap(), OpenFlags::None, false);
1929                assert!(result.is_err());
1930            }));
1931        }
1932
1933        for h in handles {
1934            h.join().unwrap();
1935        }
1936    }
1937
1938    shuttle_io_test!(
1939        open_nonexistent_without_create,
1940        test_open_nonexistent_without_create_impl
1941    );
1942
1943    /// Test concurrent writes to overlapping regions.
1944    /// This tests that the final state is consistent (one of the writes wins).
1945    fn test_concurrent_overlapping_writes_impl<F: IOFactory>(factory: F) {
1946        let io = factory.create();
1947        let path = factory.temp_dir().join("test.db");
1948        let file = io
1949            .open_file(path.to_str().unwrap(), OpenFlags::Create, false)
1950            .unwrap();
1951
1952        let write_complete = Arc::new(AtomicUsize::new(0));
1953        let mut handles = vec![];
1954
1955        // Multiple threads write to the same offset
1956        for i in 0..3 {
1957            let file = file.clone();
1958            let io = io.clone();
1959            let write_complete = write_complete.clone();
1960            handles.push(thread::spawn(move || {
1961                let data = vec![(i + 1) as u8; 100];
1962                let buf = Arc::new(Buffer::new(data));
1963                let write_complete_clone = write_complete.clone();
1964                let c = Completion::new_write(move |_| {
1965                    write_complete_clone.fetch_add(1, Ordering::SeqCst);
1966                });
1967                let c = file.pwrite(0, buf, c).unwrap();
1968                wait_completion_ok(io.as_ref(), &c);
1969            }));
1970        }
1971
1972        for h in handles {
1973            h.join().unwrap();
1974        }
1975
1976        // All writes should have completed
1977        assert_eq!(write_complete.load(Ordering::SeqCst), 3);
1978
1979        // Read back and verify we got one of the written values
1980        let read_buf = Arc::new(Buffer::new_temporary(100));
1981        let c = Completion::new_read(read_buf.clone(), |_| None);
1982        let c = file.pread(0, c).unwrap();
1983        wait_completion_ok(io.as_ref(), &c);
1984
1985        let first_byte = read_buf.as_slice()[0];
1986        assert!(first_byte == 1 || first_byte == 2 || first_byte == 3);
1987
1988        // All 100 bytes should be the same value
1989        assert!(read_buf.as_slice().iter().all(|&b| b == first_byte));
1990    }
1991
1992    shuttle_io_test!(
1993        concurrent_overlapping_writes,
1994        test_concurrent_overlapping_writes_impl
1995    );
1996}