sqlite_pagecache/
lib.rs

1//! https://www.sqlite.org/c3ref/pcache_methods2.html
2
3// This is a workaround for rusqlite/rusqlite missing support for
4// `sqlite3_pcache_methods2` and/or not publishing on crates.io
5mod ffi_bindgen;
6
7use ffi_bindgen as ffi;
8use std::os::raw::{c_int, c_uint, c_void};
9use std::ptr;
10
11pub enum InitState {
12    Ok,
13    UseDefaultCachePage,
14}
15
16type BoxError = Box<dyn std::error::Error>;
17
18pub struct PageWithMetadata {
19    pub page: Vec<u8>,
20    pub metadata: Vec<u8>,
21}
22
23pub enum CreateFlag {
24    /// Do not allocate a new page. Return NULL.
25    NoAllocation,
26
27    /// Allocate a new page if it easy and convenient to do so. Otherwise return NULL.
28    AllocateIfConvenient,
29
30    /// Make every effort to allocate a new page. Only return NULL if allocating a new page is
31    /// effectively impossible.
32    Allocate,
33}
34
35pub enum DiscardStrategy {
36    /// The page must be evicted from the cache
37    MustBeEvicted,
38
39    /// Page may be discarded or retained at the discretion of page cache implementation
40    CanDecide,
41}
42
43pub trait PageCacheBuiler<T: PageCache> {
44    /// SQLite invokes the `create` method to construct a new cache instance.
45    /// SQLite will typically create one cache instance for each open database file, though this is
46    /// not guaranteed.  The first parameter, `page_size`, is the size in bytes of the pages that
47    /// must be allocated by the cache. `page_size` will always a power of two.  The second
48    /// parameter `extra_size` is a number of bytes of extra storage associated with each page
49    /// cache entry.  The `extra_size` parameter will a number less than 250. SQLite will use the
50    /// extra extra bytes on each page to store metadata about the underlying database page on
51    /// disk. The value passed depends on the SQLite version, the target platform, and how SQLite
52    /// was compiled.
53    /// The third argument to `create`, `bpurgeable`, is true if the cache being
54    /// created will be used to cache database pages of a file stored on disk, or false if it is
55    /// used for an in-memory database. The cache implementation does not have to do anything
56    /// special based with the value of bPurgeable; it is purely advisory. On a cache where
57    /// bPurgeable is false, SQLite will never invoke [unpin] except to deliberately delete a page.
58    /// In other words, calls to [unpin] on a cache with bPurgeable set to false will always have
59    /// the "discard" flag set to true. Hence, a cache created with bPurgeable false will never
60    /// contain any unpinned pages.
61    fn create(page_size: usize, extra_size: usize, bpurgeable: bool) -> T;
62}
63pub trait PageCache {
64    /// The `cache_size` method may be called at any time by SQLite to set the suggested maximum
65    /// cache-size (number of pages stored by) the cache instance passed as the first argument.
66    /// This is the value configured using the SQLite "PRAGMA cache_size" command. It
67    /// is advisory only.
68    fn cache_size(&mut self, cache_size: usize);
69
70    /// The `page_count` method must return the number of pages currently stored in the cache, both
71    /// pinned and unpinned.
72    fn page_count(&mut self) -> usize;
73
74    /// The `fetch` method locates a page in the cache or None (see [CreateFlag] for detail on cache
75    /// miss).
76    /// The page to be fetched is determined by the `key`. The minimum key value is 1. After it has
77    /// been retrieved using `fetch`, the page is considered to be "pinned".
78    ///
79    /// SQLite will normally invoke `fetch` with a createFlag of NoAllocation or
80    /// AllocateIfConvenient. SQLite will only use a createFlag of Allocate after a prior call with
81    /// a createFlag of AllocateIfConvenient failed. In between the `fetch` calls, SQLite may
82    /// attempt to unpin one or more cache pages by spilling the content of pinned pages to disk
83    /// and synching the operating system disk cache.
84    fn fetch(&mut self, key: usize, create_flag: CreateFlag) -> Option<&mut PageWithMetadata>;
85
86    /// `unpin` is called by SQLite with a pointer to a currently pinned page.
87    /// The page cache implementation may choose to evict unpinned pages at any time.
88    fn unpin(&mut self, key: usize, discard: DiscardStrategy);
89
90    /// The `rekey` method is used to change the key value associated with the page passed as the
91    /// second argument. If the cache previously contains an entry associated with `new_key`, it must
92    /// be discarded. Any prior cache entry associated with `new_key` is guaranteed not to be pinned.
93    fn rekey(&mut self, old_key: usize, new_key: usize);
94
95    /// When SQLite calls the `truncate` method, the cache must discard all existing cache entries
96    /// with page numbers (keys) greater than or equal to the value of the `limit` parameter passed
97    /// to `truncate`. If any of these pages are pinned, they are implicitly unpinned, meaning
98    /// that they can be safely discarded.
99    fn truncate(&mut self, limit: usize);
100
101    /// The `destroy` method is used to delete a cache allocated by `create`. All resources
102    /// associated with the specified cache should be freed.
103    fn destroy(&mut self);
104
105    /// SQLite invokes the `shrink` method when it wants the page cache to free up as much of heap
106    /// memory as possible. The page cache implementation is not obligated to free any memory, but
107    /// well-behaved implementations should do their best.
108    fn shrink(&mut self);
109}
110
111struct Context<T: PageCache> {
112    pcache: T,
113}
114
115pub fn build<B: PageCacheBuiler<T>, T: PageCache>() -> *mut ffi::sqlite3_pcache_methods2 {
116    Box::into_raw(Box::new(ffi::sqlite3_pcache_methods2 {
117        iVersion: 1,
118        pArg: ptr::null_mut(),
119        xInit: Some(pcache::init),
120        xShutdown: Some(pcache::shutdown),
121        xCreate: Some(pcache::create::<B, T>),
122        xCachesize: Some(pcache::cache_size::<T>),
123        xPagecount: Some(pcache::page_count::<T>),
124        xFetch: Some(pcache::fetch::<T>),
125        xUnpin: Some(pcache::unpin::<T>),
126        xRekey: Some(pcache::rekey::<T>),
127        xTruncate: Some(pcache::truncate::<T>),
128        xDestroy: Some(pcache::destroy::<T>),
129        xShrink: Some(pcache::shrink::<T>),
130    }))
131}
132
133pub fn register(pcache: *mut ffi::sqlite3_pcache_methods2) -> Result<(), BoxError> {
134    let ret = unsafe { ffi::sqlite3_config(ffi::SQLITE_CONFIG_PCACHE2, pcache) };
135    if ret != ffi::SQLITE_OK {
136        Err(format!("sqlite3_config returned code: {}", ret).into())
137    } else {
138        Ok(())
139    }
140}
141
142mod pcache {
143    use super::*;
144
145    fn null_ptr_error() -> std::io::Error {
146        std::io::Error::new(std::io::ErrorKind::Other, "received null pointer")
147    }
148
149    fn get_ctx<'a, T: PageCache>(ptr: *mut ffi::sqlite3_pcache) -> &'a mut Context<T> {
150        unsafe {
151            (ptr as *mut Context<T>)
152                .as_mut()
153                .ok_or_else(null_ptr_error)
154                .unwrap()
155        }
156    }
157
158    pub(super) extern "C" fn init(_arg1: *mut c_void) -> c_int {
159        ffi::SQLITE_OK
160    }
161    pub(super) extern "C" fn shutdown(_arg1: *mut c_void) {}
162
163    pub(super) extern "C" fn create<Builder: PageCacheBuiler<T>, T: PageCache>(
164        page_size: c_int,
165        extra_size: c_int,
166        bpurgeable: c_int,
167    ) -> *mut ffi::sqlite3_pcache {
168        let bpurgeable = if bpurgeable == 1 { true } else { false };
169        let pcache = Builder::create(page_size as usize, extra_size as usize, bpurgeable);
170
171        Box::into_raw(Box::new(pcache)) as *mut ffi::sqlite3_pcache
172    }
173
174    pub(super) extern "C" fn cache_size<T: PageCache>(
175        arg1: *mut ffi::sqlite3_pcache,
176        n_cache_size: c_int,
177    ) {
178        let ctx = get_ctx::<T>(arg1);
179        ctx.pcache.cache_size(n_cache_size as usize);
180    }
181
182    pub(super) extern "C" fn page_count<T: PageCache>(arg1: *mut ffi::sqlite3_pcache) -> c_int {
183        let ctx = get_ctx::<T>(arg1);
184        ctx.pcache.page_count() as c_int
185    }
186
187    pub(super) extern "C" fn fetch<T: PageCache>(
188        arg1: *mut ffi::sqlite3_pcache,
189        key: c_uint,
190        create_flag: c_int,
191    ) -> *mut ffi::sqlite3_pcache_page {
192        let ctx = get_ctx::<T>(arg1);
193        let create_flag = match create_flag {
194            0 => CreateFlag::NoAllocation,
195            1 => CreateFlag::AllocateIfConvenient,
196            2 => CreateFlag::Allocate,
197            v => panic!("unknown create_flag: {}", v),
198        };
199        match ctx.pcache.fetch(key as usize, create_flag) {
200            None => ptr::null_mut(),
201            Some(buffers) => {
202                let res = ffi::sqlite3_pcache_page {
203                    pBuf: buffers.page.as_mut_ptr() as *mut ::std::os::raw::c_void,
204                    pExtra: buffers.metadata.as_mut_ptr() as *mut ::std::os::raw::c_void,
205                };
206                Box::into_raw(Box::new(res))
207            }
208        }
209    }
210
211    pub(super) extern "C" fn unpin<T: PageCache>(
212        arg1: *mut ffi::sqlite3_pcache,
213        arg2: *mut ffi::sqlite3_pcache_page,
214        discard: c_int,
215    ) {
216        todo!();
217        let ctx = get_ctx::<T>(arg1);
218        let discard = match discard {
219            0 => DiscardStrategy::CanDecide,
220            _ => DiscardStrategy::MustBeEvicted,
221        };
222        // FIXME: keep a cache key cache? Identification seems to be based on
223        // pointers.
224        let key = 999;
225
226        ctx.pcache.unpin(key, discard);
227    }
228
229    pub(super) extern "C" fn rekey<T: PageCache>(
230        arg1: *mut ffi::sqlite3_pcache,
231        arg2: *mut ffi::sqlite3_pcache_page,
232        old_key: c_uint,
233        new_key: c_uint,
234    ) {
235        let ctx = get_ctx::<T>(arg1);
236        ctx.pcache.rekey(old_key as usize, new_key as usize);
237    }
238
239    pub(super) extern "C" fn truncate<T: PageCache>(
240        arg1: *mut ffi::sqlite3_pcache,
241        i_limit: c_uint,
242    ) {
243        let ctx = get_ctx::<T>(arg1);
244        ctx.pcache.truncate(i_limit as usize);
245    }
246
247    pub(super) extern "C" fn destroy<T: PageCache>(arg1: *mut ffi::sqlite3_pcache) {
248        let ctx = get_ctx::<T>(arg1);
249        ctx.pcache.destroy();
250    }
251
252    pub(super) extern "C" fn shrink<T: PageCache>(arg1: *mut ffi::sqlite3_pcache) {
253        let ctx = get_ctx::<T>(arg1);
254        ctx.pcache.shrink();
255    }
256}