graft_sqlite/
vfs.rs

1use std::{borrow::Cow, collections::HashMap, fmt::Debug, sync::Arc};
2
3use culprit::{Culprit, ResultExt};
4use graft::{GraftErr, LogicalErr, rt::runtime::Runtime};
5use parking_lot::Mutex;
6use sqlite_plugin::{
7    flags::{AccessFlags, CreateMode, LockLevel, OpenKind, OpenMode, OpenOpts},
8    vars::{
9        self, SQLITE_BUSY, SQLITE_BUSY_SNAPSHOT, SQLITE_CANTOPEN, SQLITE_INTERNAL, SQLITE_IOERR,
10        SQLITE_NOTFOUND,
11    },
12    vfs::{Pragma, PragmaErr, SqliteErr, Vfs, VfsResult},
13};
14use thiserror::Error;
15
16use crate::{
17    file::{FileHandle, VfsFile, mem_file::MemFile, vol_file::VolFile},
18    pragma::GraftPragma,
19};
20
21#[derive(Debug, Error)]
22pub enum ErrCtx {
23    #[error("Graft error: {0}")]
24    Graft(#[from] GraftErr),
25
26    #[error("Unknown Pragma")]
27    UnknownPragma,
28
29    #[error("Pragma error: {0}")]
30    PragmaErr(Cow<'static, str>),
31
32    #[error("Tag not found")]
33    TagNotFound,
34
35    #[error("Transaction is busy")]
36    Busy,
37
38    #[error("The transaction snapshot is no longer current")]
39    BusySnapshot,
40
41    #[error("Invalid lock transition")]
42    InvalidLockTransition,
43
44    #[error("Invalid volume state")]
45    InvalidVolumeState,
46
47    #[error(transparent)]
48    IoErr(#[from] std::io::Error),
49
50    #[error(transparent)]
51    FmtErr(#[from] std::fmt::Error),
52}
53
54impl ErrCtx {
55    #[inline]
56    fn wrap<T>(cb: impl FnOnce() -> culprit::Result<T, ErrCtx>) -> VfsResult<T> {
57        match cb() {
58            Ok(t) => Ok(t),
59            Err(err) => Err(err.ctx().sqlite_err()),
60        }
61    }
62
63    fn sqlite_err(&self) -> SqliteErr {
64        match self {
65            ErrCtx::UnknownPragma => SQLITE_NOTFOUND,
66            ErrCtx::TagNotFound => SQLITE_CANTOPEN,
67            ErrCtx::Busy => SQLITE_BUSY,
68            ErrCtx::BusySnapshot => SQLITE_BUSY_SNAPSHOT,
69            ErrCtx::Graft(err) => Self::map_graft_err(err),
70            _ => SQLITE_INTERNAL,
71        }
72    }
73
74    fn map_graft_err(err: &GraftErr) -> SqliteErr {
75        match err {
76            GraftErr::Storage(_) => SQLITE_IOERR,
77            GraftErr::Remote(_) => SQLITE_IOERR,
78            GraftErr::Logical(err) => match err {
79                LogicalErr::VolumeNotFound(_) => SQLITE_IOERR,
80                LogicalErr::VolumeConcurrentWrite(_) => SQLITE_BUSY_SNAPSHOT,
81                LogicalErr::VolumeNeedsRecovery(_)
82                | LogicalErr::VolumeDiverged(_)
83                | LogicalErr::VolumeRemoteMismatch { .. } => SQLITE_INTERNAL,
84            },
85        }
86    }
87}
88
89impl<T> From<ErrCtx> for culprit::Result<T, ErrCtx> {
90    fn from(err: ErrCtx) -> culprit::Result<T, ErrCtx> {
91        Err(Culprit::new(err))
92    }
93}
94
95pub struct GraftVfs {
96    runtime: Runtime,
97    // VolFile locks keyed by tag
98    locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
99}
100
101impl GraftVfs {
102    pub fn new(runtime: Runtime) -> Self {
103        Self { runtime, locks: Default::default() }
104    }
105}
106
107impl Vfs for GraftVfs {
108    type Handle = FileHandle;
109
110    fn device_characteristics(&self) -> i32 {
111        // writes up to a single page are atomic
112        vars::SQLITE_IOCAP_ATOMIC512 |
113        vars::SQLITE_IOCAP_ATOMIC1K |
114        vars::SQLITE_IOCAP_ATOMIC2K |
115        vars::SQLITE_IOCAP_ATOMIC4K |
116        // after reboot following a crash or power loss, the only bytes in a file that were written
117        // at the application level might have changed and that adjacent bytes, even bytes within
118        // the same sector are guaranteed to be unchanged
119        vars::SQLITE_IOCAP_POWERSAFE_OVERWRITE |
120        // when data is appended to a file, the data is appended first then the size of the file is
121        // extended, never the other way around
122        vars::SQLITE_IOCAP_SAFE_APPEND |
123        // information is written to disk in the same order as calls to xWrite()
124        vars::SQLITE_IOCAP_SEQUENTIAL
125    }
126
127    fn access(&self, path: &str, flags: AccessFlags) -> VfsResult<bool> {
128        tracing::trace!("access: path={path:?}; flags={flags:?}");
129        ErrCtx::wrap(move || self.runtime.tag_exists(path).or_into_ctx())
130    }
131
132    fn open(&self, path: Option<&str>, opts: OpenOpts) -> VfsResult<Self::Handle> {
133        tracing::trace!("open: path={path:?}, opts={opts:?}");
134        ErrCtx::wrap(move || {
135            // we only open a Volume for main database files
136            if opts.kind() == OpenKind::MainDb
137                && let Some(tag) = path
138            {
139                let can_create = matches!(
140                    opts.mode(),
141                    OpenMode::ReadWrite {
142                        create: CreateMode::Create | CreateMode::MustCreate
143                    }
144                );
145
146                let vid = if can_create {
147                    // create the volume if needed
148                    if let Some(vid) = self.runtime.tag_get(tag).or_into_ctx()? {
149                        vid
150                    } else {
151                        let volume = self.runtime.volume_open(None, None, None).or_into_ctx()?;
152                        self.runtime
153                            .tag_replace(tag, volume.vid.clone())
154                            .or_into_ctx()?;
155                        volume.vid
156                    }
157                } else {
158                    // just get the existing volume
159                    self.runtime
160                        .tag_get(tag)
161                        .or_into_ctx()?
162                        .ok_or(ErrCtx::TagNotFound)?
163                };
164
165                // get or create a reserved lock for this Volume
166                let reserved_lock = self.locks.lock().entry(tag.to_owned()).or_default().clone();
167
168                return Ok(VolFile::new(
169                    self.runtime.clone(),
170                    tag.to_owned(),
171                    vid,
172                    opts,
173                    reserved_lock,
174                )
175                .into());
176            }
177
178            // all other files use in-memory storage
179            Ok(MemFile::default().into())
180        })
181    }
182
183    fn delete(&self, path: &str) -> VfsResult<()> {
184        // nothing to do, SQLite only calls xDelete on secondary
185        // files, which in this VFS are in-memory and delete on close
186        tracing::trace!("delete: path={path:?}");
187        Ok(())
188    }
189
190    fn close(&self, handle: Self::Handle) -> VfsResult<()> {
191        tracing::trace!("close: file={handle:?}");
192        ErrCtx::wrap(move || {
193            match handle {
194                FileHandle::MemFile(_) => Ok(()),
195                FileHandle::VolFile(vol_file) => {
196                    if vol_file.opts().delete_on_close() {
197                        // TODO: delete volume on close if requested
198                        // TODO: do we want to actually delete volumes? or mark them for deletion?
199                    }
200
201                    // retrieve a reference to the reserved lock for the volume
202                    let mut locks = self.locks.lock();
203                    let reserved_lock = locks
204                        .get(&vol_file.tag)
205                        .expect("reserved lock missing from lock manager");
206
207                    // clean up the lock if this was the last reference
208                    // SAFETY: we are holding a lock on the lock manager,
209                    // preventing any concurrent opens from incrementing the
210                    // reference count
211                    if Arc::strong_count(reserved_lock) == 1 {
212                        locks.remove(&vol_file.tag);
213                    }
214
215                    Ok(())
216                }
217            }
218        })
219    }
220
221    fn pragma(
222        &self,
223        handle: &mut Self::Handle,
224        pragma: Pragma<'_>,
225    ) -> Result<Option<String>, PragmaErr> {
226        tracing::trace!("pragma: file={handle:?}, pragma={pragma:?}");
227        if let FileHandle::VolFile(file) = handle {
228            match GraftPragma::try_from(&pragma)?.eval(&self.runtime, file) {
229                Ok(val) => Ok(val),
230                Err(err) => Err(PragmaErr::Fail(
231                    err.ctx().sqlite_err(),
232                    Some(format!("{err}")),
233                )),
234            }
235        } else {
236            Err(PragmaErr::NotFound)
237        }
238    }
239
240    fn lock(&self, handle: &mut Self::Handle, level: LockLevel) -> VfsResult<()> {
241        tracing::trace!("lock: file={handle:?}, level={level:?}");
242        ErrCtx::wrap(move || handle.lock(level))
243    }
244
245    fn unlock(&self, handle: &mut Self::Handle, level: LockLevel) -> VfsResult<()> {
246        tracing::trace!("unlock: file={handle:?}, level={level:?}");
247        ErrCtx::wrap(move || handle.unlock(level))
248    }
249
250    fn file_size(&self, handle: &mut Self::Handle) -> VfsResult<usize> {
251        tracing::trace!("file_size: handle={handle:?}");
252        ErrCtx::wrap(move || handle.file_size())
253    }
254
255    fn truncate(&self, handle: &mut Self::Handle, size: usize) -> VfsResult<()> {
256        tracing::trace!("truncate: handle={handle:?}, size={size}");
257        ErrCtx::wrap(move || handle.truncate(size))
258    }
259
260    fn write(&self, handle: &mut Self::Handle, offset: usize, data: &[u8]) -> VfsResult<usize> {
261        tracing::trace!(
262            "write: handle={handle:?}, offset={offset}, len={}",
263            data.len()
264        );
265        ErrCtx::wrap(move || handle.write(offset, data))
266    }
267
268    fn read(&self, handle: &mut Self::Handle, offset: usize, data: &mut [u8]) -> VfsResult<usize> {
269        tracing::trace!(
270            "read: handle={handle:?}, offset={offset}, len={}",
271            data.len()
272        );
273        ErrCtx::wrap(move || handle.read(offset, data))
274    }
275}