Skip to main content

sqlite_embedded/
pager_cache.rs

1//! Process-wide SQLite page cache, installed through `SQLITE_CONFIG_PCACHE2`.
2//!
3//! SQLite's default page cache gives every connection its own `cache_size` budget, so a process
4//! with many databases holds up to the sum of all of them while a busy database is capped at its
5//! own share. This cache replaces SQLite's page cache for every connection in the process: one
6//! byte budget covers the pages of every purgeable cache, so a busy database takes pages from
7//! idle ones and the process total stays bounded.
8//!
9//! The cache follows SQLite's page cache contract:
10//! - A page returned by `xFetch` is pinned and stays at its address until `xUnpin`.
11//! - An unpinned page of a purgeable cache may be discarded at any time, because its pager keeps
12//!   no reference to it (SQLite never unpins a dirty page). Discarding one from another
13//!   connection's cache is therefore safe.
14//! - A non-purgeable cache (an in-memory or temporary database, which `temp_store = MEMORY`
15//!   makes of every temporary b-tree) keeps every page until `xUnpin` with `discard`,
16//!   `xTruncate` or `xDestroy`, because those pages are the database itself. Its pages are
17//!   counted apart and never charged to the budget, so one connection's large sort or `VACUUM`
18//!   cannot push every other connection's pages out.
19//! - `xFetch` with `createFlag` 1 may refuse when the budget is full of pinned pages, which tells
20//!   the pager to spill; with `createFlag` 2 it allocates past the budget rather than fail, and
21//!   the overshoot is returned when pages are unpinned. Purgeable bytes can therefore exceed the
22//!   budget by the pages SQLite still pins after a refusal it cannot spill around: pages held by
23//!   open cursors, and dirty pages when no spill is possible.
24//! - A new page, including its extra area, starts zeroed, because SQLite's pager reads its header
25//!   from the extra area to tell a new page from a recycled one.
26//!
27//! Every page access takes only the lock of the shard that owns its connection's cache, so
28//! connections on different shards never wait for each other. Each shard keeps its own least
29//! recently used order; eviction walks the shards from a rotating cursor, which approximates one
30//! process-wide order without a global lock. Pages are allocated and freed outside every lock.
31//! Every call on one connection's cache runs under that connection's database mutex, or the
32//! shared b-tree's mutex in shared-cache mode, so a miss can drop the shard lock while it
33//! allocates without racing another call on the same cache. A miss still checks the key again
34//! when it re-locks, so a page it finds there is returned instead of overwritten.
35
36use std::{
37	alloc::{self, Layout},
38	cell::UnsafeCell,
39	collections::HashMap,
40	ffi::{c_int, c_uint, c_void},
41	ptr::{self, NonNull},
42	sync::{
43		Arc, OnceLock,
44		atomic::{AtomicPtr, AtomicU64, AtomicUsize, Ordering},
45	},
46};
47
48use libsqlite3_sys::{
49	SQLITE_CONFIG_PCACHE2, SQLITE_OK, sqlite3_config, sqlite3_pcache, sqlite3_pcache_methods2,
50	sqlite3_pcache_page,
51};
52use lru::LruCache;
53use parking_lot::Mutex;
54
55use crate::worker::SqlitePoolMetrics;
56
57/// Shards of the cache. Connections are spread over them by creation order.
58const SHARDS: usize = 16;
59
60/// The installed cache. SQLite's page cache configuration is process-global C state that holds
61/// a pointer to this object for the life of the process, so it cannot be owned by a pool or a
62/// database; this static is the one owner that matches that lifetime. It is published before
63/// `sqlite3_config` runs, so `xCreate` always finds it once SQLite can call it.
64static PROCESS_PAGER_CACHE: AtomicPtr<PagerCache> = AtomicPtr::new(ptr::null_mut());
65
66/// Whether `install` already ran in this process, and whether it succeeded.
67static INSTALLED: OnceLock<bool> = OnceLock::new();
68
69/// Installs the process-wide page cache with `capacity_bytes`, once per process.
70///
71/// It must run before anything else in the process uses SQLite, because SQLite accepts a page
72/// cache only before it initializes and `sqlite3_config` itself is not thread safe. Production
73/// calls it from the first `SqlitePool::new`, which runs at startup before any database opens.
74/// If SQLite is already initialized (another library opened a database first), the install fails
75/// and connections keep SQLite's per-connection cache. Returns whether the process-wide cache is
76/// active. Later calls only attach `metrics` if none are attached yet.
77pub fn install(capacity_bytes: u64, metrics: Option<Arc<dyn SqlitePoolMetrics>>) -> bool {
78	let installed = *INSTALLED.get_or_init(|| {
79		if capacity_bytes == 0 {
80			return false;
81		}
82		// SQLite keeps `pArg` and calls into this cache until the process exits, and offers no
83		// point where the pointer is released, so the cache lives as long as the process. This
84		// runs once per process, so the leak is bounded to one cache.
85		let cache: &'static PagerCache = Box::leak(Box::new(PagerCache::new(capacity_bytes)));
86		PROCESS_PAGER_CACHE.store(cache as *const PagerCache as *mut PagerCache, Ordering::Release);
87		let methods = sqlite3_pcache_methods2 {
88			iVersion: 1,
89			pArg: cache as *const PagerCache as *mut c_void,
90			xInit: Some(x_init),
91			xShutdown: Some(x_shutdown),
92			xCreate: Some(x_create),
93			xCachesize: Some(x_cachesize),
94			xPagecount: Some(x_pagecount),
95			xFetch: Some(x_fetch),
96			xUnpin: Some(x_unpin),
97			xRekey: Some(x_rekey),
98			xTruncate: Some(x_truncate),
99			xDestroy: Some(x_destroy),
100			xShrink: Some(x_shrink),
101		};
102		// SAFETY: SQLite copies the methods struct; `pArg` points at a cache leaked for the
103		// life of the process, once, so every later callback finds it alive.
104		let rc = unsafe { sqlite3_config(SQLITE_CONFIG_PCACHE2, &methods as *const _) };
105		if rc != SQLITE_OK {
106			PROCESS_PAGER_CACHE.store(ptr::null_mut(), Ordering::Release);
107			tracing::warn!(
108				rc,
109				"sqlite was initialized before the process-wide page cache could be installed; connections keep per-connection page caches"
110			);
111			return false;
112		}
113		true
114	});
115	if let (true, Some(metrics)) = (installed, metrics) {
116		attach_metrics(metrics);
117	}
118	installed
119}
120
121/// The installed cache, if any, for tests and metrics.
122pub fn installed() -> Option<&'static PagerCache> {
123	let cache = PROCESS_PAGER_CACHE.load(Ordering::Acquire);
124	// SAFETY: a non-null pointer is a cache leaked for the life of the process.
125	unsafe { cache.as_ref() }
126}
127
128/// Reports the installed cache's gauges through `metrics`, unless another pool already attached
129/// its metrics. The cache is process-wide, so whichever pool carries metrics reports it,
130/// regardless of which pool installed it.
131pub fn attach_metrics(metrics: Arc<dyn SqlitePoolMetrics>) {
132	if let Some(cache) = installed()
133		&& cache.metrics.set(metrics).is_ok()
134	{
135		cache.report_capacity();
136		cache.report();
137	}
138}
139
140/// Every page of every connection's cache.
141pub struct PagerCache {
142	shards: Box<[Mutex<Shard>]>,
143	capacity_bytes: u64,
144	/// Bytes of purgeable pages, the only bytes the budget bounds.
145	purgeable_bytes: AtomicU64,
146	/// Bytes of non-purgeable pages, which are the in-memory databases themselves.
147	unbudgeted_bytes: AtomicU64,
148	pages: AtomicU64,
149	/// `createFlag` 1 fetches refused so SQLite would spill, for tests and diagnostics.
150	refusals: AtomicU64,
151	/// Pages discarded to make room, for tests and diagnostics.
152	evictions: AtomicU64,
153	next_cache: AtomicUsize,
154	evict_cursor: AtomicUsize,
155	metrics: OnceLock<Arc<dyn SqlitePoolMetrics>>,
156}
157
158/// One shard's unpinned pages of purgeable caches, least recently unpinned first. A key is the
159/// owning cache's handle address and the page number.
160struct Shard {
161	lru: LruCache<(usize, u32), ()>,
162}
163
164/// One connection's cache, handed to SQLite as its `sqlite3_pcache` pointer.
165struct CacheHandle {
166	cache: &'static PagerCache,
167	shard: usize,
168	/// Reached only while holding `cache.shards[shard]`.
169	state: UnsafeCell<CacheState>,
170}
171
172struct CacheState {
173	/// Allocation of one page: the slot header, then the page and extra areas.
174	layout: Layout,
175	/// Offset of the page area within an allocation.
176	buf_offset: usize,
177	/// Bytes one page charges: its allocation size.
178	slot_bytes: u64,
179	page_size: usize,
180	purgeable: bool,
181	pages: HashMap<u32, NonNull<Slot>>,
182	/// An upper bound on every key ever held, so `xTruncate` above it skips the scan.
183	max_key: u32,
184}
185
186/// One page's header, at the start of its allocation. `page` is first so a
187/// `*mut sqlite3_pcache_page` handed to SQLite is the slot's address.
188#[repr(C)]
189struct Slot {
190	page: sqlite3_pcache_page,
191	key: u32,
192	pinned: bool,
193}
194
195/// Pages freed by a call, released after its lock is dropped.
196type Freed = Vec<(NonNull<Slot>, Layout)>;
197
198impl PagerCache {
199	fn new(capacity_bytes: u64) -> Self {
200		Self {
201			shards: (0..SHARDS)
202				.map(|_| {
203					Mutex::new(Shard {
204						lru: LruCache::unbounded(),
205					})
206				})
207				.collect(),
208			capacity_bytes,
209			purgeable_bytes: AtomicU64::new(0),
210			unbudgeted_bytes: AtomicU64::new(0),
211			pages: AtomicU64::new(0),
212			refusals: AtomicU64::new(0),
213			evictions: AtomicU64::new(0),
214			next_cache: AtomicUsize::new(0),
215			evict_cursor: AtomicUsize::new(0),
216			metrics: OnceLock::new(),
217		}
218	}
219
220	/// Bytes of purgeable pages across every connection, the bytes the budget bounds.
221	pub fn bytes(&self) -> u64 {
222		self.purgeable_bytes.load(Ordering::Relaxed)
223	}
224
225	/// Bytes of non-purgeable pages (in-memory and temporary databases), outside the budget.
226	pub fn unbudgeted_bytes(&self) -> u64 {
227		self.unbudgeted_bytes.load(Ordering::Relaxed)
228	}
229
230	/// Pages the cache holds across every connection.
231	pub fn pages(&self) -> u64 {
232		self.pages.load(Ordering::Relaxed)
233	}
234
235	/// The configured budget.
236	pub fn capacity_bytes(&self) -> u64 {
237		self.capacity_bytes
238	}
239
240	/// `createFlag` 1 fetches refused because the budget was full of pinned pages.
241	pub fn refusals(&self) -> u64 {
242		self.refusals.load(Ordering::Relaxed)
243	}
244
245	/// Pages discarded to make room for others.
246	pub fn evictions(&self) -> u64 {
247		self.evictions.load(Ordering::Relaxed)
248	}
249
250	fn report_capacity(&self) {
251		if let Some(metrics) = self.metrics.get() {
252			metrics.set_pager_cache_capacity_bytes(self.capacity_bytes);
253		}
254	}
255
256	/// Publishes the counts. Called only when a page is allocated or freed, never on a hit.
257	fn report(&self) {
258		if let Some(metrics) = self.metrics.get() {
259			metrics.set_pager_cache_bytes(self.bytes());
260			metrics.set_pager_cache_unbudgeted_bytes(self.unbudgeted_bytes());
261			metrics.set_pager_cache_pages(self.pages());
262		}
263	}
264
265	/// Frees pages collected under a lock, after the lock is dropped.
266	fn release(&self, freed: Freed) {
267		if freed.is_empty() {
268			return;
269		}
270		for (slot, layout) in freed {
271			// SAFETY: every slot was allocated with `layout` in `allocate`, removed from its
272			// cache's map and LRU under the lock, and is freed exactly once here.
273			unsafe { alloc::dealloc(slot.as_ptr() as *mut u8, layout) };
274		}
275		self.report();
276	}
277
278	/// Charges or credits one page of `state` to the counters.
279	fn account(&self, state: &CacheState, added: bool) {
280		let counter = if state.purgeable {
281			&self.purgeable_bytes
282		} else {
283			&self.unbudgeted_bytes
284		};
285		if added {
286			counter.fetch_add(state.slot_bytes, Ordering::Relaxed);
287			self.pages.fetch_add(1, Ordering::Relaxed);
288		} else {
289			counter.fetch_sub(state.slot_bytes, Ordering::Relaxed);
290			self.pages.fetch_sub(1, Ordering::Relaxed);
291		}
292	}
293
294	/// Reserves `bytes` of the budget for a purgeable page, evicting unpinned pages until it
295	/// fits. Returns false when nothing is left to evict and the budget is still full.
296	fn reserve(&self, bytes: u64) -> bool {
297		loop {
298			let current = self.purgeable_bytes.load(Ordering::Relaxed);
299			if current.saturating_add(bytes) <= self.capacity_bytes {
300				if self
301					.purgeable_bytes
302					.compare_exchange_weak(
303						current,
304						current + bytes,
305						Ordering::Relaxed,
306						Ordering::Relaxed,
307					)
308					.is_ok()
309				{
310					return true;
311				}
312				continue;
313			}
314			if !self.evict_one() {
315				return false;
316			}
317		}
318	}
319
320	/// Evicts unpinned pages until the budget is no longer exceeded, or none are left.
321	fn shed_overshoot(&self) {
322		while self.bytes() > self.capacity_bytes {
323			if !self.evict_one() {
324				return;
325			}
326		}
327	}
328
329	/// Discards one least recently unpinned page, trying each shard once from a rotating
330	/// cursor. Takes one shard lock at a time, never two. Returns false when no shard has an
331	/// unpinned page.
332	fn evict_one(&self) -> bool {
333		for _ in 0..SHARDS {
334			let shard_index = self.evict_cursor.fetch_add(1, Ordering::Relaxed) % SHARDS;
335			let mut shard = self.shards[shard_index].lock();
336			let Some(((handle_addr, key), ())) = shard.lru.pop_lru() else {
337				continue;
338			};
339			// SAFETY: a key in the LRU names a live handle of this shard; `x_destroy` removes
340			// every key of a handle under this lock before the handle is freed, and its state
341			// is reached under this lock.
342			let state = unsafe { &mut *(*(handle_addr as *const CacheHandle)).state.get() };
343			let freed = state.pages.remove(&key).map(|slot| (slot, state.layout));
344			if freed.is_some() {
345				self.account(state, false);
346				self.evictions.fetch_add(1, Ordering::Relaxed);
347			}
348			drop(shard);
349			self.release(freed.into_iter().collect());
350			return true;
351		}
352		false
353	}
354}
355
356/// Removes `key` from `state` and the shard's LRU, returning the page to free.
357fn take_page(
358	cache: &PagerCache,
359	shard: &mut Shard,
360	handle_addr: usize,
361	state: &mut CacheState,
362	key: u32,
363	freed: &mut Freed,
364) {
365	if let Some(slot) = state.pages.remove(&key) {
366		shard.lru.pop(&(handle_addr, key));
367		cache.account(state, false);
368		freed.push((slot, state.layout));
369	}
370}
371
372/// Allocates a pinned, zeroed page for `key` of a cache with `state`'s layout, without holding
373/// any lock. Returns `None` when memory is exhausted, which SQLite reports as `SQLITE_NOMEM`.
374fn allocate(
375	layout: Layout,
376	buf_offset: usize,
377	page_size: usize,
378	key: u32,
379) -> Option<NonNull<Slot>> {
380	// SAFETY: `layout` has a non-zero size, because it includes the slot header.
381	let base = NonNull::new(unsafe { alloc::alloc_zeroed(layout) })?;
382	let slot = base.cast::<Slot>();
383	// SAFETY: the allocation holds the header at its start and `page_size` plus the extra area at
384	// `buf_offset`, all derived from `base`, so the page pointers stay valid until it is freed.
385	unsafe {
386		let buf = base.as_ptr().add(buf_offset);
387		ptr::write(
388			slot.as_ptr(),
389			Slot {
390				page: sqlite3_pcache_page {
391					pBuf: buf as *mut c_void,
392					pExtra: buf.add(page_size) as *mut c_void,
393				},
394				key,
395				pinned: true,
396			},
397		);
398	}
399	Some(slot)
400}
401
402/// The handle SQLite passes back for a cache.
403///
404/// # Safety
405/// `cache` must be a pointer returned by `x_create` and not yet passed to `x_destroy`.
406unsafe fn handle<'a>(cache: *mut sqlite3_pcache) -> &'a CacheHandle {
407	unsafe { &*(cache as *const CacheHandle) }
408}
409
410unsafe extern "C" fn x_init(_arg: *mut c_void) -> c_int {
411	SQLITE_OK
412}
413
414unsafe extern "C" fn x_shutdown(_arg: *mut c_void) {}
415
416unsafe extern "C" fn x_create(
417	page_size: c_int,
418	extra_size: c_int,
419	purgeable: c_int,
420) -> *mut sqlite3_pcache {
421	// `xCreate` receives no `pArg`, so it reaches the installed cache through the process owner.
422	match installed() {
423		Some(cache) => create(cache, page_size, extra_size, purgeable),
424		None => ptr::null_mut(),
425	}
426}
427
428/// Creates one connection's cache inside `cache`. Returns null if the page layout is invalid,
429/// which SQLite reports as `SQLITE_NOMEM`.
430fn create(
431	cache: &'static PagerCache,
432	page_size: c_int,
433	extra_size: c_int,
434	purgeable: c_int,
435) -> *mut sqlite3_pcache {
436	let page_size = page_size.max(0) as usize;
437	let extra_size = extra_size.max(0) as usize;
438	let Ok(area) = Layout::from_size_align(page_size + extra_size, 8) else {
439		return ptr::null_mut();
440	};
441	let Ok((layout, buf_offset)) = Layout::new::<Slot>().extend(area) else {
442		return ptr::null_mut();
443	};
444	let layout = layout.pad_to_align();
445	let shard = cache.next_cache.fetch_add(1, Ordering::Relaxed) % SHARDS;
446	let handle = Box::new(CacheHandle {
447		cache,
448		shard,
449		state: UnsafeCell::new(CacheState {
450			layout,
451			buf_offset,
452			slot_bytes: layout.size() as u64,
453			page_size,
454			purgeable: purgeable != 0,
455			pages: HashMap::new(),
456			max_key: 0,
457		}),
458	});
459	Box::into_raw(handle) as *mut sqlite3_pcache
460}
461
462/// With this cache, `cache_size` reaches it only through this callback, and the process budget
463/// replaces each connection's `cache_size`, so the suggestion is ignored. SQLite decides when to
464/// spill from `createFlag` 1 refusals and `cache_spill`, not from `cache_size`.
465unsafe extern "C" fn x_cachesize(_cache: *mut sqlite3_pcache, _pages: c_int) {}
466
467unsafe extern "C" fn x_pagecount(cache: *mut sqlite3_pcache) -> c_int {
468	// SAFETY: SQLite passes a live cache.
469	let handle = unsafe { handle(cache) };
470	let _shard = handle.cache.shards[handle.shard].lock();
471	// SAFETY: the state is reached under its shard's lock.
472	let state = unsafe { &*handle.state.get() };
473	state.pages.len().min(c_int::MAX as usize) as c_int
474}
475
476unsafe extern "C" fn x_fetch(
477	cache: *mut sqlite3_pcache,
478	key: c_uint,
479	create_flag: c_int,
480) -> *mut sqlite3_pcache_page {
481	// SAFETY: SQLite passes a live cache.
482	let handle = unsafe { handle(cache) };
483	let pager = handle.cache;
484	let handle_addr = handle as *const CacheHandle as usize;
485
486	// The hit path: one shard lock, one map lookup, and an LRU removal for an unpinned page.
487	let (layout, buf_offset, page_size, slot_bytes, purgeable) = {
488		let mut shard = pager.shards[handle.shard].lock();
489		// SAFETY: the state is reached under its shard's lock.
490		let state = unsafe { &mut *handle.state.get() };
491		if let Some(slot) = state.pages.get(&key).copied() {
492			// SAFETY: a slot in the map is alive and reached only under the lock.
493			let slot_ref = unsafe { &mut *slot.as_ptr() };
494			if !slot_ref.pinned {
495				slot_ref.pinned = true;
496				shard.lru.pop(&(handle_addr, key));
497			}
498			return slot.as_ptr() as *mut sqlite3_pcache_page;
499		}
500		if create_flag == 0 {
501			return ptr::null_mut();
502		}
503		(
504			state.layout,
505			state.buf_offset,
506			state.page_size,
507			state.slot_bytes,
508			state.purgeable,
509		)
510	};
511
512	// A miss: reserve the budget and allocate with no lock held. Calls on one cache run under its
513	// connection's database mutex (or the shared b-tree's mutex), so no other call on this cache
514	// races this one; eviction from other threads only removes unpinned pages.
515	if purgeable && !pager.reserve(slot_bytes) {
516		// A purgeable cache refuses a page past the budget when SQLite can spill instead.
517		if create_flag == 1 {
518			pager.refusals.fetch_add(1, Ordering::Relaxed);
519			return ptr::null_mut();
520		}
521		pager
522			.purgeable_bytes
523			.fetch_add(slot_bytes, Ordering::Relaxed);
524	}
525	let Some(slot) = allocate(layout, buf_offset, page_size, key) else {
526		if purgeable {
527			pager
528				.purgeable_bytes
529				.fetch_sub(slot_bytes, Ordering::Relaxed);
530		}
531		return ptr::null_mut();
532	};
533
534	{
535		let mut shard = pager.shards[handle.shard].lock();
536		// SAFETY: the state is reached under its shard's lock.
537		let state = unsafe { &mut *handle.state.get() };
538		if let Some(existing) = state.pages.get(&key).copied() {
539			// The key appeared while the lock was dropped. Keep the existing page, pinned and
540			// out of the LRU, and give back the page just allocated and its charge.
541			// SAFETY: a slot in the map is alive and reached only under the lock.
542			let existing_ref = unsafe { &mut *existing.as_ptr() };
543			if !existing_ref.pinned {
544				existing_ref.pinned = true;
545				shard.lru.pop(&(handle_addr, key));
546			}
547			drop(shard);
548			if purgeable {
549				pager
550					.purgeable_bytes
551					.fetch_sub(slot_bytes, Ordering::Relaxed);
552			}
553			// SAFETY: allocated with `layout` above and never published.
554			unsafe { alloc::dealloc(slot.as_ptr() as *mut u8, layout) };
555			return existing.as_ptr() as *mut sqlite3_pcache_page;
556		}
557		state.pages.insert(key, slot);
558		state.max_key = state.max_key.max(key);
559	}
560	if !purgeable {
561		pager
562			.unbudgeted_bytes
563			.fetch_add(slot_bytes, Ordering::Relaxed);
564	}
565	pager.pages.fetch_add(1, Ordering::Relaxed);
566	pager.report();
567	slot.as_ptr() as *mut sqlite3_pcache_page
568}
569
570unsafe extern "C" fn x_unpin(
571	cache: *mut sqlite3_pcache,
572	page: *mut sqlite3_pcache_page,
573	discard: c_int,
574) {
575	// SAFETY: SQLite passes a live cache and a page this cache returned.
576	let handle = unsafe { handle(cache) };
577	let pager = handle.cache;
578	let handle_addr = handle as *const CacheHandle as usize;
579	let mut freed = Freed::new();
580	{
581		let mut shard = pager.shards[handle.shard].lock();
582		// SAFETY: the state is reached under its shard's lock.
583		let state = unsafe { &mut *handle.state.get() };
584		// SAFETY: `page` is the first field of a live `Slot`.
585		let slot = unsafe { &mut *(page as *mut Slot) };
586		if discard != 0 {
587			take_page(pager, &mut shard, handle_addr, state, slot.key, &mut freed);
588		} else {
589			slot.pinned = false;
590			if state.purgeable {
591				shard.lru.put((handle_addr, slot.key), ());
592			}
593		}
594	}
595	pager.release(freed);
596	// Pages allocated past the budget while pinned are returned here.
597	pager.shed_overshoot();
598}
599
600unsafe extern "C" fn x_rekey(
601	cache: *mut sqlite3_pcache,
602	_page: *mut sqlite3_pcache_page,
603	old_key: c_uint,
604	new_key: c_uint,
605) {
606	if old_key == new_key {
607		return;
608	}
609	// SAFETY: SQLite passes a live cache and a page this cache returned.
610	let handle = unsafe { handle(cache) };
611	let pager = handle.cache;
612	let handle_addr = handle as *const CacheHandle as usize;
613	let mut freed = Freed::new();
614	{
615		let mut shard = pager.shards[handle.shard].lock();
616		// SAFETY: the state is reached under its shard's lock.
617		let state = unsafe { &mut *handle.state.get() };
618		// Any page already at `new_key` is unpinned by contract and is discarded.
619		take_page(pager, &mut shard, handle_addr, state, new_key, &mut freed);
620		if let Some(slot) = state.pages.remove(&old_key) {
621			// SAFETY: the slot is alive and reached only under the lock.
622			let pinned = unsafe {
623				let slot = &mut *slot.as_ptr();
624				slot.key = new_key;
625				slot.pinned
626			};
627			state.pages.insert(new_key, slot);
628			state.max_key = state.max_key.max(new_key);
629			if !pinned && shard.lru.pop(&(handle_addr, old_key)).is_some() {
630				shard.lru.put((handle_addr, new_key), ());
631			}
632		}
633	}
634	pager.release(freed);
635}
636
637unsafe extern "C" fn x_truncate(cache: *mut sqlite3_pcache, limit: c_uint) {
638	// SAFETY: SQLite passes a live cache.
639	let handle = unsafe { handle(cache) };
640	let pager = handle.cache;
641	let handle_addr = handle as *const CacheHandle as usize;
642	let mut freed = Freed::new();
643	{
644		let mut shard = pager.shards[handle.shard].lock();
645		// SAFETY: the state is reached under its shard's lock.
646		let state = unsafe { &mut *handle.state.get() };
647		if limit > state.max_key {
648			return;
649		}
650		let keys: Vec<u32> = state
651			.pages
652			.keys()
653			.copied()
654			.filter(|key| *key >= limit)
655			.collect();
656		// Pinned pages at or above the limit are implicitly unpinned and discarded too.
657		for key in keys {
658			take_page(pager, &mut shard, handle_addr, state, key, &mut freed);
659		}
660		state.max_key = limit.saturating_sub(1);
661	}
662	pager.release(freed);
663}
664
665unsafe extern "C" fn x_destroy(cache: *mut sqlite3_pcache) {
666	// SAFETY: SQLite passes a live cache, here for the last time. The handle is freed only after
667	// every LRU key naming it is removed under its shard's lock, so no eviction can reach it.
668	let handle_ptr = cache as *mut CacheHandle;
669	let handle = unsafe { &*handle_ptr };
670	let pager = handle.cache;
671	let handle_addr = handle_ptr as usize;
672	let mut freed = Freed::new();
673	{
674		let mut shard = pager.shards[handle.shard].lock();
675		// SAFETY: the state is reached under its shard's lock.
676		let state = unsafe { &mut *handle.state.get() };
677		let keys: Vec<u32> = state.pages.keys().copied().collect();
678		for key in keys {
679			take_page(pager, &mut shard, handle_addr, state, key, &mut freed);
680		}
681	}
682	pager.release(freed);
683	// SAFETY: created by `Box::into_raw` in `create`, and no lock or LRU key refers to it now.
684	drop(unsafe { Box::from_raw(handle_ptr) });
685}
686
687unsafe extern "C" fn x_shrink(cache: *mut sqlite3_pcache) {
688	// SAFETY: SQLite passes a live cache.
689	let handle = unsafe { handle(cache) };
690	let pager = handle.cache;
691	let handle_addr = handle as *const CacheHandle as usize;
692	let mut freed = Freed::new();
693	{
694		let mut shard = pager.shards[handle.shard].lock();
695		// SAFETY: the state is reached under its shard's lock.
696		let state = unsafe { &mut *handle.state.get() };
697		if !state.purgeable {
698			return;
699		}
700		let unpinned: Vec<u32> = state
701			.pages
702			.iter()
703			// SAFETY: slots in the map are alive.
704			.filter(|(_, slot)| !unsafe { slot.as_ref() }.pinned)
705			.map(|(key, _)| *key)
706			.collect();
707		for key in unpinned {
708			take_page(pager, &mut shard, handle_addr, state, key, &mut freed);
709		}
710	}
711	pager.release(freed);
712}
713
714