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}