Skip to main content

sqlite_embedded/vfs/
transport.rs

1//! Custom SQLite VFS backed by KV operations over the KV channel.
2//!
3//! This crate owns the KV-backed SQLite behavior used by `rivetkit-napi`.
4
5use std::collections::{BTreeMap, HashMap, HashSet};
6use std::ffi::{CStr, CString, c_char, c_int, c_void};
7use std::ptr;
8use std::slice;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
11use std::time::{Duration, Instant};
12
13use crate::page_buffer::{PageBudget, PageBuffer, PageTrace};
14use anyhow::Result;
15use async_trait::async_trait;
16use futures_util::stream::{self, StreamExt};
17use libsqlite3_sys::*;
18use parking_lot::{Mutex, RwLock};
19use rivet_envoy_protocol as protocol;
20use sqlite_types::{PageBytes, is_head_fence_mismatch};
21use tokio::runtime::Handle;
22use tracing::Instrument;
23
24use crate::optimization_flags::{
25	SqliteOptimizationFlags, SqliteVfsPageCacheMode, sqlite_optimization_flags,
26};
27use crate::sqlite_page::{PageClass, classify};
28
29const DEFAULT_MAX_PAGES_PER_STAGE: usize = 4_000;
30const DEFAULT_PAGE_SIZE: usize = 4096;
31const STAGE_PIPELINE_BYTE_BUDGET: usize = 8 * 1024 * 1024;
32const STAGE_MAX_SEGMENT_BYTES: usize = sqlite_types::SHARD_SIZE as usize
33	* sqlite_types::COMMIT_SEGMENT_MAX_SHARDS as usize
34	* DEFAULT_PAGE_SIZE;
35const STAGE_PIPELINE_DEPTH: usize = STAGE_PIPELINE_BYTE_BUDGET / STAGE_MAX_SEGMENT_BYTES;
36const NATIVE_DATABASE_DROP_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
37const MAX_PATHNAME: c_int = 64;
38const TEMP_AUX_PATH_PREFIX: &str = "__sqlite_temp__";
39const SQLITE_HEADER_MAGIC: &[u8; 16] = b"SQLite format 3\0";
40const EMPTY_DB_PAGE_HEADER_PREFIX: [u8; 108] = [
41	83, 81, 76, 105, 116, 101, 32, 102, 111, 114, 109, 97, 116, 32, 51, 0, 16, 0, 1, 1, 0, 64, 32,
42	32, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
43	0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
44	0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 46, 138, 17, 13, 0, 0, 0, 0, 16, 0, 0,
45];
46
47static NEXT_TEMP_AUX_ID: AtomicU64 = AtomicU64::new(1);
48
49unsafe extern "C" {
50	fn sqlite3_close_v2(db: *mut sqlite3) -> c_int;
51}
52
53fn empty_db_page() -> Vec<u8> {
54	let mut page = vec![0u8; DEFAULT_PAGE_SIZE];
55	page[..EMPTY_DB_PAGE_HEADER_PREFIX.len()].copy_from_slice(&EMPTY_DB_PAGE_HEADER_PREFIX);
56	page
57}
58
59fn sqlite_header_page_size(page: &[u8]) -> Option<usize> {
60	if page.len() < 100 || &page[..SQLITE_HEADER_MAGIC.len()] != SQLITE_HEADER_MAGIC {
61		return None;
62	}
63
64	let raw = u16::from_be_bytes([page[16], page[17]]);
65	let page_size = if raw == 1 { 65_536 } else { usize::from(raw) };
66
67	if (512..=65_536).contains(&page_size) && page_size.is_power_of_two() {
68		Some(page_size)
69	} else {
70		None
71	}
72}
73
74fn sqlite_header_db_size_pages(page: &[u8]) -> Option<u32> {
75	if page.len() < 100 || &page[..SQLITE_HEADER_MAGIC.len()] != SQLITE_HEADER_MAGIC {
76		return None;
77	}
78
79	let db_size_pages = u32::from_be_bytes([page[28], page[29], page[30], page[31]]);
80	Some(db_size_pages.max(1))
81}
82
83fn panic_message(payload: &Box<dyn std::any::Any + Send>) -> String {
84	if let Some(message) = payload.downcast_ref::<&str>() {
85		message.to_string()
86	} else if let Some(message) = payload.downcast_ref::<String>() {
87		message.clone()
88	} else {
89		"unknown panic".to_string()
90	}
91}
92
93macro_rules! vfs_catch_unwind {
94	($err_val:expr, $body:expr) => {
95		match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body)) {
96			Ok(result) => result,
97			Err(panic) => {
98				tracing::error!(msg = panic_message(&panic), "sqlite callback panicked");
99				$err_val
100			}
101		}
102	};
103}
104
105#[async_trait]
106pub trait SqliteTransport: Send + Sync {
107	async fn get_pages(
108		&self,
109		request: protocol::SqliteGetPagesRequest,
110	) -> Result<protocol::SqliteGetPagesResponse>;
111
112	async fn commit(
113		&self,
114		request: protocol::SqliteCommitRequest,
115	) -> Result<protocol::SqliteCommitResponse>;
116
117	/// Opens a staged commit, for a commit too large to send in one message.
118	///
119	/// Required rather than defaulted. A default made staging look optional, and the transport that
120	/// runs `depot vacuum` in-process silently took it: every commit above the single-shot threshold
121	/// failed there, which is exactly the commit shape a vacuum produces. A transport that genuinely
122	/// cannot stage has to say so in its own body.
123	async fn commit_stage_begin(
124		&self,
125		request: protocol::SqliteCommitStageBeginRequest,
126	) -> Result<protocol::SqliteCommitStageBeginResponse>;
127
128	/// Stages one shard-aligned segment of an open staged commit.
129	async fn commit_stage_segment(
130		&self,
131		request: protocol::SqliteCommitStageSegmentRequest,
132	) -> Result<protocol::SqliteCommitStageSegmentResponse>;
133
134	/// Publishes an open staged commit, making every staged segment visible at once.
135	async fn commit_finalize(
136		&self,
137		request: protocol::SqliteCommitFinalizeRequest,
138	) -> Result<protocol::SqliteCommitFinalizeResponse>;
139}
140
141pub type SqliteTransportHandle = Arc<dyn SqliteTransport>;
142fn sqlite_now_ms() -> Result<i64> {
143	use std::time::{SystemTime, UNIX_EPOCH};
144
145	Ok(SystemTime::now()
146		.duration_since(UNIX_EPOCH)?
147		.as_millis()
148		.try_into()?)
149}
150
151#[derive(Debug, Clone)]
152pub struct VfsConfig {
153	/// The process-wide budget this database's page buffers share. A database opened on a
154	/// worker pool uses the pool's budget.
155	pub page_budget: Arc<PageBudget>,
156	pub page_cache_mode: SqliteVfsPageCacheMode,
157	pub staging_cache_ttl_ms: u64,
158	pub max_pages_per_stage: usize,
159	/// Idle time after which the worker releases this database's caches. Zero disables.
160	pub idle_release_ms: u64,
161	/// The most pages one transaction may change. Values above Depot's own commit cap,
162	/// `sqlite_types::MAX_COMMIT_DIRTY_PAGES`, are clamped to it.
163	pub max_commit_dirty_pages: usize,
164	
165}
166
167impl Default for VfsConfig {
168	fn default() -> Self {
169		Self::from_optimization_flags(*sqlite_optimization_flags())
170	}
171}
172
173impl VfsConfig {
174	/// The most pages one transaction may change, never above Depot's commit cap.
175	pub fn commit_page_cap(&self) -> usize {
176		self.max_commit_dirty_pages
177			.min(sqlite_types::MAX_COMMIT_DIRTY_PAGES)
178	}
179
180	pub fn from_optimization_flags(flags: SqliteOptimizationFlags) -> Self {
181		let caches_pages = flags.vfs_page_cache_mode.caches_any_pages();
182		Self {
183			page_budget: PageBudget::new(
184				if caches_pages {
185					flags.vfs_page_buffer_process_pages
186				} else {
187					0
188				},
189				if caches_pages {
190					flags.vfs_staging_cache_ttl_ms
191				} else {
192					0
193				},
194				None,
195			),
196			page_cache_mode: flags.vfs_page_cache_mode,
197			staging_cache_ttl_ms: if caches_pages {
198				flags.vfs_staging_cache_ttl_ms
199			} else {
200				0
201			},
202			max_pages_per_stage: DEFAULT_MAX_PAGES_PER_STAGE,
203			idle_release_ms: flags.idle_release_ms,
204			max_commit_dirty_pages: sqlite_types::MAX_COMMIT_DIRTY_PAGES,
205			
206		}
207	}
208}
209
210#[derive(Debug, Clone, Default)]
211pub(crate) struct InitialPages {
212	pub pages: Vec<(u32, PageBytes)>,
213	pub head_txid: Option<u64>,
214	pub requested_page_count: u32,
215}
216
217impl From<Vec<(u32, Vec<u8>)>> for InitialPages {
218	fn from(pages: Vec<(u32, Vec<u8>)>) -> Self {
219		let requested_page_count = pages.len().try_into().unwrap_or(u32::MAX);
220		Self {
221			pages: pages
222				.into_iter()
223				.map(|(pgno, bytes)| (pgno, bytes.into()))
224				.collect(),
225			head_txid: None,
226			requested_page_count,
227		}
228	}
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
232pub enum CommitPath {
233	Fast,
234	Slow,
235	/// Written as several staged segments plus a finalize, because the commit was too large to send
236	/// in one message.
237	Staged,
238}
239
240#[derive(Debug, Clone)]
241pub struct BufferedCommitRequest {
242	pub actor_id: String,
243	pub new_db_size_pages: u32,
244	pub dirty_pages: Vec<BufferedDirtyPage>,
245	pub expected_head_txid: Option<u64>,
246}
247
248#[derive(Debug, Clone)]
249pub struct BufferedDirtyPage {
250	pub pgno: u32,
251	pub bytes: PageBytes,
252}
253
254#[derive(Debug, Clone)]
255pub struct BufferedCommitOutcome {
256	pub path: CommitPath,
257	pub db_size_pages: u32,
258	pub head_txid: Option<u64>,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub enum CommitBufferError {
263	FenceMismatch(String),
264	Indeterminate(String),
265	/// Storage refused the commit before running it, or its request timed out before it was
266	/// sent. Nothing was committed, so the transaction rolls back and may run again.
267	Storage(SqliteStorageError),
268	Other(String),
269}
270
271/// A SQLite storage request that failed without changing anything, in a way the caller can act
272/// on. The VFS reports it to SQLite as an I/O error, and the statement's command returns it
273/// instead of SQLite's generic `disk I/O error`.
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub enum SqliteStorageError {
276	/// Storage refused the request before running it, as when the volume's writes are ahead of
277	/// its tablet's apply. The request may be sent again after `retry_after_ms`.
278	Overloaded {
279		retry_after_ms: u64,
280		message: String,
281	},
282	/// The request's deadline passed before storage answered a read, or before a write was
283	/// sent. A write whose request was sent has an unknown outcome instead.
284	Timeout { operation: String },
285}
286
287impl std::fmt::Display for SqliteStorageError {
288	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289		match self {
290			Self::Overloaded {
291				retry_after_ms,
292				message,
293			} => write!(
294				f,
295				"sqlite storage is overloaded, retry after {retry_after_ms} ms: {message}"
296			),
297			Self::Timeout { operation } => {
298				write!(
299					f,
300					"sqlite storage {operation} timed out before storage answered"
301				)
302			}
303		}
304	}
305}
306
307impl std::error::Error for SqliteStorageError {}
308
309/// A transport's report that a SQLite request's deadline passed before its response arrived,
310/// for a read or for a write it never sent. A transport reports a sent write that timed out as
311/// [`SqliteCommitIndeterminateError`] instead, since storage may still have committed it.
312#[derive(Debug)]
313pub struct SqliteStorageTimeoutError {
314	pub operation: String,
315}
316
317impl std::fmt::Display for SqliteStorageTimeoutError {
318	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319		write!(f, "sqlite {} request timed out", self.operation)
320	}
321}
322
323impl std::error::Error for SqliteStorageTimeoutError {}
324
325/// The group and code of a storage refusal that ran nothing and carries a retry hint.
326const DEPOT_OVERLOADED_GROUP: &str = "depot";
327const DEPOT_OVERLOADED_CODE: &str = "overloaded";
328
329/// The hint a `depot.overloaded` response carries in its message, or this when it names none.
330const DEFAULT_OVERLOADED_RETRY_AFTER_MS: u64 = 100;
331
332/// The typed failure a storage error response stands for, if it is a refusal with a hint.
333fn storage_refusal(error: &protocol::SqliteErrorResponse) -> Option<SqliteStorageError> {
334	if error.group.as_str() != DEPOT_OVERLOADED_GROUP
335		|| error.code.as_str() != DEPOT_OVERLOADED_CODE
336	{
337		return None;
338	}
339	let message = error.message.to_string();
340	Some(SqliteStorageError::Overloaded {
341		retry_after_ms: retry_after_hint(&message).unwrap_or(DEFAULT_OVERLOADED_RETRY_AFTER_MS),
342		message,
343	})
344}
345
346/// Reads the `retry_after_ms=<ms>` parameter Engine writes into a refusal's message.
347fn retry_after_hint(message: &str) -> Option<u64> {
348	let (_, rest) = message.split_once("retry_after_ms=")?;
349	let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
350	digits.parse().ok()
351}
352
353/// The typed failure a transport error stands for, if it is a timeout that changed nothing.
354fn storage_timeout(error: &anyhow::Error) -> Option<SqliteStorageError> {
355	error
356		.downcast_ref::<SqliteStorageTimeoutError>()
357		.map(|timeout| SqliteStorageError::Timeout {
358			operation: timeout.operation.clone(),
359		})
360}
361
362/// Marks a lost commit reply so the VFS can fail closed instead of using stale state.
363#[derive(Debug)]
364pub struct SqliteCommitIndeterminateError(pub String);
365
366impl std::fmt::Display for SqliteCommitIndeterminateError {
367	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368		write!(f, "sqlite {} result is indeterminate", self.0)
369	}
370}
371
372impl std::error::Error for SqliteCommitIndeterminateError {}
373
374/// A transaction tried to change more pages than one commit may carry.
375///
376/// The VFS refuses the write that would cross the cap, before any of the transaction reaches
377/// Depot, and SQLite rolls the transaction back. The connection stays usable.
378#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct SqliteTransactionTooLargeError {
380	pub max_pages: usize,
381	pub page_size: usize,
382}
383
384impl std::fmt::Display for SqliteTransactionTooLargeError {
385	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386		let max_kib = self.max_pages * self.page_size / 1024;
387		let max_size = if max_kib >= 1024 {
388			format!("{} MiB", max_kib / 1024)
389		} else {
390			format!("{max_kib} KiB")
391		};
392		write!(
393			f,
394			"sqlite transaction changes more than {} pages ({}), the most one commit can hold, \
395			 so the statement failed and its changes were rolled back; split the work into smaller \
396			 transactions. VACUUM rewrites the whole \
397			 database in one transaction, so it only runs while the database's in-use pages fit under \
398			 this limit",
399			self.max_pages, max_size,
400		)
401	}
402}
403
404impl std::error::Error for SqliteTransactionTooLargeError {}
405
406/// A file control opcode that reads this VFS's commit page cap into an `i64`. SQLite reserves
407/// opcodes up to 100 for itself.
408pub(crate) const FCNTL_COMMIT_PAGE_CAP: c_int = 0x5256_0001;
409
410#[derive(Debug, Clone, Copy, Default)]
411pub struct SqliteVfsMetricsSnapshot {
412	pub request_build_ns: u64,
413	pub serialize_ns: u64,
414	pub transport_ns: u64,
415	pub state_update_ns: u64,
416	pub total_ns: u64,
417	pub commit_count: u64,
418	pub page_cache_entries: u64,
419	pub page_cache_weighted_size: u64,
420	pub page_cache_capacity_pages: u64,
421	pub write_buffer_dirty_pages: u64,
422	pub db_size_pages: u64,
423}
424
425pub const MAX_PROFILED_GET_PAGES_REQUESTS: usize = 16;
426pub const MAX_PROFILED_TRANSACTION_STATEMENTS: usize = 32;
427
428/// Bounded details for one physical `get_pages` call made while a native SQLite
429/// command is executing.
430#[derive(Debug, Clone, Default)]
431pub struct SqliteGetPagesProfile {
432	pub ordinal: u64,
433	pub duration_ns: u64,
434	pub demand_requested: u64,
435	pub response_present: u64,
436	pub response_missing: u64,
437	pub overflow_expansion_extra: u64,
438	pub response_bytes: u64,
439	pub success: bool,
440}
441
442/// Fixed-size, connection-local counters collected by VFS callbacks for one
443/// native SQLite command. The worker installs and removes this context around
444/// each command, so VFS activity cannot be attributed to a neighboring query.
445#[derive(Debug, Clone, Default)]
446pub struct SqliteOperationProfile {
447	pub worker_wait_ns: u64,
448	pub sqlite_execution_ns: u64,
449	pub storage_ns: u64,
450	pub bind_count: u64,
451	pub bind_logical_bytes: u64,
452	pub result_rows: u64,
453	pub result_columns: u64,
454	pub result_logical_bytes: u64,
455	pub sqlite_requested_pages: u64,
456	pub cache_hit_pages: u64,
457	pub cache_miss_pages: u64,
458	pub depot_demand_requested_pages: u64,
459	pub response_present_pages: u64,
460	pub response_missing_pages: u64,
461	pub overflow_expansion_extra_pages: u64,
462	pub btree_pages: u64,
463	pub non_btree_pages: u64,
464	pub prefetch_consumed_pages: u64,
465	pub prefetch_unused_pages: u64,
466	pub dirty_pages: u64,
467	pub storage_response_bytes: u64,
468	pub dirty_bytes: u64,
469	pub get_pages_round_trips: u64,
470	pub commit_round_trips: u64,
471	pub get_pages_requests: [Option<SqliteGetPagesProfile>; MAX_PROFILED_GET_PAGES_REQUESTS],
472	pub omitted_get_pages_requests: u64,
473}
474
475#[derive(Debug, Clone)]
476pub struct SqliteOperationMetric {
477	pub operation_type: &'static str,
478	pub fingerprint: String,
479	pub fingerprint_source: &'static str,
480	pub transaction_mode: &'static str,
481	pub storage_transport: &'static str,
482	pub outcome: &'static str,
483	pub sql_bytes: u64,
484	pub total_ns: u64,
485	pub transaction_wait_ns: u64,
486	pub profile: SqliteOperationProfile,
487}
488
489#[derive(Debug, Clone)]
490pub struct SqliteTransactionMetric {
491	pub fingerprint: String,
492	pub fingerprint_source: &'static str,
493	pub shape_fingerprint: String,
494	pub statement_fingerprint_hashes: [Option<[u8; 16]>; MAX_PROFILED_TRANSACTION_STATEMENTS],
495	pub omitted_statement_fingerprints: u64,
496	pub storage_transport: &'static str,
497	pub outcome: &'static str,
498	pub total_ns: u64,
499	pub transaction_wait_ns: u64,
500	pub worker_wait_ns: u64,
501	pub storage_ns: u64,
502	pub local_work_ns: u64,
503	pub application_time_ns: u64,
504	pub commit_ns: u64,
505	pub get_pages_round_trips: u64,
506	pub statement_count: u64,
507	pub dirty_pages: u64,
508	pub dirty_bytes: u64,
509}
510
511impl SqliteOperationProfile {
512	fn push_get_pages(&mut self, request: SqliteGetPagesProfile, request_limit: usize) {
513		self.storage_ns = self.storage_ns.saturating_add(request.duration_ns);
514		self.get_pages_round_trips = self.get_pages_round_trips.saturating_add(1);
515		self.depot_demand_requested_pages = self
516			.depot_demand_requested_pages
517			.saturating_add(request.demand_requested);
518		self.response_present_pages = self
519			.response_present_pages
520			.saturating_add(request.response_present);
521		self.response_missing_pages = self
522			.response_missing_pages
523			.saturating_add(request.response_missing);
524		self.overflow_expansion_extra_pages = self
525			.overflow_expansion_extra_pages
526			.saturating_add(request.overflow_expansion_extra);
527		self.storage_response_bytes = self
528			.storage_response_bytes
529			.saturating_add(request.response_bytes);
530		let index = self.get_pages_round_trips.saturating_sub(1) as usize;
531		if index < request_limit
532			&& let Some(slot) = self.get_pages_requests.get_mut(index)
533		{
534			*slot = Some(request);
535		} else {
536			self.omitted_get_pages_requests = self.omitted_get_pages_requests.saturating_add(1);
537		}
538	}
539}
540
541pub(crate) struct SqliteOperationProfileGuard<'a> {
542	ctx: &'a VfsContext,
543	active: bool,
544}
545
546impl SqliteOperationProfileGuard<'_> {
547	pub(crate) fn finish(mut self) -> SqliteOperationProfile {
548		self.active = false;
549		self.ctx.finish_operation_profile()
550	}
551}
552
553impl Drop for SqliteOperationProfileGuard<'_> {
554	fn drop(&mut self) {
555		if self.active {
556			let _ = self.ctx.finish_operation_profile();
557		}
558	}
559}
560
561/// Cumulative count of network round trips the VFS has issued to the engine.
562///
563/// `get_pages` counts `SqliteGetPagesRequest` fetches and `commit` counts
564/// `SqliteCommitRequest` commits. Diffing two snapshots gives the round trips
565/// performed by the work that ran between them, such as a SQLite transaction.
566#[derive(Debug, Clone, Copy, Default)]
567pub struct SqliteRoundTripCounts {
568	pub get_pages: u64,
569	pub commit: u64,
570}
571
572impl SqliteRoundTripCounts {
573	pub fn total(&self) -> u64 {
574		self.get_pages.saturating_add(self.commit)
575	}
576
577	pub fn since(&self, earlier: SqliteRoundTripCounts) -> SqliteRoundTripCounts {
578		SqliteRoundTripCounts {
579			get_pages: self.get_pages.saturating_sub(earlier.get_pages),
580			commit: self.commit.saturating_sub(earlier.commit),
581		}
582	}
583}
584
585#[derive(Debug, Clone, Copy)]
586pub enum SqliteOpenPhase {
587	InitialPreload,
588	VfsRegister,
589	WorkerReady,
590	Total,
591}
592
593impl SqliteOpenPhase {
594	pub fn as_label(self) -> &'static str {
595		match self {
596			SqliteOpenPhase::InitialPreload => "initial_preload",
597			SqliteOpenPhase::VfsRegister => "vfs_register",
598			SqliteOpenPhase::WorkerReady => "worker_ready",
599			SqliteOpenPhase::Total => "total",
600		}
601	}
602}
603
604pub trait SqliteVfsMetrics: Send + Sync {
605	fn profiling_enabled(&self) -> bool;
606
607	fn max_profiled_get_pages_requests(&self) -> usize;
608
609	/// Records an operation and returns whether its selected fingerprint was admitted.
610	fn observe_operation_profile(&self, profile: &SqliteOperationMetric) -> bool;
611
612	/// Records a transaction and returns whether its selected fingerprint was admitted.
613	fn observe_transaction_profile(&self, profile: &SqliteTransactionMetric) -> bool;
614
615	/// Selects a diagnostic before its caller computes SQL fingerprints.
616	fn operation_diagnostic_selected(&self, _profile: &SqliteOperationMetric) -> bool {
617		true
618	}
619
620	fn transaction_diagnostic_selected(&self, _profile: &SqliteTransactionMetric) -> bool {
621		true
622	}
623
624	/// Emits an event already selected by operation_diagnostic_selected.
625	fn emit_operation_diagnostic_event(
626		&self,
627		actor_id: &str,
628		generation: Option<u64>,
629		profile: &SqliteOperationMetric,
630	);
631
632	fn emit_transaction_diagnostic_event(
633		&self,
634		actor_id: &str,
635		generation: Option<u64>,
636		profile: &SqliteTransactionMetric,
637	);
638
639	fn record_fingerprint_catalog(
640		&self,
641		operation_type: &'static str,
642		fingerprint: &str,
643		identity: &str,
644		format_version: u8,
645	);
646
647	fn record_resolve_pages(&self, requested_pages: u64);
648
649	fn record_resolve_cache_hits(&self, pages: u64);
650
651	fn record_resolve_cache_misses(&self, pages: u64);
652
653	fn record_get_pages_request(&self, pages: u64, prefetch_pages: u64, page_size: u64);
654
655	fn observe_get_pages_duration(&self, duration_ns: u64);
656
657	fn observe_open_phase(&self, phase: SqliteOpenPhase, outcome: &'static str, duration_ns: u64);
658
659	fn record_startup_preload_pages(&self, kind: &'static str, pages: u64);
660
661	/// Records one successful commit and the dirty pages it wrote.
662	fn record_commit(&self, dirty_pages: u64);
663
664	fn observe_commit_phases(
665		&self,
666		request_build_ns: u64,
667		serialize_ns: u64,
668		transport_ns: u64,
669		state_update_ns: u64,
670		total_ns: u64,
671	);
672
673	fn set_worker_queue_depth(&self, depth: u64);
674
675	fn set_worker_active(&self, active: bool);
676
677	fn set_worker_inflight(&self, active: bool);
678
679	/// Reports the bytes this database's VFS page caches hold, after each command, after an
680	/// idle release and as zero when the worker stops.
681	fn set_vfs_cached_bytes(&self, bytes: u64);
682	/// Prefetched pages discarded before their first use, including close and lazy TTL expiry.
683	fn record_prefetch_unused_pages(&self, _pages: u64) {}
684
685	fn set_coordinator_queue_depth(&self, depth: u64);
686
687	fn record_worker_queue_overload(&self);
688
689	fn observe_worker_command_duration(
690		&self,
691		operation: &'static str,
692		in_tx: bool,
693		stmt_kind: &'static str,
694		duration_ns: u64,
695	);
696
697	fn observe_transaction_round_trips(&self, get_pages_round_trips: u64, commit_round_trips: u64);
698
699	fn record_worker_command_error(&self, operation: &'static str, code: &'static str);
700
701	fn observe_worker_close_duration(&self, duration_ns: u64);
702
703	fn record_worker_close_timeout(&self);
704
705	fn record_worker_crash(&self);
706
707	fn record_worker_unclean_close(&self);
708}
709
710#[derive(Debug, Clone, Copy, Default)]
711struct CommitTransportMetrics {
712	serialize_ns: u64,
713	transport_ns: u64,
714}
715
716enum CommitWait<T> {
717	Completed(T),
718	TimedOut,
719}
720
721pub struct VfsContext {
722	actor_id: String,
723	generation: Option<u64>,
724	runtime: Handle,
725	transport: SqliteTransportHandle,
726	config: VfsConfig,
727	state: RwLock<VfsState>,
728	aux_files: RwLock<BTreeMap<String, Arc<AuxFileState>>>,
729	last_error: Mutex<Option<String>>,
730	indeterminate_result: Mutex<Option<String>>,
731	transient_commit_error: Mutex<Option<String>>,
732	/// Set when a write was refused for crossing the commit page cap, until the worker reports it.
733	transaction_too_large: Mutex<Option<SqliteTransactionTooLargeError>>,
734	/// The typed storage failure behind the I/O error SQLite last saw, for the command that
735	/// ran the statement to return instead of SQLite's generic message.
736	storage_error: Mutex<Option<SqliteStorageError>>,
737	fatal_error: RwLock<Option<String>>,
738	
739	
740	
741	
742	commit_atomic_count: AtomicU64,
743	/// Diagnostic attempts, distinct even when a refused/timed-out commit did not advance commit_total.
744	trace_commit_attempts: AtomicU64,
745	io_methods: Box<sqlite3_io_methods>,
746	// Performance counters
747	pub resolve_pages_total: AtomicU64,
748	pub resolve_pages_cache_hits: AtomicU64,
749	pub resolve_pages_fetches: AtomicU64,
750	pub pages_fetched_total: AtomicU64,
751	pub prefetch_pages_total: AtomicU64,
752	/// Pages SQLite read from what the server sent beyond the requested pages.
753	pub prefetch_consumed_total: AtomicU64,
754	pub commit_total: AtomicU64,
755	pub commit_request_build_ns: AtomicU64,
756	pub commit_serialize_ns: AtomicU64,
757	pub commit_transport_ns: AtomicU64,
758	pub commit_state_update_ns: AtomicU64,
759	pub commit_duration_ns_total: AtomicU64,
760	// SQLite invokes VFS callbacks synchronously, so this profiling context cannot
761	// use an async mutex. The native worker releases each guard before returning
762	// to async code.
763	operation_profile_active: AtomicBool,
764	operation_profile: Mutex<Option<SqliteOperationProfile>>,
765	operation_prefetch_unused_start: AtomicU64,
766	reported_prefetch_unused: AtomicU64,
767	metrics: Option<Arc<dyn SqliteVfsMetrics>>,
768}
769
770#[derive(Debug, Clone)]
771struct VfsState {
772	db_size_pages: u32,
773	/// Size depot last acknowledged. `db_size_pages` moving away from this is a pending size change
774	/// that has to reach depot even when no page bytes are dirty, because SQLite reports a shrink
775	/// through `xTruncate` alone.
776	committed_db_size_pages: u32,
777	head_txid: Option<u64>,
778	page_size: usize,
779	/// Pages fetched ahead of SQLite's requests. A page leaves when SQLite reads it.
780	prefetch_buffer: PageBuffer,
781	/// Pages this database committed in the last `staging_cache_ttl_ms`, for re-reads after
782	/// SQLite's pager evicts them. A page leaves when SQLite reads it back.
783	committed_buffer: PageBuffer,
784	prefetch_unused_total: Arc<AtomicU64>,
785	write_buffer: WriteBuffer,
786	/// Pages of transactions SQLite committed while commits are corked, not yet sent to depot.
787	/// `Some` while corked: each commit adds its pages here instead of committing, and uncorking
788	/// sends them all as one commit. Reads see these pages before any cache.
789	corked: Option<BTreeMap<u32, PageBytes>>,
790	dead: bool,
791	/// Database size after the last transaction moved into the cork, which a commit of the cork
792	/// alone carries.
793	corked_db_size_pages: u32,
794	/// Set when a main database write was refused for crossing the commit page cap or a commit
795	/// of the write buffer failed, until SQLite's rollback commits or a new transaction starts.
796	/// Until then the write buffer holds pages of a transaction SQLite is abandoning, which a
797	/// close must discard, not commit.
798	refused_write_pending: bool,
799	/// Set by `NativeDatabase::Drop` once it has published every committed transaction, just
800	/// before SQLite closes. Main file writes after that come from SQLite rolling back a
801	/// transaction left open at close, which restores the state Depot and the cork already
802	/// hold, so commits discard them instead of sending them. A `SqliteVfs` serves one
803	/// connection for its lifetime, so the flag never outlives the connection that set it.
804	closing: bool,
805}
806
807#[derive(Debug, Clone, Copy, PartialEq, Eq)]
808enum CorkOutcome {
809	NotCorked,
810	Corked,
811	Overflow,
812}
813
814#[derive(Debug, Clone, Default)]
815struct WriteBuffer {
816	in_atomic_write: bool,
817	saved_db_size: u32,
818	dirty: BTreeMap<u32, PageBytes>,
819}
820
821impl WriteBuffer {
822	fn snapshot(&self) -> Vec<BufferedDirtyPage> {
823		self.dirty
824			.iter()
825			.map(|(pgno, bytes)| BufferedDirtyPage {
826				pgno: *pgno,
827				bytes: bytes.clone(),
828			})
829			.collect()
830	}
831}
832
833#[derive(Debug)]
834enum GetPagesError {
835	FenceMismatch(String),
836	Storage(SqliteStorageError),
837	Other(String),
838}
839
840#[repr(C)]
841struct VfsFile {
842	base: sqlite3_file,
843	ctx: *const VfsContext,
844	aux: *mut AuxFileHandle,
845}
846
847#[derive(Default)]
848struct AuxFileState {
849	bytes: Mutex<Vec<u8>>,
850	/// Page records written to this file since it was last emptied, counted only for the main
851	/// database's rollback journal.
852	journal_records: AtomicUsize,
853}
854
855struct AuxFileHandle {
856	path: String,
857	state: Arc<AuxFileState>,
858	delete_on_close: bool,
859	/// Whether this is the main database's rollback journal, whose size the commit cap bounds.
860	is_main_journal: bool,
861}
862
863#[derive(Debug, Clone, Copy, PartialEq, Eq)]
864enum PageCacheInsertKind {
865	Target,
866	PartialTarget,
867	Prefetch,
868	Startup,
869}
870
871unsafe impl Send for VfsContext {}
872unsafe impl Sync for VfsContext {}
873
874pub struct SqliteVfs {
875	_registration: SqliteVfsRegistration,
876	_name: CString,
877	ctx: Box<VfsContext>,
878}
879
880unsafe impl Send for SqliteVfs {}
881unsafe impl Sync for SqliteVfs {}
882
883struct SqliteVfsRegistration {
884	vfs_ptr: *mut sqlite3_vfs,
885}
886
887pub struct NativeDatabase {
888	db: *mut sqlite3,
889	_vfs: NativeVfsHandle,
890}
891
892unsafe impl Send for NativeDatabase {}
893
894pub type NativeVfsHandle = Arc<SqliteVfs>;
895pub type NativeConnection = NativeDatabase;
896
897impl VfsState {
898	/// The database size every committed transaction left, including corked ones.
899	fn committed_size_pages_before_transaction(&self) -> u32 {
900		if self.corked.is_some() {
901			self.corked_db_size_pages
902		} else {
903			self.committed_db_size_pages
904		}
905	}
906
907	/// Drops the write buffer of a transaction SQLite has not committed and restores the size
908	/// from before it. Depot and the cork already hold every committed transaction: each
909	/// commit syncs the main file (connections cannot turn `synchronous` off), and the sync
910	/// sends or corks its pages, so the write buffer holds only the unfinished transaction.
911	fn discard_uncommitted_pages(&mut self) {
912		self.write_buffer.dirty.clear();
913		self.write_buffer.in_atomic_write = false;
914		self.db_size_pages = self.committed_size_pages_before_transaction();
915		self.refused_write_pending = false;
916	}
917
918	fn new(config: &VfsConfig, _profiling_enabled: bool) -> Self {
919		let prefetch_unused_total = Arc::new(AtomicU64::new(0));
920		let prefetch_buffer =
921			PageBuffer::with_unused_counter(&config.page_budget, prefetch_unused_total.clone());
922		let committed_buffer = build_page_buffer(config);
923		let mut state = Self {
924			db_size_pages: 1,
925			committed_db_size_pages: 1,
926			head_txid: None,
927			page_size: DEFAULT_PAGE_SIZE,
928			prefetch_buffer,
929			committed_buffer,
930			prefetch_unused_total,
931			write_buffer: WriteBuffer::default(),
932			corked: None,
933			dead: false,
934			corked_db_size_pages: 0,
935			closing: false,
936			refused_write_pending: false,
937		};
938		state.cache_page(
939			config,
940			PageCacheInsertKind::Startup,
941			1,
942			empty_db_page().into(),
943		);
944		state
945	}
946
947	fn cache_page(
948		&mut self,
949		config: &VfsConfig,
950		kind: PageCacheInsertKind,
951		pgno: u32,
952		bytes: PageBytes,
953	) {
954		if !should_cache_page(config, kind, pgno) {
955			return;
956		}
957		cache_page(config, &self.prefetch_buffer, kind, pgno, bytes);
958	}
959
960	/// Hands a buffered page to SQLite: the page leaves the buffer, because SQLite's pager owns
961	/// every page it has read. Page 1 stays, because SQLite also reads its header directly,
962	/// outside the pager. So does a page SQLite reads only part of (`partial`), such as the
963	/// last page of an overflow chain read directly, bypassing the pager: SQLite reads that page
964	/// again whole through the pager.
965	fn take_buffered_page(
966		&self,
967		config: &VfsConfig,
968		pgno: u32,
969		partial: bool,
970	) -> Option<(PageBytes, bool)> {
971		if !can_read_cached_page(config, pgno) {
972			return None;
973		}
974		if pgno == 1 || partial {
975			let bytes = self
976				.committed_buffer
977				.get(&pgno)
978				.or_else(|| self.prefetch_buffer.get(&pgno))?;
979			return Some((bytes, false));
980		}
981
982		// Dropping the page here relies on SQLite 3.46.0 as bundled by libsqlite3-sys 0.30.1; line
983		// numbers are in that crate's `sqlite3/sqlite3.c` amalgamation.
984		// With `SQLITE_DIRECT_OVERFLOW_READ=0` (set in `.cargo/config.toml`), `journal_mode =
985		// DELETE` (so no WAL reads of the database file) and no memory map (`iVersion` 1 has no
986		// `xFetch`), the only whole-page `xRead` of the main database is `readDbPage`
987		// (sqlite3.c:60138, pager.c), which reads
988		// into a page-cache slot that the pager holds pinned while it is in use. The other
989		// main-database reads are partial and only touch page 1: the 100-byte header in
990		// `sqlite3PagerReadFileheader` (sqlite3.c:60994) and the 16-byte change counter in
991		// `pagerSharedLock` (sqlite3.c:62490). The direct overflow read in `accessPayload`
992		// (sqlite3.c:75763) would read every overflow page but the last of a chain whole, past
993		// the cache, but that flag compiles it out.
994		//
995		// SQLite does not keep the page forever. Once unpinned, it leaves the cache when the
996		// process-wide cache in `pager_cache.rs` evicts it under its byte budget (`PRAGMA
997		// cache_size` does not bound it, since `x_cachesize` ignores it), on `xShrink` from
998		// `sqlite3_db_release_memory`, and on `pager_reset`: an I/O error at unlock
999		// (sqlite3.c:59012), a page size change (60888), close (61281), VACUUM's
1000		// `sqlite3PagerClearCache` (64577) or hot-journal playback (60009). The change counter
1001		// check that resets the cache when another connection wrote (62499) does not run here,
1002		// because `locking_mode = EXCLUSIVE` keeps the pager in its read state between
1003		// transactions, and the VFS serves exactly one connection per database. A page that
1004		// left both caches costs one depot fetch on its next read and never returns wrong data,
1005		// because each buffered page is a copy depot already holds: fetched from it, or written
1006		// by a commit it acknowledged.
1007		if let Some(bytes) = self.committed_buffer.take(&pgno) {
1008			// A neighboring miss may have prefetched this page after its commit.
1009			// The authoritative committed copy wins; its redundant speculative copy
1010			// was not consumed and must leave the buffer (and count as unused).
1011			self.prefetch_buffer.invalidate(&pgno);
1012			Some((bytes, false))
1013		} else {
1014			self.prefetch_buffer.take_with_status(&pgno)
1015		}
1016	}
1017
1018	
1019
1020	/// A page a corked commit holds, which no cache may stand in for.
1021	fn corked_page(&self, pgno: u32) -> Option<&PageBytes> {
1022		self.corked.as_ref().and_then(|corked| corked.get(&pgno))
1023	}
1024
1025	/// While corked, moves the finished transaction's pages into the cork instead of
1026	/// committing them.
1027	///
1028	/// The cork is sent as one commit, so it holds at most `max_pages` pages. When the
1029	/// transaction would take it past that, nothing moves and the result is `Overflow`: the
1030	/// caller commits the cork on its own first, then corks the transaction.
1031	fn cork_dirty_pages(&mut self, max_pages: usize) -> CorkOutcome {
1032		let Some(corked) = self.corked.as_mut() else {
1033			return CorkOutcome::NotCorked;
1034		};
1035		let added_pages = self
1036			.write_buffer
1037			.dirty
1038			.keys()
1039			.filter(|pgno| !corked.contains_key(pgno))
1040			.count();
1041		if corked.len() + added_pages > max_pages {
1042			return CorkOutcome::Overflow;
1043		}
1044		corked.append(&mut self.write_buffer.dirty);
1045		self.corked_db_size_pages = self.db_size_pages;
1046		CorkOutcome::Corked
1047	}
1048
1049	/// Keeps a committed page for a re-read after SQLite's pager evicts it, including under another
1050	/// connection's pressure on the process-wide pager cache, and drops any older prefetched copy. SQLite's pager holds the page right after the commit, so read-ahead
1051	/// skips it.
1052	fn note_committed_page(&mut self, config: &VfsConfig, pgno: u32, bytes: &PageBytes) {
1053		self.prefetch_buffer.invalidate(&pgno);
1054		if pgno != 1 {}
1055		if config.staging_cache_ttl_ms == 0 || !config.page_cache_mode.caches_any_pages() {
1056			return;
1057		}
1058		self.committed_buffer.insert(pgno, bytes.clone());
1059	}
1060
1061	fn seed_page(
1062		&mut self,
1063		config: &VfsConfig,
1064		kind: PageCacheInsertKind,
1065		pgno: u32,
1066		page: PageBytes,
1067	) {
1068		if pgno == 1 {
1069			self.seed_main_page(config, kind, page);
1070		} else {
1071			self.cache_page(config, kind, pgno, page);
1072		}
1073	}
1074
1075	fn seed_main_page(&mut self, config: &VfsConfig, kind: PageCacheInsertKind, page: PageBytes) {
1076		if let Some(page_size) = sqlite_header_page_size(&page) {
1077			self.page_size = page_size;
1078		}
1079		if let Some(db_size_pages) = sqlite_header_db_size_pages(&page) {
1080			self.db_size_pages = db_size_pages;
1081			self.committed_db_size_pages = db_size_pages;
1082		}
1083		self.cache_page(config, kind, 1, page);
1084	}
1085
1086	fn invalidate_page_buffers(&mut self) {
1087		self.prefetch_buffer.invalidate_all();
1088		self.committed_buffer.invalidate_all();
1089	}
1090}
1091
1092fn build_page_buffer(config: &VfsConfig) -> PageBuffer {
1093	PageBuffer::new(&config.page_budget)
1094}
1095
1096fn cache_page(
1097	config: &VfsConfig,
1098	prefetch_buffer: &PageBuffer,
1099	kind: PageCacheInsertKind,
1100	pgno: u32,
1101	bytes: PageBytes,
1102) {
1103	if !should_cache_page(config, kind, pgno) {
1104		return;
1105	}
1106	if kind == PageCacheInsertKind::Prefetch || (kind == PageCacheInsertKind::Startup && pgno != 1)
1107	{
1108		prefetch_buffer.insert_prefetched(pgno, bytes);
1109	} else {
1110		prefetch_buffer.insert(pgno, bytes);
1111	}
1112}
1113
1114fn should_cache_page(config: &VfsConfig, kind: PageCacheInsertKind, pgno: u32) -> bool {
1115	match kind {
1116		PageCacheInsertKind::Target => false,
1117		PageCacheInsertKind::PartialTarget => true,
1118		PageCacheInsertKind::Prefetch => {
1119			config.staging_cache_ttl_ms > 0 && config.page_cache_mode.caches_prefetched_pages()
1120		}
1121		PageCacheInsertKind::Startup => {
1122			pgno == 1
1123				|| (config.staging_cache_ttl_ms > 0
1124					&& config.page_cache_mode.caches_startup_preloaded_pages())
1125		}
1126	}
1127}
1128
1129fn can_read_cached_page(config: &VfsConfig, pgno: u32) -> bool {
1130	pgno == 1 || config.page_cache_mode.caches_any_pages()
1131}
1132
1133impl VfsContext {
1134	fn new(
1135		actor_id: String,
1136		generation: Option<u64>,
1137		runtime: Handle,
1138		transport: SqliteTransportHandle,
1139		config: VfsConfig,
1140		io_methods: sqlite3_io_methods,
1141		initial_pages: impl Into<InitialPages>,
1142		metrics: Option<Arc<dyn SqliteVfsMetrics>>,
1143	) -> std::result::Result<Self, String> {
1144		let profiling_enabled = metrics
1145			.as_ref()
1146			.is_some_and(|metrics| metrics.profiling_enabled());
1147		let mut state = VfsState::new(&config, profiling_enabled);
1148		let initial_pages = initial_pages.into();
1149		state.head_txid = initial_pages.head_txid;
1150		for (pgno, page) in initial_pages.pages {
1151			state.seed_page(&config, PageCacheInsertKind::Startup, pgno, page);
1152		}
1153
1154		Ok(Self {
1155			actor_id,
1156			generation,
1157			runtime,
1158			transport,
1159			config: config.clone(),
1160			state: RwLock::new(state),
1161			aux_files: RwLock::new(BTreeMap::new()),
1162			last_error: Mutex::new(None),
1163			indeterminate_result: Mutex::new(None),
1164			transient_commit_error: Mutex::new(None),
1165			transaction_too_large: Mutex::new(None),
1166			storage_error: Mutex::new(None),
1167			fatal_error: RwLock::new(None),
1168			
1169			
1170			
1171			
1172			commit_atomic_count: AtomicU64::new(0),
1173			trace_commit_attempts: AtomicU64::new(0),
1174			io_methods: Box::new(io_methods),
1175			resolve_pages_total: AtomicU64::new(0),
1176			resolve_pages_cache_hits: AtomicU64::new(0),
1177			resolve_pages_fetches: AtomicU64::new(0),
1178			pages_fetched_total: AtomicU64::new(0),
1179			prefetch_pages_total: AtomicU64::new(0),
1180			prefetch_consumed_total: AtomicU64::new(0),
1181			commit_total: AtomicU64::new(0),
1182			commit_request_build_ns: AtomicU64::new(0),
1183			commit_serialize_ns: AtomicU64::new(0),
1184			commit_transport_ns: AtomicU64::new(0),
1185			commit_state_update_ns: AtomicU64::new(0),
1186			commit_duration_ns_total: AtomicU64::new(0),
1187			operation_profile_active: AtomicBool::new(false),
1188			operation_profile: Mutex::new(None),
1189			operation_prefetch_unused_start: AtomicU64::new(0),
1190			reported_prefetch_unused: AtomicU64::new(0),
1191			metrics,
1192		})
1193	}
1194
1195	fn begin_operation_profile(&self) {
1196		if self
1197			.metrics
1198			.as_ref()
1199			.is_some_and(|metrics| metrics.profiling_enabled())
1200		{
1201			let unused = self
1202				.state
1203				.read()
1204				.prefetch_unused_total
1205				.load(Ordering::Relaxed);
1206			self.operation_prefetch_unused_start
1207				.store(unused, Ordering::Relaxed);
1208			*self.operation_profile.lock() = Some(SqliteOperationProfile::default());
1209			self.operation_profile_active.store(true, Ordering::Release);
1210		}
1211	}
1212
1213	fn report_prefetch_unused(&self) {
1214		let total = self
1215			.state
1216			.read()
1217			.prefetch_unused_total
1218			.load(Ordering::Relaxed);
1219		let previous = self
1220			.reported_prefetch_unused
1221			.fetch_max(total, Ordering::Relaxed);
1222		if total > previous
1223			&& let Some(metrics) = &self.metrics
1224		{
1225			metrics.record_prefetch_unused_pages(total - previous);
1226		}
1227	}
1228
1229	fn finish_operation_profile(&self) -> SqliteOperationProfile {
1230		self.report_prefetch_unused();
1231		if !self.operation_profile_active.swap(false, Ordering::AcqRel) {
1232			return SqliteOperationProfile::default();
1233		}
1234		let mut profile = self.operation_profile.lock().take().unwrap_or_default();
1235		let unused = self
1236			.state
1237			.read()
1238			.prefetch_unused_total
1239			.load(Ordering::Relaxed)
1240			.saturating_sub(self.operation_prefetch_unused_start.load(Ordering::Relaxed));
1241		profile.prefetch_unused_pages = unused;
1242		profile
1243	}
1244
1245	fn update_operation_profile(&self, update: impl FnOnce(&mut SqliteOperationProfile)) {
1246		if !self.operation_profile_active.load(Ordering::Relaxed) {
1247			return;
1248		}
1249		if let Some(profile) = self.operation_profile.lock().as_mut() {
1250			update(profile);
1251		}
1252	}
1253
1254	fn profile_request_limit(&self) -> usize {
1255		self.metrics
1256			.as_ref()
1257			.map_or(0, |metrics| metrics.max_profiled_get_pages_requests())
1258			.min(MAX_PROFILED_GET_PAGES_REQUESTS)
1259	}
1260
1261	fn clear_last_error(&self) {
1262		*self.last_error.lock() = None;
1263	}
1264
1265	fn set_last_error(&self, message: String) {
1266		*self.last_error.lock() = Some(message);
1267	}
1268
1269	fn mark_indeterminate(&self, operation: String) {
1270		*self.indeterminate_result.lock() = Some(operation.clone());
1271		self.mark_fatal(SqliteCommitIndeterminateError(operation).to_string());
1272	}
1273
1274	fn take_indeterminate_result(&self) -> Option<String> {
1275		self.indeterminate_result.lock().take()
1276	}
1277
1278	fn clone_last_error(&self) -> Option<String> {
1279		self.last_error.lock().clone()
1280	}
1281
1282	fn clone_fatal_error(&self) -> Option<String> {
1283		self.fatal_error.read().clone()
1284	}
1285
1286	fn defer_transient_commit_error(&self, message: String) {
1287		*self.transient_commit_error.lock() = Some(message);
1288	}
1289
1290	fn take_transient_commit_error(&self) -> Option<String> {
1291		self.transient_commit_error.lock().take()
1292	}
1293
1294	fn transaction_too_large_error(&self) -> SqliteTransactionTooLargeError {
1295		SqliteTransactionTooLargeError {
1296			max_pages: self.config.commit_page_cap(),
1297			page_size: self.page_size(),
1298		}
1299	}
1300
1301	/// Refuses a write that would take the transaction past the commit page cap. SQLite treats
1302	/// `SQLITE_FULL` like a full disk: it fails the statement and rolls back, restoring the
1303	/// pages from its journal, so nothing over the cap is ever held or sent.
1304	fn refuse_oversize_transaction(&self) -> c_int {
1305		let error = self.transaction_too_large_error();
1306		tracing::warn!(
1307			actor_id = %self.actor_id,
1308			max_pages = error.max_pages,
1309			"sqlite transaction exceeds the commit page cap; the application must split it into smaller transactions"
1310		);
1311		self.set_last_error(error.to_string());
1312		*self.transaction_too_large.lock() = Some(error);
1313		SQLITE_FULL
1314	}
1315
1316	/// A commit refused for crossing the cap. The write cap keeps every transaction under it,
1317	/// so this is only a backstop.
1318	fn transaction_too_large_commit_error(&self) -> CommitBufferError {
1319		CommitBufferError::Other(self.transaction_too_large_error().to_string())
1320	}
1321
1322	/// Commits the transactions held in the cork as one commit and leaves the cork empty, for
1323	/// when the next transaction would take the cork past one commit. Corked transactions were
1324	/// never promised to be atomic together, only to become durable, and each commit still
1325	/// holds whole transactions.
1326	fn commit_cork_alone(&self) -> std::result::Result<(), CommitBufferError> {
1327		let request = {
1328			let state = self.state.read();
1329			let Some(corked) = state.corked.as_ref() else {
1330				return Ok(());
1331			};
1332			BufferedCommitRequest {
1333				actor_id: self.actor_id.clone(),
1334				new_db_size_pages: state.corked_db_size_pages,
1335				expected_head_txid: state.head_txid,
1336				dirty_pages: corked
1337					.iter()
1338					.map(|(pgno, bytes)| BufferedDirtyPage {
1339						pgno: *pgno,
1340						bytes: bytes.clone(),
1341					})
1342					.collect(),
1343			}
1344		};
1345
1346		let outcome = match self.block_on_buffered_commit(request.clone(), None) {
1347			Ok(CommitWait::Completed((outcome, _))) => outcome,
1348			Ok(CommitWait::TimedOut) => {
1349				return Err(CommitBufferError::Other(
1350					"sqlite commit timed out".to_string(),
1351				));
1352			}
1353			Err(err) => {
1354				tracing::error!(
1355					actor_id = %self.actor_id,
1356					dirty_pages = request.dirty_pages.len(),
1357					?err,
1358					"sqlite commit of a full cork failed"
1359				);
1360				handle_non_finalize_commit_error(self, &err);
1361				return Err(err);
1362			}
1363		};
1364		self.commit_total.fetch_add(1, Ordering::Relaxed);
1365		if let Some(metrics) = &self.metrics {
1366			metrics.record_commit(request.dirty_pages.len() as u64);
1367		}
1368
1369		let mut state = self.state.write();
1370		state.committed_db_size_pages = outcome.db_size_pages;
1371		state.head_txid = outcome
1372			.head_txid
1373			.or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
1374		for dirty_page in &request.dirty_pages {
1375			state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
1376		}
1377		if let Some(corked) = state.corked.as_mut() {
1378			corked.clear();
1379		}
1380		Ok(())
1381	}
1382
1383	pub(crate) fn take_transaction_too_large(&self) -> Option<SqliteTransactionTooLargeError> {
1384		self.transaction_too_large.lock().take()
1385	}
1386
1387	/// Records a typed storage failure SQLite is about to see as an I/O error.
1388	fn set_storage_error(&self, error: SqliteStorageError) {
1389		self.set_last_error(error.to_string());
1390		*self.storage_error.lock() = Some(error);
1391	}
1392
1393	pub(crate) fn take_storage_error(&self) -> Option<SqliteStorageError> {
1394		self.storage_error.lock().take()
1395	}
1396
1397	/// Discards the pages of a transaction whose write was refused, when the database closes
1398	/// before SQLite has rolled it back. Depot and the cork still hold the state from before
1399	/// that transaction, which is exactly what the rollback would restore.
1400	fn discard_refused_transaction(&self) {
1401		let mut state = self.state.write();
1402		if !state.refused_write_pending {
1403			return;
1404		}
1405		tracing::debug!(
1406			actor_id = %self.actor_id,
1407			dirty_pages = state.write_buffer.dirty.len(),
1408			"discarding the pages of an abandoned sqlite transaction at close"
1409		);
1410		state.discard_uncommitted_pages();
1411	}
1412
1413	/// Marks the write buffer as holding a transaction whose commit failed. SQLite reports the
1414	/// failure and rolls the transaction back on its next access, which rewrites the journaled
1415	/// pages and commits them; a close before that access must discard the buffer instead.
1416	fn abandon_write_buffer(&self) {
1417		self.state.write().refused_write_pending = true;
1418	}
1419
1420	/// Discards the pages of a transaction still open when the database closes. SQLite never
1421	/// committed it, so none of its pages may reach Depot.
1422	fn discard_open_transaction(&self) {
1423		let mut state = self.state.write();
1424		if state.write_buffer.dirty.is_empty()
1425			&& !state.write_buffer.in_atomic_write
1426			&& state.db_size_pages == state.committed_size_pages_before_transaction()
1427		{
1428			return;
1429		}
1430		tracing::debug!(
1431			actor_id = %self.actor_id,
1432			dirty_pages = state.write_buffer.dirty.len(),
1433			"discarding the pages of an uncommitted sqlite transaction at close"
1434		);
1435		state.discard_uncommitted_pages();
1436	}
1437
1438	/// Drops every page a closing connection failed to send, including the commits SQLite
1439	/// completed while corked. Those commits are lost for this connection, so the close no
1440	/// longer holds them and the next open reads whatever Depot accepted. Returns how many
1441	/// pages were dropped.
1442	fn discard_unsent_commits(&self) -> usize {
1443		let mut state = self.state.write();
1444		let dirty_pages = state.write_buffer.dirty.len();
1445		state.discard_uncommitted_pages();
1446		dirty_pages
1447	}
1448
1449	
1450
1451	pub(crate) fn take_last_error(&self) -> Option<String> {
1452		self.last_error.lock().take()
1453	}
1454
1455	fn add_commit_phase_metrics(
1456		&self,
1457		request_build_ns: u64,
1458		transport_metrics: CommitTransportMetrics,
1459		state_update_ns: u64,
1460		total_ns: u64,
1461	) {
1462		self.update_operation_profile(|profile| {
1463			profile.storage_ns = profile
1464				.storage_ns
1465				.saturating_add(transport_metrics.transport_ns);
1466			profile.commit_round_trips = profile.commit_round_trips.saturating_add(1);
1467		});
1468		if let Some(metrics) = &self.metrics {
1469			metrics.observe_commit_phases(
1470				request_build_ns,
1471				transport_metrics.serialize_ns,
1472				transport_metrics.transport_ns,
1473				state_update_ns,
1474				total_ns,
1475			);
1476		}
1477		self.commit_request_build_ns
1478			.fetch_add(request_build_ns, Ordering::Relaxed);
1479		self.commit_serialize_ns
1480			.fetch_add(transport_metrics.serialize_ns, Ordering::Relaxed);
1481		self.commit_transport_ns
1482			.fetch_add(transport_metrics.transport_ns, Ordering::Relaxed);
1483		self.commit_state_update_ns
1484			.fetch_add(state_update_ns, Ordering::Relaxed);
1485		self.commit_duration_ns_total
1486			.fetch_add(total_ns, Ordering::Relaxed);
1487	}
1488
1489	fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
1490		let state = self.state.read();
1491
1492		SqliteVfsMetricsSnapshot {
1493			request_build_ns: self.commit_request_build_ns.load(Ordering::Relaxed),
1494			serialize_ns: self.commit_serialize_ns.load(Ordering::Relaxed),
1495			transport_ns: self.commit_transport_ns.load(Ordering::Relaxed),
1496			state_update_ns: self.commit_state_update_ns.load(Ordering::Relaxed),
1497			total_ns: self.commit_duration_ns_total.load(Ordering::Relaxed),
1498			commit_count: self.commit_total.load(Ordering::Relaxed),
1499			page_cache_entries: state
1500				.prefetch_buffer
1501				.entry_count()
1502				.saturating_add(state.committed_buffer.entry_count()),
1503			page_cache_weighted_size: state.prefetch_buffer.weighted_size(),
1504			page_cache_capacity_pages: self.config.page_budget.capacity_pages(),
1505			write_buffer_dirty_pages: state.write_buffer.dirty.len() as u64,
1506			db_size_pages: state.db_size_pages as u64,
1507		}
1508	}
1509
1510	fn block_on_buffered_commit(
1511		&self,
1512		request: BufferedCommitRequest,
1513		timeout: Option<Duration>,
1514	) -> std::result::Result<
1515		CommitWait<(BufferedCommitOutcome, CommitTransportMetrics)>,
1516		CommitBufferError,
1517	> {
1518		let trace_pages =
1519			tracing::enabled!(target: "sqlite_prefetch::trace", tracing::Level::TRACE).then(|| {
1520				(
1521					request
1522						.dirty_pages
1523						.iter()
1524						.map(|page| page.pgno)
1525						.collect::<Vec<_>>(),
1526					request.new_db_size_pages,
1527				)
1528			});
1529		let attempt = trace_pages
1530			.as_ref()
1531			.map(|_| self.trace_commit_attempts.fetch_add(1, Ordering::Relaxed));
1532		let span = tracing::trace_span!(target: "sqlite_prefetch::trace", "native_sqlite_commit",
1533            stream_id = %format_args!("sqlite-{:p}", self), actor_id = %self.actor_id,
1534            generation = self.generation, commit_attempt = attempt,
1535            local_request_id = %format_args!("c{}", self.commit_total.load(Ordering::Relaxed)));
1536		let commit =
1537			commit_buffered_pages(&*self.transport, request, self.config.commit_page_cap())
1538				.instrument(span.clone());
1539		let result = if let Some(timeout) = timeout {
1540			match self
1541				.runtime
1542				.block_on(async { tokio::time::timeout(timeout, commit).await })
1543			{
1544				Ok(result) => result,
1545				Err(_) => {
1546					span.in_scope(|| tracing::trace!(target: "sqlite_prefetch::trace", event = "commit_result", outcome = "timeout"));
1547					return Ok(CommitWait::TimedOut);
1548				}
1549			}
1550		} else {
1551			self.runtime.block_on(commit)
1552		};
1553
1554		let _span = span.enter();
1555		tracing::trace!(target: "sqlite_prefetch::trace", event = "commit_result",
1556            outcome = if result.is_ok() { "acknowledged" } else { "error" });
1557
1558		if result.is_ok()
1559			&& let Some((pages, size_pages)) = trace_pages
1560		{
1561			let request_id = self.commit_total.load(Ordering::Relaxed);
1562			for pgno in pages {
1563				tracing::trace!(target: "sqlite_prefetch::trace", event = "access", op = "write",
1564                    stream_id = %format_args!("sqlite-{:p}", self),
1565                    pgno, hit = false, request_id = %format_args!("c{request_id}"));
1566			}
1567			tracing::trace!(target: "sqlite_prefetch::trace", event = "commit",
1568                stream_id = %format_args!("sqlite-{:p}", self),
1569                request_id = %format_args!("c{request_id}"), size_pages);
1570		}
1571		result.map(CommitWait::Completed)
1572	}
1573
1574	fn round_trip_counts(&self) -> SqliteRoundTripCounts {
1575		SqliteRoundTripCounts {
1576			get_pages: self.resolve_pages_fetches.load(Ordering::Relaxed),
1577			commit: self.commit_total.load(Ordering::Relaxed),
1578		}
1579	}
1580
1581	fn page_size(&self) -> usize {
1582		self.state.read().page_size.max(DEFAULT_PAGE_SIZE)
1583	}
1584
1585	fn open_aux_file(&self, path: &str) -> Arc<AuxFileState> {
1586		let mut aux_files = self.aux_files.write();
1587		aux_files
1588			.entry(path.to_string())
1589			.or_insert_with(|| Arc::new(AuxFileState::default()))
1590			.clone()
1591	}
1592
1593	fn aux_file_exists(&self, path: &str) -> bool {
1594		self.aux_files.read().contains_key(path)
1595	}
1596
1597	fn read_aux_file(&self, path: &str) -> Option<Vec<u8>> {
1598		let state = self.aux_files.read().get(path).cloned()?;
1599		let bytes = state.bytes.lock().clone();
1600		Some(bytes)
1601	}
1602
1603	fn delete_aux_file(&self, path: &str) {
1604		self.aux_files.write().remove(path);
1605	}
1606
1607	
1608
1609	
1610
1611	
1612
1613	
1614
1615	fn is_dead(&self) -> bool {
1616		self.state.read().dead
1617	}
1618
1619	fn mark_fatal(&self, message: String) {
1620		self.set_last_error(message.clone());
1621		self.state.write().dead = true;
1622		let mut fatal_error = self.fatal_error.write();
1623		if fatal_error.is_none() {
1624			*fatal_error = Some(message);
1625		}
1626	}
1627
1628	/// Resolves `target_pgnos` from the buffers or the transport. `partial` marks a read of
1629	/// less than a whole page, which leaves buffered pages in place; see `take_buffered_page`.
1630	fn resolve_pages(
1631		&self,
1632		target_pgnos: &[u32],
1633		partial: bool,
1634	) -> std::result::Result<HashMap<u32, Option<PageBytes>>, GetPagesError> {
1635		use std::sync::atomic::Ordering::Relaxed;
1636		let request_id = self.resolve_pages_total.fetch_add(1, Relaxed);
1637		self.update_operation_profile(|profile| {
1638			profile.sqlite_requested_pages = profile
1639				.sqlite_requested_pages
1640				.saturating_add(target_pgnos.len() as u64);
1641		});
1642		if let Some(metrics) = &self.metrics {
1643			metrics.record_resolve_pages(target_pgnos.len() as u64);
1644		}
1645
1646		let mut resolved = HashMap::new();
1647		let mut missing = Vec::new();
1648		let mut seen = HashSet::new();
1649		let mut prefetch_consumed = 0_u64;
1650
1651		{
1652			let state = self.state.read();
1653			if state.dead {
1654				return Err(GetPagesError::Other(
1655					"sqlite actor lost its fence".to_string(),
1656				));
1657			}
1658
1659			for pgno in target_pgnos.iter().copied() {
1660				if !seen.insert(pgno) {
1661					continue;
1662				}
1663				if let Some(bytes) = state.write_buffer.dirty.get(&pgno) {
1664					resolved.insert(pgno, Some(bytes.clone()));
1665					continue;
1666				}
1667				if let Some(bytes) = state.corked_page(pgno) {
1668					resolved.insert(pgno, Some(bytes.clone()));
1669					continue;
1670				}
1671				if let Some((bytes, was_prefetched)) =
1672					state.take_buffered_page(&self.config, pgno, partial)
1673				{
1674					prefetch_consumed = prefetch_consumed.saturating_add(u64::from(was_prefetched));
1675					resolved.insert(pgno, Some(bytes));
1676					continue;
1677				}
1678				missing.push(pgno);
1679			}
1680		}
1681		if tracing::enabled!(target: "sqlite_prefetch::trace", tracing::Level::TRACE) {
1682			for pgno in target_pgnos {
1683				tracing::trace!(target: "sqlite_prefetch::trace", event = "access", op = "read",
1684                    stream_id = %format_args!("sqlite-{:p}", self),
1685                    pgno, hit = resolved.contains_key(pgno), request_id);
1686			}
1687		}
1688		if prefetch_consumed > 0 {
1689			self.prefetch_consumed_total
1690				.fetch_add(prefetch_consumed, Relaxed);
1691			self.update_operation_profile(|profile| {
1692				profile.prefetch_consumed_pages = profile
1693					.prefetch_consumed_pages
1694					.saturating_add(prefetch_consumed);
1695			});
1696		}
1697
1698		if missing.is_empty() {
1699			self.resolve_pages_cache_hits
1700				.fetch_add(target_pgnos.len() as u64, Relaxed);
1701			if let Some(metrics) = &self.metrics {
1702				metrics.record_resolve_cache_hits(target_pgnos.len() as u64);
1703			}
1704			self.update_operation_profile(|profile| {
1705				profile.cache_hit_pages = profile
1706					.cache_hit_pages
1707					.saturating_add(target_pgnos.len() as u64);
1708			});
1709			return Ok(resolved);
1710		}
1711		self.resolve_pages_cache_hits
1712			.fetch_add((seen.len() - missing.len()) as u64, Relaxed);
1713		if let Some(metrics) = &self.metrics {
1714			metrics.record_resolve_cache_hits((seen.len() - missing.len()) as u64);
1715			metrics.record_resolve_cache_misses(missing.len() as u64);
1716		}
1717		self.update_operation_profile(|profile| {
1718			profile.cache_hit_pages = profile
1719				.cache_hit_pages
1720				.saturating_add((seen.len() - missing.len()) as u64);
1721			profile.cache_miss_pages = profile
1722				.cache_miss_pages
1723				.saturating_add(missing.len() as u64);
1724		});
1725
1726		let (to_fetch, page_size, db_size_pages, expected_head_txid) = {
1727			let state = self.state.read();
1728			// The page server plans every prefetch; the client asks for what SQLite missed.
1729			let to_fetch = missing.clone();
1730			(
1731				to_fetch,
1732				state.page_size.max(1),
1733				state.db_size_pages,
1734				state.head_txid,
1735			)
1736		};
1737
1738		{
1739			self.resolve_pages_fetches.fetch_add(1, Relaxed);
1740			self.pages_fetched_total
1741				.fetch_add(to_fetch.len() as u64, Relaxed);
1742			tracing::debug!(
1743				actor_id = %self.actor_id,
1744				generation = ?self.generation,
1745				requested_pages = ?target_pgnos,
1746				missing_pages = ?missing,
1747				total_fetch_pages = to_fetch.len(),
1748				total_fetch_bytes = to_fetch.len().saturating_mul(page_size),
1749				db_size_pages,
1750				expected_head_txid,
1751				"vfs get_pages fetch"
1752			);
1753		}
1754
1755		let trace_span = tracing::trace_span!(target: "sqlite_prefetch::trace", "native_sqlite_read",
1756            stream_id = %format_args!("sqlite-{:p}", self), actor_id = %self.actor_id,
1757            generation = self.generation, local_request_id = request_id);
1758		let get_pages_start = Instant::now();
1759		// Transport rejection, including envoy shutdown while a VFS callback is
1760		// active, becomes GetPagesError here. The SQLite callback maps that to
1761		// SQLITE_IOERR_* because VFS has no richer async transport error channel.
1762		let response = self.runtime.block_on(
1763			self.transport
1764				.get_pages(protocol::SqliteGetPagesRequest {
1765					actor_id: self.actor_id.clone().into(),
1766					pgnos: to_fetch.clone(),
1767					expected_generation: None,
1768					expected_head_txid,
1769				})
1770				.instrument(trace_span.clone()),
1771		);
1772		// Only synchronous response handling holds this guard; the transport future above
1773		// carries its span across polls instead of entering a span across an await.
1774		let _trace_guard = trace_span.enter();
1775		let get_pages_duration_ns = get_pages_start.elapsed().as_nanos() as u64;
1776		if let Some(metrics) = &self.metrics {
1777			metrics.observe_get_pages_duration(get_pages_duration_ns);
1778		}
1779		let profile_active = self.operation_profile_active.load(Ordering::Relaxed);
1780		let ordinal = if profile_active {
1781			self.operation_profile
1782				.lock()
1783				.as_ref()
1784				.map_or(1, |profile| profile.get_pages_round_trips.saturating_add(1))
1785		} else {
1786			1
1787		};
1788		let response = match response {
1789			Ok(response) => response,
1790			Err(err) => {
1791				self.update_operation_profile(|profile| {
1792					profile.push_get_pages(
1793						SqliteGetPagesProfile {
1794							ordinal,
1795							duration_ns: get_pages_duration_ns,
1796							demand_requested: missing.len() as u64,
1797							..Default::default()
1798						},
1799						self.profile_request_limit(),
1800					);
1801				});
1802				if let Some(error) = storage_timeout(&err) {
1803					return Err(GetPagesError::Storage(error));
1804				}
1805				return Err(GetPagesError::Other(err.to_string()));
1806			}
1807		};
1808
1809		match response {
1810			protocol::SqliteGetPagesResponse::SqliteGetPagesOk(ok) => {
1811				if profile_active {
1812					let requested = to_fetch.iter().copied().collect::<HashSet<_>>();
1813					let mut request_profile = SqliteGetPagesProfile {
1814						ordinal,
1815						duration_ns: get_pages_duration_ns,
1816						demand_requested: missing.len() as u64,
1817						success: true,
1818						..Default::default()
1819					};
1820					let mut btree_pages = 0_u64;
1821					let mut non_btree_pages = 0_u64;
1822					for fetched in &ok.pages {
1823						if !requested.contains(&fetched.pgno) {
1824							request_profile.overflow_expansion_extra =
1825								request_profile.overflow_expansion_extra.saturating_add(1);
1826						}
1827						if let Some(bytes) = &fetched.bytes {
1828							request_profile.response_present =
1829								request_profile.response_present.saturating_add(1);
1830							request_profile.response_bytes = request_profile
1831								.response_bytes
1832								.saturating_add(bytes.len() as u64);
1833							match classify(fetched.pgno, bytes) {
1834								PageClass::Btree => btree_pages = btree_pages.saturating_add(1),
1835								PageClass::Overflow => {
1836									non_btree_pages = non_btree_pages.saturating_add(1)
1837								}
1838							}
1839						} else {
1840							request_profile.response_missing =
1841								request_profile.response_missing.saturating_add(1);
1842						}
1843					}
1844					self.update_operation_profile(|profile| {
1845						profile.btree_pages = profile.btree_pages.saturating_add(btree_pages);
1846						profile.non_btree_pages =
1847							profile.non_btree_pages.saturating_add(non_btree_pages);
1848						profile.push_get_pages(request_profile, self.profile_request_limit());
1849					});
1850				}
1851				let response_head_txid = ok.head_txid;
1852				if let Some(head_txid) = response_head_txid {
1853					self.state.write().head_txid = Some(head_txid);
1854				}
1855				let missing_pages = missing.iter().copied().collect::<HashSet<_>>();
1856				let prefetch_buffer = self.state.read().prefetch_buffer.clone();
1857				#[cfg(debug_assertions)]
1858				let mut returned_pgnos = HashSet::new();
1859				#[cfg(debug_assertions)]
1860				let mut returned_missing_pages = Vec::new();
1861				#[cfg(debug_assertions)]
1862				let mut returned_missing_in_range_pages = Vec::new();
1863				let mut server_prefetched = 0_u64;
1864				for (ordinal, fetched) in ok.pages.into_iter().enumerate() {
1865					tracing::trace!(target: "sqlite_prefetch::trace", event = "page_received",
1866                        ordinal, pgno = fetched.pgno, present = fetched.bytes.is_some(),
1867                        bytes = fetched.bytes.as_ref().map_or(0, |bytes| bytes.len()),
1868                        demand = missing_pages.contains(&fetched.pgno));
1869					// Missing pages and the synthetic empty header are not delivered data.
1870					let delivered = fetched
1871						.bytes
1872						.as_ref()
1873						.and_then(|bytes| PageTrace::capture(ordinal, fetched.pgno, bytes.len()));
1874					#[cfg(debug_assertions)]
1875					{
1876						returned_pgnos.insert(fetched.pgno);
1877						if fetched.bytes.is_none() {
1878							returned_missing_pages.push(fetched.pgno);
1879							if fetched.pgno <= db_size_pages {
1880								returned_missing_in_range_pages.push(fetched.pgno);
1881							}
1882						}
1883					}
1884					let bytes = if fetched.bytes.is_none()
1885						&& self.commit_total.load(Relaxed) == 0
1886						&& missing_pages.contains(&fetched.pgno)
1887						&& fetched.pgno == 1
1888					{
1889						// An empty file can already have a nonzero inode version. Its
1890						// synthetic SQLite page must retain the storage response's fence.
1891						self.state.write().head_txid = Some(response_head_txid.unwrap_or(0));
1892						Some(PageBytes::from(empty_db_page()))
1893					} else {
1894						fetched.bytes.map(PageBytes::from)
1895					};
1896					if let Some(bytes) = &bytes {
1897						// A page fetched for a partial read is read again whole next, so it
1898						// stays buffered like a read-ahead page for that read.
1899						let kind = if missing_pages.contains(&fetched.pgno) {
1900							if partial {
1901								PageCacheInsertKind::PartialTarget
1902							} else {
1903								PageCacheInsertKind::Target
1904							}
1905						} else {
1906							PageCacheInsertKind::Prefetch
1907						};
1908						if kind == PageCacheInsertKind::Prefetch {
1909							server_prefetched += 1;
1910							if should_cache_page(&self.config, kind, fetched.pgno) {
1911								prefetch_buffer.insert_prefetched_traced(
1912									fetched.pgno,
1913									bytes.clone(),
1914									delivered,
1915								);
1916							} else if let Some(trace) = delivered {
1917								trace.disposition("unused");
1918							}
1919						} else {
1920							// A partial read already consumed these bytes even though a
1921							// copy remains for a later whole-page read.
1922							if let Some(trace) = delivered {
1923								trace.disposition("consumed");
1924							}
1925							cache_page(
1926								&self.config,
1927								&prefetch_buffer,
1928								kind,
1929								fetched.pgno,
1930								bytes.clone(),
1931							);
1932						}
1933					}
1934					resolved.insert(fetched.pgno, bytes);
1935				}
1936				self.prefetch_pages_total
1937					.fetch_add(server_prefetched, Relaxed);
1938				if let Some(metrics) = &self.metrics {
1939					metrics.record_get_pages_request(
1940						to_fetch.len() as u64,
1941						server_prefetched,
1942						page_size as u64,
1943					);
1944				}
1945				#[cfg(debug_assertions)]
1946				{
1947					let absent_response_pages = to_fetch
1948						.iter()
1949						.copied()
1950						.filter(|pgno| !returned_pgnos.contains(pgno))
1951						.collect::<Vec<_>>();
1952					let absent_in_range_pages = absent_response_pages
1953						.iter()
1954						.copied()
1955						.filter(|pgno| *pgno <= db_size_pages)
1956						.collect::<Vec<_>>();
1957					if !returned_missing_in_range_pages.is_empty()
1958						|| !absent_in_range_pages.is_empty()
1959					{
1960						tracing::warn!(
1961							actor_id = %self.actor_id,
1962							requested_pages = ?target_pgnos,
1963							missing_pages = ?missing,
1964							fetch_pages = ?to_fetch,
1965							db_size_pages,
1966							returned_missing_pages = ?returned_missing_pages,
1967							returned_missing_in_range_pages = ?returned_missing_in_range_pages,
1968							absent_response_pages = ?absent_response_pages,
1969							absent_in_range_pages = ?absent_in_range_pages,
1970							"sqlite get_pages returned missing pages within declared db size"
1971						);
1972					}
1973				}
1974				for pgno in missing {
1975					resolved.entry(pgno).or_insert(None);
1976				}
1977				Ok(resolved)
1978			}
1979			protocol::SqliteGetPagesResponse::SqliteErrorResponse(error) => {
1980				self.update_operation_profile(|profile| {
1981					profile.push_get_pages(
1982						SqliteGetPagesProfile {
1983							ordinal,
1984							duration_ns: get_pages_duration_ns,
1985							demand_requested: missing.len() as u64,
1986							..Default::default()
1987						},
1988						self.profile_request_limit(),
1989					);
1990				});
1991				if self.commit_total.load(Relaxed) == 0
1992					&& missing.contains(&1)
1993					&& is_initial_main_page_missing(&error.message)
1994				{
1995					for pgno in missing {
1996						let bytes = if pgno == 1 {
1997							Some(PageBytes::from(empty_db_page()))
1998						} else {
1999							None
2000						};
2001						resolved.entry(pgno).or_insert(bytes);
2002					}
2003					return Ok(resolved);
2004				}
2005				if is_head_fence_mismatch_response(&error) {
2006					return Err(GetPagesError::FenceMismatch(error.message.to_string()));
2007				}
2008				if let Some(refusal) = storage_refusal(&error) {
2009					return Err(GetPagesError::Storage(refusal));
2010				}
2011				Err(GetPagesError::Other(error.message.to_string()))
2012			}
2013		}
2014	}
2015
2016	fn flush_dirty_pages(
2017		&self,
2018	) -> std::result::Result<Option<BufferedCommitOutcome>, CommitBufferError> {
2019		match self.flush_dirty_pages_with_timeout(None)? {
2020			CommitWait::Completed(outcome) => Ok(outcome),
2021			CommitWait::TimedOut => Err(CommitBufferError::Other(
2022				"sqlite commit timed out".to_string(),
2023			)),
2024		}
2025	}
2026
2027	fn flush_dirty_pages_with_timeout(
2028		&self,
2029		timeout: Option<Duration>,
2030	) -> std::result::Result<CommitWait<Option<BufferedCommitOutcome>>, CommitBufferError> {
2031		let total_start = Instant::now();
2032		let request_build_start = Instant::now();
2033		let request = {
2034			let mut state = self.state.write();
2035			if state.dead {
2036				return Err(CommitBufferError::Other(
2037					"sqlite actor lost its fence".to_string(),
2038				));
2039			}
2040			if state.closing {
2041				state.discard_uncommitted_pages();
2042				return Ok(CommitWait::Completed(None));
2043			}
2044			if state.write_buffer.in_atomic_write
2045				|| (state.write_buffer.dirty.is_empty()
2046					&& state.db_size_pages == state.committed_db_size_pages)
2047			{
2048				return Ok(CommitWait::Completed(None));
2049			}
2050			// A corked commit is complete for SQLite once its pages are in the cork.
2051			match state.cork_dirty_pages(self.config.commit_page_cap()) {
2052				CorkOutcome::NotCorked => {}
2053				CorkOutcome::Corked => {
2054					state.refused_write_pending = false;
2055					return Ok(CommitWait::Completed(None));
2056				}
2057				CorkOutcome::Overflow => {
2058					drop(state);
2059					self.commit_cork_alone()?;
2060					let mut state = self.state.write();
2061					return match state.cork_dirty_pages(self.config.commit_page_cap()) {
2062						CorkOutcome::Corked | CorkOutcome::NotCorked => {
2063							state.refused_write_pending = false;
2064							Ok(CommitWait::Completed(None))
2065						}
2066						CorkOutcome::Overflow => Err(self.transaction_too_large_commit_error()),
2067					};
2068				}
2069			}
2070
2071			BufferedCommitRequest {
2072				actor_id: self.actor_id.clone(),
2073				new_db_size_pages: state.db_size_pages,
2074				expected_head_txid: state.head_txid,
2075				dirty_pages: state.write_buffer.snapshot(),
2076			}
2077		};
2078		let request_build_ns = request_build_start.elapsed().as_nanos() as u64;
2079		self.update_operation_profile(|profile| {
2080			profile.dirty_pages = profile
2081				.dirty_pages
2082				.saturating_add(request.dirty_pages.len() as u64);
2083			profile.dirty_bytes = profile.dirty_bytes.saturating_add(
2084				request
2085					.dirty_pages
2086					.iter()
2087					.map(|page| page.bytes.len() as u64)
2088					.sum::<u64>(),
2089			);
2090		});
2091
2092		let (outcome, transport_metrics) =
2093			// Transport rejection, including envoy shutdown while a VFS callback is
2094			// active, becomes CommitBufferError here. xSync and xClose then surface
2095			// it to SQLite as SQLITE_IOERR_*.
2096			match self.block_on_buffered_commit(request.clone(), timeout) {
2097				Ok(CommitWait::Completed(outcome)) => outcome,
2098				Ok(CommitWait::TimedOut) => return Ok(CommitWait::TimedOut),
2099				Err(err) => {
2100					tracing::error!(
2101						actor_id = %self.actor_id,
2102						new_db_size_pages = request.new_db_size_pages,
2103						dirty_pages = request.dirty_pages.len(),
2104						?err,
2105						"sqlite flush commit failed"
2106					);
2107					handle_non_finalize_commit_error(self, &err);
2108					return Err(err);
2109				}
2110			};
2111		self.commit_total
2112			.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2113		if let Some(metrics) = &self.metrics {
2114			metrics.record_commit(request.dirty_pages.len() as u64);
2115		}
2116		tracing::debug!(
2117			dirty_pages = request.dirty_pages.len(),
2118			path = ?outcome.path,
2119			requested_db_size_pages = request.new_db_size_pages,
2120			db_size_pages = outcome.db_size_pages,
2121			request_build_ns,
2122			serialize_ns = transport_metrics.serialize_ns,
2123			transport_ns = transport_metrics.transport_ns,
2124			"vfs commit complete (flush)"
2125		);
2126		#[cfg(debug_assertions)]
2127		{
2128			if outcome.db_size_pages != request.new_db_size_pages {
2129				tracing::warn!(
2130					actor_id = %self.actor_id,
2131					dirty_pages = request.dirty_pages.len(),
2132					path = ?outcome.path,
2133					requested_db_size_pages = request.new_db_size_pages,
2134					outcome_db_size_pages = outcome.db_size_pages,
2135					"sqlite flush commit returned db size different from request"
2136				);
2137			}
2138		}
2139		let state_update_start = Instant::now();
2140		let mut state = self.state.write();
2141		state.db_size_pages = outcome.db_size_pages;
2142		state.committed_db_size_pages = outcome.db_size_pages;
2143		state.head_txid = outcome
2144			.head_txid
2145			.or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
2146		for dirty_page in &request.dirty_pages {
2147			state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
2148		}
2149		state.write_buffer.dirty.clear();
2150		state.refused_write_pending = false;
2151		let state_update_ns = state_update_start.elapsed().as_nanos() as u64;
2152		self.add_commit_phase_metrics(
2153			request_build_ns,
2154			transport_metrics,
2155			state_update_ns,
2156			total_start.elapsed().as_nanos() as u64,
2157		);
2158		Ok(CommitWait::Completed(Some(outcome)))
2159	}
2160
2161	fn commit_atomic_write(&self) -> std::result::Result<(), CommitBufferError> {
2162		match self.commit_atomic_write_with_timeout(None)? {
2163			CommitWait::Completed(()) => Ok(()),
2164			CommitWait::TimedOut => Err(CommitBufferError::Other(
2165				"sqlite commit timed out".to_string(),
2166			)),
2167		}
2168	}
2169
2170	fn commit_atomic_write_with_timeout(
2171		&self,
2172		timeout: Option<Duration>,
2173	) -> std::result::Result<CommitWait<()>, CommitBufferError> {
2174		let total_start = Instant::now();
2175		let request_build_start = Instant::now();
2176		let request = {
2177			let mut state = self.state.write();
2178			if state.dead {
2179				return Err(CommitBufferError::Other(
2180					"sqlite actor lost its fence".to_string(),
2181				));
2182			}
2183			if !state.write_buffer.in_atomic_write {
2184				return Ok(CommitWait::Completed(()));
2185			}
2186			if state.closing {
2187				state.discard_uncommitted_pages();
2188				return Ok(CommitWait::Completed(()));
2189			}
2190			if state.write_buffer.dirty.is_empty()
2191				&& state.db_size_pages == state.committed_db_size_pages
2192			{
2193				state.write_buffer.in_atomic_write = false;
2194				return Ok(CommitWait::Completed(()));
2195			}
2196			match state.cork_dirty_pages(self.config.commit_page_cap()) {
2197				CorkOutcome::NotCorked => {}
2198				CorkOutcome::Corked => {
2199					state.write_buffer.in_atomic_write = false;
2200					state.refused_write_pending = false;
2201					return Ok(CommitWait::Completed(()));
2202				}
2203				CorkOutcome::Overflow => {
2204					drop(state);
2205					self.commit_cork_alone()?;
2206					let mut state = self.state.write();
2207					return match state.cork_dirty_pages(self.config.commit_page_cap()) {
2208						CorkOutcome::Corked | CorkOutcome::NotCorked => {
2209							state.write_buffer.in_atomic_write = false;
2210							state.refused_write_pending = false;
2211							Ok(CommitWait::Completed(()))
2212						}
2213						CorkOutcome::Overflow => Err(self.transaction_too_large_commit_error()),
2214					};
2215				}
2216			}
2217
2218			BufferedCommitRequest {
2219				actor_id: self.actor_id.clone(),
2220				new_db_size_pages: state.db_size_pages,
2221				expected_head_txid: state.head_txid,
2222				dirty_pages: state.write_buffer.snapshot(),
2223			}
2224		};
2225		let request_build_ns = request_build_start.elapsed().as_nanos() as u64;
2226		self.update_operation_profile(|profile| {
2227			profile.dirty_pages = profile
2228				.dirty_pages
2229				.saturating_add(request.dirty_pages.len() as u64);
2230			profile.dirty_bytes = profile.dirty_bytes.saturating_add(
2231				request
2232					.dirty_pages
2233					.iter()
2234					.map(|page| page.bytes.len() as u64)
2235					.sum::<u64>(),
2236			);
2237		});
2238
2239		let (outcome, transport_metrics) =
2240			match self.block_on_buffered_commit(request.clone(), timeout) {
2241				Ok(CommitWait::Completed(outcome)) => outcome,
2242				Ok(CommitWait::TimedOut) => return Ok(CommitWait::TimedOut),
2243				Err(err) => {
2244					tracing::error!(
2245						actor_id = %self.actor_id,
2246						new_db_size_pages = request.new_db_size_pages,
2247						dirty_pages = request.dirty_pages.len(),
2248						?err,
2249						"sqlite atomic commit failed"
2250					);
2251					handle_non_finalize_commit_error(self, &err);
2252					return Err(err);
2253				}
2254			};
2255		self.commit_total
2256			.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2257		if let Some(metrics) = &self.metrics {
2258			metrics.record_commit(request.dirty_pages.len() as u64);
2259		}
2260		tracing::debug!(
2261			dirty_pages = request.dirty_pages.len(),
2262			path = ?outcome.path,
2263			requested_db_size_pages = request.new_db_size_pages,
2264			db_size_pages = outcome.db_size_pages,
2265			request_build_ns,
2266			serialize_ns = transport_metrics.serialize_ns,
2267			transport_ns = transport_metrics.transport_ns,
2268			"vfs commit complete (atomic)"
2269		);
2270		#[cfg(debug_assertions)]
2271		{
2272			if outcome.db_size_pages != request.new_db_size_pages {
2273				tracing::warn!(
2274					actor_id = %self.actor_id,
2275					dirty_pages = request.dirty_pages.len(),
2276					path = ?outcome.path,
2277					requested_db_size_pages = request.new_db_size_pages,
2278					outcome_db_size_pages = outcome.db_size_pages,
2279					"sqlite atomic commit returned db size different from request"
2280				);
2281			}
2282		}
2283		self.clear_last_error();
2284		let state_update_start = Instant::now();
2285		let mut state = self.state.write();
2286		state.db_size_pages = outcome.db_size_pages;
2287		state.committed_db_size_pages = outcome.db_size_pages;
2288		state.head_txid = outcome
2289			.head_txid
2290			.or_else(|| state.head_txid.map(|head_txid| head_txid.saturating_add(1)));
2291		for dirty_page in &request.dirty_pages {
2292			state.note_committed_page(&self.config, dirty_page.pgno, &dirty_page.bytes);
2293		}
2294		state.write_buffer.dirty.clear();
2295		state.write_buffer.in_atomic_write = false;
2296		state.refused_write_pending = false;
2297		let state_update_ns = state_update_start.elapsed().as_nanos() as u64;
2298		self.add_commit_phase_metrics(
2299			request_build_ns,
2300			transport_metrics,
2301			state_update_ns,
2302			total_start.elapsed().as_nanos() as u64,
2303		);
2304		Ok(CommitWait::Completed(()))
2305	}
2306
2307	/// Returns true when the truncate left behind a size change that only a commit can carry to
2308	/// depot.
2309	///
2310	/// SQLite's batch-atomic commit path truncates *after* `SQLITE_FCNTL_COMMIT_ATOMIC_WRITE` and
2311	/// does not sync afterwards, so a shrink recorded only in memory here never reaches depot. When
2312	/// the write buffer is empty the caller has to issue a size-only commit. When it is not, the
2313	/// buffered pages carry the new size on the commit that follows, and committing here would
2314	/// publish a transaction SQLite has not finished.
2315	fn truncate_main_file(&self, size: sqlite3_int64) -> bool {
2316		let page_size = self.page_size() as i64;
2317		let truncated_pages = ((size + page_size - 1) / page_size) as u32;
2318		let mut state = self.state.write();
2319		state.db_size_pages = truncated_pages;
2320		state
2321			.write_buffer
2322			.dirty
2323			.retain(|pgno, _| *pgno <= truncated_pages);
2324		let between_transactions =
2325			!state.write_buffer.in_atomic_write && state.write_buffer.dirty.is_empty();
2326		if let Some(corked) = state.corked.as_mut() {
2327			corked.retain(|pgno, _| *pgno <= truncated_pages);
2328		}
2329		// A shrink between transactions belongs to the last corked one.
2330		if state.corked.is_some() && between_transactions {
2331			state.corked_db_size_pages = truncated_pages;
2332		}
2333		state.invalidate_page_buffers();
2334		// A corked size change rides the uncorking commit.
2335		state.corked.is_none()
2336			&& !state.write_buffer.in_atomic_write
2337			&& state.write_buffer.dirty.is_empty()
2338			&& state.db_size_pages != state.committed_db_size_pages
2339	}
2340
2341	/// Corks commits: until `uncork`, each transaction SQLite commits stays in memory instead
2342	/// of going to depot. SQLite and its readers see the transactions as committed; none is
2343	/// durable until uncorking commits them all at once.
2344	fn cork(&self) {
2345		let mut state = self.state.write();
2346		if state.corked.is_none() {
2347			state.corked = Some(BTreeMap::new());
2348			// A cork inside a transaction starts from the committed size, not the size the
2349			// open transaction's spilled pages reached, so a close that discards the
2350			// transaction does not publish its growth.
2351			state.corked_db_size_pages = state.committed_db_size_pages;
2352		}
2353	}
2354
2355	/// Sends every transaction committed while corked to depot as one commit, then commits
2356	/// directly again. It must run between transactions, and fails without uncorking inside
2357	/// one. On a failed commit the pages are not durable and the database is unusable, as
2358	/// after any failed commit.
2359	fn uncork(&self) -> std::result::Result<(), CommitBufferError> {
2360		{
2361			let mut state = self.state.write();
2362			if state.corked.is_none() {
2363				return Ok(());
2364			}
2365			if state.write_buffer.in_atomic_write || !state.write_buffer.dirty.is_empty() {
2366				return Err(CommitBufferError::Other(
2367					"cannot uncork sqlite commits inside a transaction".to_string(),
2368				));
2369			}
2370			state.write_buffer.dirty = state.corked.take().unwrap_or_default();
2371		}
2372		self.flush_dirty_pages().map(|_| ())
2373	}
2374}
2375
2376impl Drop for VfsContext {
2377	fn drop(&mut self) {
2378		self.state.write().invalidate_page_buffers();
2379		self.report_prefetch_unused();
2380		let state = self.state.read();
2381		let page_cache_entries = state
2382			.prefetch_buffer
2383			.entry_count()
2384			.saturating_add(state.committed_buffer.entry_count());
2385		let page_cache_weighted_size = state.prefetch_buffer.weighted_size();
2386		tracing::debug!(
2387			actor_id = %self.actor_id,
2388			generation = ?self.generation,
2389			resolve_pages_calls = self.resolve_pages_total.load(Ordering::Relaxed),
2390			resolve_pages_cache_hits = self.resolve_pages_cache_hits.load(Ordering::Relaxed),
2391			get_pages_round_trips = self.resolve_pages_fetches.load(Ordering::Relaxed),
2392			pages_fetched_total = self.pages_fetched_total.load(Ordering::Relaxed),
2393			prefetch_pages_total = self.prefetch_pages_total.load(Ordering::Relaxed),
2394			prefetch_consumed_total = self.prefetch_consumed_total.load(Ordering::Relaxed),
2395			commit_count = self.commit_total.load(Ordering::Relaxed),
2396			db_size_pages = state.db_size_pages,
2397			head_txid = state.head_txid,
2398			page_cache_entries,
2399			page_cache_weighted_size,
2400			page_cache_capacity_pages = self.config.page_budget.capacity_pages(),
2401			"sqlite vfs close summary"
2402		);
2403	}
2404}
2405
2406fn cleanup_batch_atomic_probe(db: *mut sqlite3) {
2407	if let Err(err) = sqlite_exec(db, "DROP TABLE IF EXISTS __rivet_batch_probe;") {
2408		tracing::warn!(%err, "failed to clean up sqlite batch atomic probe table");
2409	}
2410}
2411
2412fn assert_batch_atomic_probe(db: *mut sqlite3, vfs: &SqliteVfs) -> std::result::Result<(), String> {
2413	let commit_atomic_before = vfs.commit_atomic_count();
2414	let probe_sql = "\
2415		BEGIN IMMEDIATE;\
2416		CREATE TABLE IF NOT EXISTS __rivet_batch_probe(x INTEGER);\
2417		INSERT INTO __rivet_batch_probe VALUES(1);\
2418		DELETE FROM __rivet_batch_probe;\
2419		DROP TABLE IF EXISTS __rivet_batch_probe;\
2420		COMMIT;\
2421	";
2422
2423	if let Err(err) = sqlite_exec(db, probe_sql) {
2424		let last_error = vfs.clone_last_error();
2425		tracing::error!(
2426			%err,
2427			last_error = ?last_error,
2428			commit_atomic_before,
2429			"sqlite batch atomic probe failed"
2430		);
2431		cleanup_batch_atomic_probe(db);
2432		if let Some(last_error) = last_error {
2433			return Err(format!(
2434				"batch atomic probe failed: {err}; vfs last_error: {last_error}"
2435			));
2436		}
2437		return Err(format!("batch atomic probe failed: {err}"));
2438	}
2439
2440	let commit_atomic_after = vfs.commit_atomic_count();
2441	if commit_atomic_after == commit_atomic_before {
2442		tracing::error!(
2443			commit_atomic_before,
2444			commit_atomic_after,
2445			last_error = ?vfs.clone_last_error(),
2446			"batch atomic writes not active for sqlite, SQLITE_ENABLE_BATCH_ATOMIC_WRITE may be missing"
2447		);
2448		cleanup_batch_atomic_probe(db);
2449		return Err(
2450			"batch atomic writes not active for sqlite, SQLITE_ENABLE_BATCH_ATOMIC_WRITE may be missing"
2451				.to_string(),
2452		);
2453	}
2454
2455	Ok(())
2456}
2457
2458/// Ephemeral commit sink used only by the process-startup batch-atomic probe.
2459///
2460/// The probe transaction is intentionally small enough to use a single-shot commit. Pages only
2461/// need to remain visible inside that transaction, where the VFS write buffer owns them, so the
2462/// sink does not need to retain committed data.
2463struct BatchAtomicProbeTransport;
2464
2465#[async_trait]
2466impl SqliteTransport for BatchAtomicProbeTransport {
2467	async fn get_pages(
2468		&self,
2469		request: protocol::SqliteGetPagesRequest,
2470	) -> Result<protocol::SqliteGetPagesResponse> {
2471		Ok(protocol::SqliteGetPagesResponse::SqliteGetPagesOk(
2472			protocol::SqliteGetPagesOk {
2473				pages: request
2474					.pgnos
2475					.into_iter()
2476					.map(|pgno| protocol::SqliteFetchedPage { pgno, bytes: None })
2477					.collect(),
2478				head_txid: Some(0),
2479			},
2480		))
2481	}
2482
2483	async fn commit(
2484		&self,
2485		_request: protocol::SqliteCommitRequest,
2486	) -> Result<protocol::SqliteCommitResponse> {
2487		Ok(protocol::SqliteCommitResponse::SqliteCommitOk(
2488			protocol::SqliteCommitOk { head_txid: None },
2489		))
2490	}
2491
2492	async fn commit_stage_begin(
2493		&self,
2494		_request: protocol::SqliteCommitStageBeginRequest,
2495	) -> Result<protocol::SqliteCommitStageBeginResponse> {
2496		anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2497	}
2498
2499	async fn commit_stage_segment(
2500		&self,
2501		_request: protocol::SqliteCommitStageSegmentRequest,
2502	) -> Result<protocol::SqliteCommitStageSegmentResponse> {
2503		anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2504	}
2505
2506	async fn commit_finalize(
2507		&self,
2508		_request: protocol::SqliteCommitFinalizeRequest,
2509	) -> Result<protocol::SqliteCommitFinalizeResponse> {
2510		anyhow::bail!("batch-atomic startup probe unexpectedly attempted a staged commit")
2511	}
2512}
2513
2514fn verify_batch_atomic_writes_at_process_startup_blocking(
2515	runtime: Handle,
2516	config: VfsConfig,
2517) -> std::result::Result<(), String> {
2518	let probe_id = NEXT_TEMP_AUX_ID.fetch_add(1, Ordering::Relaxed);
2519	let file_name = format!("batch-atomic-startup-probe-{probe_id}.db");
2520	let vfs = SqliteVfs::register_with_transport_and_initial_pages(
2521		&format!("batch-atomic-startup-probe-{probe_id}"),
2522		Arc::new(BatchAtomicProbeTransport),
2523		file_name.clone(),
2524		runtime,
2525		config,
2526		InitialPages::default(),
2527		None,
2528	)?;
2529	let connection = open_connection(
2530		Arc::new(vfs),
2531		&file_name,
2532		SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
2533	)?;
2534	configure_connection_for_database(connection.as_ptr(), &connection._vfs, &file_name)
2535		.map_err(|error| error.to_string())?;
2536	assert_batch_atomic_probe(connection.as_ptr(), &connection._vfs)
2537}
2538
2539/// Verifies the linked SQLite pager and the production VFS batch-atomic callbacks before this
2540/// process becomes eligible to run actors.
2541pub async fn verify_batch_atomic_writes_at_process_startup() -> Result<()> {
2542	verify_batch_atomic_writes_at_process_startup_with_config(VfsConfig::default()).await
2543}
2544
2545async fn verify_batch_atomic_writes_at_process_startup_with_config(
2546	config: VfsConfig,
2547) -> Result<()> {
2548	let runtime = Handle::try_current().map_err(|err| {
2549		anyhow::anyhow!("batch-atomic startup probe requires a Tokio runtime: {err}")
2550	})?;
2551	let result = tokio::task::spawn_blocking(move || {
2552		verify_batch_atomic_writes_at_process_startup_blocking(runtime, config)
2553	})
2554	.await
2555	.map_err(|err| anyhow::anyhow!("batch-atomic startup probe task failed: {err}"))?;
2556	result.map_err(anyhow::Error::msg)
2557}
2558
2559fn handle_non_finalize_commit_error(ctx: &VfsContext, err: &CommitBufferError) {
2560	match err {
2561		CommitBufferError::FenceMismatch(message) => ctx.mark_fatal(message.clone()),
2562		CommitBufferError::Indeterminate(operation) => {
2563			ctx.mark_indeterminate(operation.clone());
2564		}
2565		CommitBufferError::Storage(error) => ctx.set_storage_error(error.clone()),
2566		CommitBufferError::Other(message) => ctx.set_last_error(message.clone()),
2567	}
2568}
2569
2570fn handle_finalize_fence_error(ctx: &VfsContext, err: &CommitBufferError) {
2571	if let CommitBufferError::FenceMismatch(reason) = err {
2572		ctx.mark_fatal(reason.clone());
2573	}
2574}
2575
2576
2577
2578pub(crate) async fn fetch_initial_pages_for_registration(
2579	transport: SqliteTransportHandle,
2580	actor_id: &str,
2581	generation: u64,
2582	_config: &VfsConfig,
2583) -> std::result::Result<InitialPages, String> {
2584	fetch_initial_pages(transport, actor_id.to_string(), generation, 1).await
2585}
2586
2587async fn fetch_initial_pages(
2588	transport: SqliteTransportHandle,
2589	actor_id: String,
2590	generation: u64,
2591	page_count: u32,
2592) -> std::result::Result<InitialPages, String> {
2593	tracing::debug!(
2594		actor_id = %actor_id,
2595		generation,
2596		page_count,
2597		"sqlite initial page preload request"
2598	);
2599
2600	let request_actor_id = actor_id.clone();
2601	let response = transport
2602		.get_pages(protocol::SqliteGetPagesRequest {
2603			actor_id: request_actor_id.into(),
2604			pgnos: (1..=page_count).collect(),
2605			expected_generation: None,
2606			expected_head_txid: None,
2607		})
2608		.await;
2609
2610	match response {
2611		Ok(protocol::SqliteGetPagesResponse::SqliteGetPagesOk(ok)) => {
2612			let head_txid = ok.head_txid;
2613			let pages: Vec<_> = ok
2614				.pages
2615				.into_iter()
2616				.filter_map(|page| page.bytes.map(|bytes| (page.pgno, bytes.into())))
2617				.collect();
2618			tracing::debug!(
2619				actor_id = %actor_id,
2620				generation,
2621				page_count,
2622				loaded_pages = pages.len(),
2623				head_txid,
2624				"sqlite initial page preload result"
2625			);
2626			Ok(InitialPages {
2627				pages,
2628				head_txid,
2629				requested_page_count: page_count,
2630			})
2631		}
2632		Ok(protocol::SqliteGetPagesResponse::SqliteErrorResponse(error)) => {
2633			if !is_initial_main_page_missing(&error.message) {
2634				return Err(format!(
2635					"sqlite initial page fetch failed: {}",
2636					error.message
2637				));
2638			}
2639			tracing::info!(
2640				actor_id = %actor_id,
2641				generation,
2642				page_count,
2643				error = %error.message,
2644				"sqlite initial page fetch did not find persisted data"
2645			);
2646			Ok(InitialPages {
2647				pages: Vec::new(),
2648				head_txid: Some(0),
2649				requested_page_count: page_count,
2650			})
2651		}
2652		Err(err) => Err(format!("sqlite initial page fetch failed: {err}")),
2653	}
2654}
2655
2656fn is_initial_main_page_missing(message: &str) -> bool {
2657	message.contains("sqlite database was not found in this bucket branch")
2658		|| message.contains("sqlite meta missing for get_pages")
2659		|| message == "actor does not exist"
2660}
2661
2662fn next_temp_aux_path() -> String {
2663	format!(
2664		"{TEMP_AUX_PATH_PREFIX}-{}",
2665		NEXT_TEMP_AUX_ID.fetch_add(1, Ordering::Relaxed)
2666	)
2667}
2668
2669unsafe fn get_aux_state(file: &VfsFile) -> Option<&AuxFileHandle> {
2670	(!file.aux.is_null()).then(|| &*file.aux)
2671}
2672
2673/// Dirty pages above which a commit is staged in segments rather than sent as one message.
2674///
2675/// Matches the engine's single-shot cap. A commit at or under it fits one FDB transaction with its
2676/// page bytes included, so it takes the single round trip; anything larger has to be staged because
2677/// the engine would reject it, not merely because staging is faster.
2678///
2679/// Kept as its own value rather than imported, since the engine is free to lower its cap and this
2680/// side must keep working against an engine on either side of that change. Sending a single-shot
2681/// commit the engine refuses fails the commit, while staging one it would have accepted only costs
2682/// extra round trips.
2683const COMMIT_STAGE_THRESHOLD_PAGES: usize = 320;
2684
2685async fn commit_buffered_pages(
2686	transport: &dyn SqliteTransport,
2687	request: BufferedCommitRequest,
2688	max_dirty_pages: usize,
2689) -> std::result::Result<(BufferedCommitOutcome, CommitTransportMetrics), CommitBufferError> {
2690	// Refused before sending rather than discovered partway through. The engine enforces the same
2691	// cap and is the authority, but finding out from it means staging up to 128 MiB of segments
2692	// first, and the bytes already staged are only reclaimed when some later commit reopens the txid.
2693	if request.dirty_pages.len() > max_dirty_pages {
2694		return Err(CommitBufferError::Other(format!(
2695			"commit of {} dirty pages exceeds the {} page maximum",
2696			request.dirty_pages.len(),
2697			max_dirty_pages,
2698		)));
2699	}
2700
2701	if request.dirty_pages.len() > COMMIT_STAGE_THRESHOLD_PAGES {
2702		return commit_staged_pages(transport, request).await;
2703	}
2704
2705	let mut metrics = CommitTransportMetrics::default();
2706	let serialize_start = Instant::now();
2707	let BufferedCommitRequest {
2708		actor_id,
2709		new_db_size_pages,
2710		dirty_pages,
2711		expected_head_txid,
2712	} = request;
2713	let commit_request = protocol::SqliteCommitRequest {
2714		actor_id: actor_id.into(),
2715		dirty_pages: dirty_pages
2716			.into_iter()
2717			.map(|page| protocol::SqliteDirtyPage {
2718				pgno: page.pgno,
2719				bytes: page.bytes.into_bytes(),
2720			})
2721			.collect(),
2722		db_size_pages: new_db_size_pages,
2723		now_ms: sqlite_now_ms().map_err(|err| CommitBufferError::Other(err.to_string()))?,
2724		expected_generation: None,
2725		expected_head_txid,
2726	};
2727	metrics.serialize_ns += serialize_start.elapsed().as_nanos() as u64;
2728	let transport_start = Instant::now();
2729	match transport
2730		.commit(commit_request)
2731		.await
2732		.map_err(commit_transport_error)?
2733	{
2734		protocol::SqliteCommitResponse::SqliteCommitOk(ok) => {
2735			metrics.transport_ns += transport_start.elapsed().as_nanos() as u64;
2736			Ok((
2737				BufferedCommitOutcome {
2738					path: CommitPath::Fast,
2739					db_size_pages: new_db_size_pages,
2740					head_txid: ok.head_txid,
2741				},
2742				metrics,
2743			))
2744		}
2745		protocol::SqliteCommitResponse::SqliteErrorResponse(error) => {
2746			if is_head_fence_mismatch_response(&error) {
2747				Err(CommitBufferError::FenceMismatch(error.message.to_string()))
2748			} else if let Some(refusal) = storage_refusal(&error) {
2749				Err(CommitBufferError::Storage(refusal))
2750			} else {
2751				Err(CommitBufferError::Other(error.message.to_string()))
2752			}
2753		}
2754	}
2755}
2756
2757/// Commits pages too numerous for one message by staging them in shard-aligned segments.
2758///
2759/// The whole commit fails on any error, exactly as the single-shot path does: SQLite sees one failed
2760/// commit, never a partial one. Nothing staged is visible until finalize, so an abandoned attempt
2761/// leaves the database exactly as it was, and the engine reclaims the staged bytes when the next
2762/// attempt reopens the same txid.
2763async fn commit_staged_pages(
2764	transport: &dyn SqliteTransport,
2765	request: BufferedCommitRequest,
2766) -> std::result::Result<(BufferedCommitOutcome, CommitTransportMetrics), CommitBufferError> {
2767	let mut metrics = CommitTransportMetrics::default();
2768	let serialize_start = Instant::now();
2769	let mut dirty_pages = request.dirty_pages;
2770	// Cutting segments needs ascending pages, and sorting once here keeps the per-segment work to a
2771	// slice.
2772	dirty_pages.sort_by_key(|page| page.pgno);
2773	metrics.serialize_ns += serialize_start.elapsed().as_nanos() as u64;
2774
2775	let transport_start = Instant::now();
2776	let txid = match transport
2777		.commit_stage_begin(protocol::SqliteCommitStageBeginRequest {
2778			actor_id: request.actor_id.clone().into(),
2779			expected_generation: None,
2780			expected_head_txid: request.expected_head_txid,
2781		})
2782		.await
2783		.map_err(commit_transport_error)?
2784	{
2785		protocol::SqliteCommitStageBeginResponse::SqliteCommitStageBeginOk(ok) => ok.txid,
2786		protocol::SqliteCommitStageBeginResponse::SqliteErrorResponse(error) => {
2787			return Err(staged_commit_error(error));
2788		}
2789	};
2790
2791	let segments = sqlite_types::cut_page_segments(&dirty_pages, |page| page.pgno);
2792	let segment_first_pgnos = segments.iter().map(|(first_pgno, _)| *first_pgno).collect();
2793	// Each segment spans at most five shards. Buffered requests cap their combined page bytes,
2794	// while ordered responses preserve the first failure and finalize waits for every admission.
2795	let responses = stream::iter(segments)
2796		.map(|(first_pgno, segment)| {
2797			let dirty_pages = segment
2798				.iter()
2799				.map(|page| protocol::SqliteDirtyPage {
2800					pgno: page.pgno,
2801					bytes: page.bytes.clone().into_bytes(),
2802				})
2803				.collect();
2804			let actor_id = request.actor_id.clone();
2805			async move {
2806				match transport
2807					.commit_stage_segment(protocol::SqliteCommitStageSegmentRequest {
2808						actor_id: actor_id.into(),
2809						expected_generation: None,
2810						txid,
2811						first_pgno,
2812						dirty_pages,
2813					})
2814					.await
2815					.map_err(commit_transport_error)?
2816				{
2817					protocol::SqliteCommitStageSegmentResponse::SqliteCommitStageSegmentOk(_) => {
2818						Ok(())
2819					}
2820					protocol::SqliteCommitStageSegmentResponse::SqliteErrorResponse(error) => {
2821						Err(staged_commit_error(error))
2822					}
2823				}
2824			}
2825		})
2826		.buffered(STAGE_PIPELINE_DEPTH)
2827		.collect::<Vec<_>>()
2828		.await;
2829	for response in responses {
2830		response?;
2831	}
2832
2833	let head_txid = match transport
2834		.commit_finalize(protocol::SqliteCommitFinalizeRequest {
2835			actor_id: request.actor_id.clone().into(),
2836			expected_generation: None,
2837			txid,
2838			new_db_size_pages: request.new_db_size_pages,
2839			now_ms: sqlite_now_ms().map_err(|err| CommitBufferError::Other(err.to_string()))?,
2840			segment_first_pgnos,
2841		})
2842		.await
2843		.map_err(commit_transport_error)?
2844	{
2845		protocol::SqliteCommitFinalizeResponse::SqliteCommitFinalizeOk(ok) => ok.head_txid,
2846		protocol::SqliteCommitFinalizeResponse::SqliteErrorResponse(error) => {
2847			return Err(staged_commit_error(error));
2848		}
2849	};
2850	metrics.transport_ns += transport_start.elapsed().as_nanos() as u64;
2851
2852	Ok((
2853		BufferedCommitOutcome {
2854			path: CommitPath::Staged,
2855			db_size_pages: request.new_db_size_pages,
2856			head_txid,
2857		},
2858		metrics,
2859	))
2860}
2861
2862fn commit_transport_error(error: anyhow::Error) -> CommitBufferError {
2863	if let Some(timeout) = storage_timeout(&error) {
2864		return CommitBufferError::Storage(timeout);
2865	}
2866	match error.downcast::<SqliteCommitIndeterminateError>() {
2867		Ok(error) => CommitBufferError::Indeterminate(error.0),
2868		Err(error) => CommitBufferError::Other(error.to_string()),
2869	}
2870}
2871
2872fn staged_commit_error(error: protocol::SqliteErrorResponse) -> CommitBufferError {
2873	if is_head_fence_mismatch_response(&error) {
2874		CommitBufferError::FenceMismatch(error.message.to_string())
2875	} else if let Some(refusal) = storage_refusal(&error) {
2876		CommitBufferError::Storage(refusal)
2877	} else {
2878		CommitBufferError::Other(error.message.to_string())
2879	}
2880}
2881
2882fn is_head_fence_mismatch_response(error: &protocol::SqliteErrorResponse) -> bool {
2883	is_head_fence_mismatch(&error.group, &error.code)
2884}
2885
2886unsafe fn get_file(p: *mut sqlite3_file) -> &'static mut VfsFile {
2887	&mut *(p as *mut VfsFile)
2888}
2889
2890unsafe fn get_vfs_ctx(p: *mut sqlite3_vfs) -> &'static VfsContext {
2891	&*((*p).pAppData as *const VfsContext)
2892}
2893
2894fn sqlite_error_message(db: *mut sqlite3) -> String {
2895	unsafe {
2896		if db.is_null() {
2897			"unknown sqlite error".to_string()
2898		} else {
2899			CStr::from_ptr(sqlite3_errmsg(db))
2900				.to_string_lossy()
2901				.into_owned()
2902		}
2903	}
2904}
2905
2906pub(crate) fn sqlite_exec(db: *mut sqlite3, sql: &str) -> std::result::Result<(), String> {
2907	let c_sql = CString::new(sql).map_err(|err| err.to_string())?;
2908	let rc = unsafe { sqlite3_exec(db, c_sql.as_ptr(), None, ptr::null_mut(), ptr::null_mut()) };
2909	if rc != SQLITE_OK {
2910		return Err(format!(
2911			"`{sql}` failed with code {rc}: {}",
2912			sqlite_error_message(db)
2913		));
2914	}
2915	Ok(())
2916}
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936fn page_span(offset: i64, length: usize, page_size: usize) -> std::result::Result<Vec<u32>, ()> {
2937	if offset < 0 {
2938		return Err(());
2939	}
2940	if length == 0 {
2941		return Ok(Vec::new());
2942	}
2943
2944	let start = offset as usize / page_size + 1;
2945	let end = (offset as usize + length - 1) / page_size + 1;
2946	Ok((start as u32..=end as u32).collect())
2947}
2948
2949unsafe extern "C" fn io_close(p_file: *mut sqlite3_file) -> c_int {
2950	vfs_catch_unwind!(SQLITE_IOERR, {
2951		if p_file.is_null() {
2952			return SQLITE_OK;
2953		}
2954		let file = get_file(p_file);
2955		let result = if !file.aux.is_null() {
2956			let aux = Box::from_raw(file.aux);
2957			if aux.delete_on_close {
2958				let ctx = &*file.ctx;
2959				ctx.delete_aux_file(&aux.path);
2960			}
2961			file.aux = ptr::null_mut();
2962			Ok(())
2963		} else {
2964			let ctx = &*file.ctx;
2965			
2966			ctx.discard_refused_transaction();
2967			let should_flush = {
2968				let state = ctx.state.read();
2969				state.write_buffer.in_atomic_write
2970					|| !state.write_buffer.dirty.is_empty()
2971					|| state.db_size_pages != state.committed_db_size_pages
2972					|| state.corked.is_some()
2973			};
2974			if should_flush {
2975				if ctx.state.read().write_buffer.in_atomic_write {
2976					ctx.commit_atomic_write().map(|_| ())
2977				} else {
2978					ctx.uncork()
2979						.and_then(|()| ctx.flush_dirty_pages().map(|_| ()))
2980				}
2981			} else {
2982				Ok(())
2983			}
2984		};
2985		file.base.pMethods = ptr::null();
2986		match result {
2987			Ok(()) => SQLITE_OK,
2988			Err(err) => {
2989				let ctx = &*file.ctx;
2990				handle_finalize_fence_error(ctx, &err);
2991				SQLITE_IOERR
2992			}
2993		}
2994	})
2995}
2996
2997unsafe extern "C" fn io_read(
2998	p_file: *mut sqlite3_file,
2999	p_buf: *mut c_void,
3000	i_amt: c_int,
3001	i_offset: sqlite3_int64,
3002) -> c_int {
3003	vfs_catch_unwind!(SQLITE_IOERR_READ, {
3004		if i_amt <= 0 {
3005			return SQLITE_OK;
3006		}
3007
3008		let file = get_file(p_file);
3009		if let Some(aux) = get_aux_state(file) {
3010			if i_offset < 0 {
3011				return SQLITE_IOERR_READ;
3012			}
3013
3014			let offset = i_offset as usize;
3015			let requested = i_amt as usize;
3016			let buf = slice::from_raw_parts_mut(p_buf.cast::<u8>(), requested);
3017			buf.fill(0);
3018
3019			let bytes = aux.state.bytes.lock();
3020			if offset >= bytes.len() {
3021				return SQLITE_IOERR_SHORT_READ;
3022			}
3023
3024			let copy_len = requested.min(bytes.len() - offset);
3025			buf[..copy_len].copy_from_slice(&bytes[offset..offset + copy_len]);
3026			return if copy_len < requested {
3027				SQLITE_IOERR_SHORT_READ
3028			} else {
3029				SQLITE_OK
3030			};
3031		}
3032
3033		let ctx = &*file.ctx;
3034		if ctx.is_dead() {
3035			return SQLITE_IOERR_READ;
3036		}
3037
3038		let buf = slice::from_raw_parts_mut(p_buf.cast::<u8>(), i_amt as usize);
3039		let requested_pages = match page_span(i_offset, i_amt as usize, ctx.page_size()) {
3040			Ok(pages) => pages,
3041			Err(_) => return SQLITE_IOERR_READ,
3042		};
3043		let page_size = ctx.page_size();
3044		let (file_size, db_size_pages) = {
3045			let state = ctx.state.read();
3046			(
3047				state.db_size_pages as usize * state.page_size,
3048				state.db_size_pages,
3049			)
3050		};
3051
3052		let partial = i_offset as usize % page_size != 0 || i_amt as usize % page_size != 0;
3053		let resolved = match ctx.resolve_pages(&requested_pages, partial) {
3054			Ok(pages) => pages,
3055			Err(GetPagesError::FenceMismatch(message)) => {
3056				tracing::error!(
3057					actor_id = %ctx.actor_id,
3058					requested_pages = ?requested_pages,
3059					error = %message,
3060					"sqlite xRead hit fatal sqlite error"
3061				);
3062				ctx.mark_fatal(message);
3063				return SQLITE_IOERR_READ;
3064			}
3065			Err(GetPagesError::Storage(error)) => {
3066				tracing::warn!(
3067					actor_id = %ctx.actor_id,
3068					requested_pages = ?requested_pages,
3069					%error,
3070					"sqlite xRead failed without changing storage; the statement fails with a typed storage error"
3071				);
3072				ctx.set_storage_error(error);
3073				return SQLITE_IOERR_READ;
3074			}
3075			Err(GetPagesError::Other(message)) => {
3076				tracing::error!(
3077					actor_id = %ctx.actor_id,
3078					requested_pages = ?requested_pages,
3079					error = %message,
3080					"sqlite xRead failed to resolve pages"
3081				);
3082				ctx.set_last_error(message);
3083				return SQLITE_IOERR_READ;
3084			}
3085		};
3086		ctx.clear_last_error();
3087
3088		#[cfg(debug_assertions)]
3089		{
3090			let missing_in_range_pages = requested_pages
3091				.iter()
3092				.copied()
3093				.filter(|pgno| *pgno <= db_size_pages)
3094				.filter(|pgno| !matches!(resolved.get(pgno), Some(Some(_))))
3095				.collect::<Vec<_>>();
3096			if !missing_in_range_pages.is_empty() {
3097				tracing::warn!(
3098					actor_id = %ctx.actor_id,
3099					offset = i_offset,
3100					amount = i_amt,
3101					page_size,
3102					db_size_pages,
3103					file_size,
3104					requested_pages = ?requested_pages,
3105					missing_in_range_pages = ?missing_in_range_pages,
3106					"sqlite xRead would zero-fill pages within declared db size"
3107				);
3108			}
3109		}
3110
3111		buf.fill(0);
3112		for pgno in requested_pages.iter().copied() {
3113			let Some(Some(bytes)) = resolved.get(&pgno) else {
3114				continue;
3115			};
3116			let page_start = (pgno as usize - 1) * page_size;
3117			let copy_start = page_start.max(i_offset as usize);
3118			let copy_end = (page_start + page_size).min(i_offset as usize + i_amt as usize);
3119			if copy_start >= copy_end {
3120				continue;
3121			}
3122			let page_offset = copy_start - page_start;
3123			let dest_offset = copy_start - i_offset as usize;
3124			let copy_len = copy_end - copy_start;
3125			buf[dest_offset..dest_offset + copy_len]
3126				.copy_from_slice(&bytes[page_offset..page_offset + copy_len]);
3127		}
3128
3129		if i_offset as usize + i_amt as usize > file_size {
3130			return SQLITE_IOERR_SHORT_READ;
3131		}
3132
3133		SQLITE_OK
3134	})
3135}
3136
3137/// How many page records a write adds to the main rollback journal.
3138///
3139/// A rollback replays every journal record, so the journal holds at most as many records as
3140/// one commit may carry. SQLite writes a record as a 4 byte page number, the page at an offset
3141/// that is never sector-aligned, and a 4 byte checksum; headers are page-sized but
3142/// sector-aligned. When it moves an in-memory journal to the file, it copies it in chunks of
3143/// any size, so other writes that extend the file count every record they could hold. That
3144/// overcounts the headers, which only makes the cap refuse a little earlier.
3145fn journal_records_added(offset: usize, len: usize, file_len: usize, page_size: usize) -> usize {
3146	if len == page_size {
3147		return usize::from(offset % DEFAULT_PAGE_SIZE != 0);
3148	}
3149	if len == 4 {
3150		return 0;
3151	}
3152	let appended = (offset + len).saturating_sub(file_len.max(offset));
3153	appended.div_ceil(page_size + 8)
3154}
3155
3156unsafe extern "C" fn io_write(
3157	p_file: *mut sqlite3_file,
3158	p_buf: *const c_void,
3159	i_amt: c_int,
3160	i_offset: sqlite3_int64,
3161) -> c_int {
3162	vfs_catch_unwind!(SQLITE_IOERR_WRITE, {
3163		if i_amt <= 0 {
3164			return SQLITE_OK;
3165		}
3166
3167		let file = get_file(p_file);
3168		if let Some(aux) = get_aux_state(file) {
3169			if i_offset < 0 {
3170				return SQLITE_IOERR_WRITE;
3171			}
3172
3173			let offset = i_offset as usize;
3174			let source = slice::from_raw_parts(p_buf.cast::<u8>(), i_amt as usize);
3175			let end = offset + source.len();
3176			let mut bytes = aux.state.bytes.lock();
3177			if aux.is_main_journal {
3178				let ctx = &*file.ctx;
3179				let added =
3180					journal_records_added(offset, source.len(), bytes.len(), ctx.page_size());
3181				let records = aux.state.journal_records.load(Ordering::Relaxed);
3182				if added > 0 && records + added > ctx.config.commit_page_cap() {
3183					drop(bytes);
3184					return ctx.refuse_oversize_transaction();
3185				}
3186				aux.state
3187					.journal_records
3188					.store(records + added, Ordering::Relaxed);
3189			}
3190			if bytes.len() < end {
3191				bytes.resize(end, 0);
3192			}
3193			bytes[offset..end].copy_from_slice(source);
3194			return SQLITE_OK;
3195		}
3196
3197		let ctx = &*file.ctx;
3198		if ctx.is_dead() {
3199			return SQLITE_IOERR_WRITE;
3200		}
3201
3202		let page_size = ctx.page_size();
3203		let source = slice::from_raw_parts(p_buf.cast::<u8>(), i_amt as usize);
3204		let target_pages = match page_span(i_offset, i_amt as usize, page_size) {
3205			Ok(pages) => pages,
3206			Err(_) => return SQLITE_IOERR_WRITE,
3207		};
3208
3209		// Bound the transaction's dirty pages before holding them, not when committing them.
3210		// Pages the transaction already changed stay writable, so SQLite's rollback, which
3211		// rewrites journaled pages, always fits.
3212		{
3213			let state = ctx.state.read();
3214			let new_pages = target_pages
3215				.iter()
3216				.filter(|pgno| !state.write_buffer.dirty.contains_key(pgno))
3217				.count();
3218			if state.write_buffer.dirty.len() + new_pages > ctx.config.commit_page_cap() {
3219				drop(state);
3220				ctx.state.write().refused_write_pending = true;
3221				return ctx.refuse_oversize_transaction();
3222			}
3223		}
3224
3225		// Fast path: for full-page aligned writes we don't need the existing
3226		// page data because we're overwriting every byte. Skip resolve_pages
3227		// to eliminate a round trip to the engine per page. Also, for pages
3228		// beyond db_size_pages (new allocations), there's nothing to fetch.
3229		let offset = i_offset as usize;
3230		let amt = i_amt as usize;
3231		let is_aligned_full_page = offset % page_size == 0 && amt % page_size == 0;
3232
3233		let mut resolved = HashMap::new();
3234		if !is_aligned_full_page {
3235			let (db_size_pages, pages_to_resolve): (u32, Vec<u32>) = {
3236				let state = ctx.state.read();
3237				let known_max = state.db_size_pages;
3238				(
3239					known_max,
3240					target_pages
3241						.iter()
3242						.copied()
3243						.filter(|pgno| {
3244							*pgno <= known_max
3245								&& !state.write_buffer.dirty.contains_key(pgno)
3246								&& state.corked_page(*pgno).is_none()
3247						})
3248						.collect(),
3249				)
3250			};
3251
3252			if !pages_to_resolve.is_empty() {
3253				resolved = match ctx.resolve_pages(&pages_to_resolve, false) {
3254					Ok(pages) => pages,
3255					Err(GetPagesError::FenceMismatch(message)) => {
3256						ctx.mark_fatal(message);
3257						return SQLITE_IOERR_WRITE;
3258					}
3259					Err(GetPagesError::Storage(error)) => {
3260						ctx.set_storage_error(error);
3261						return SQLITE_IOERR_WRITE;
3262					}
3263					Err(GetPagesError::Other(message)) => {
3264						ctx.set_last_error(message);
3265						return SQLITE_IOERR_WRITE;
3266					}
3267				};
3268			}
3269			for pgno in &target_pages {
3270				if *pgno > db_size_pages {
3271					resolved.entry(*pgno).or_insert(None);
3272				}
3273			}
3274			#[cfg(debug_assertions)]
3275			{
3276				let missing_existing_pages = target_pages
3277					.iter()
3278					.copied()
3279					.filter(|pgno| *pgno <= db_size_pages)
3280					.filter(|pgno| pages_to_resolve.contains(pgno))
3281					.filter(|pgno| !matches!(resolved.get(pgno), Some(Some(_))))
3282					.collect::<Vec<_>>();
3283				if !missing_existing_pages.is_empty() {
3284					tracing::warn!(
3285						actor_id = %ctx.actor_id,
3286						offset = i_offset,
3287						amount = i_amt,
3288						page_size,
3289						db_size_pages,
3290						target_pages = ?target_pages,
3291						missing_existing_pages = ?missing_existing_pages,
3292						"sqlite xWrite partial update would synthesize existing pages from zeros"
3293					);
3294				}
3295			}
3296		}
3297
3298		let mut state = ctx.state.write();
3299		for pgno in target_pages {
3300			let page_start = (pgno as usize - 1) * page_size;
3301			let patch_start = page_start.max(offset);
3302			let patch_end = (page_start + page_size).min(offset + amt);
3303			let Some(copy_len) = patch_end.checked_sub(patch_start) else {
3304				continue;
3305			};
3306			if copy_len == 0 {
3307				continue;
3308			}
3309
3310			let page_offset = patch_start - page_start;
3311			let source_offset = patch_start - offset;
3312			if is_aligned_full_page {
3313				state.write_buffer.dirty.insert(
3314					pgno,
3315					source[source_offset..source_offset + copy_len]
3316						.to_vec()
3317						.into(),
3318				);
3319				continue;
3320			}
3321
3322			let mut page = state
3323				.write_buffer
3324				.dirty
3325				.get_mut(&pgno)
3326				.map(std::mem::take)
3327				.map(PageBytes::into_vec)
3328				.unwrap_or_else(|| {
3329					resolved
3330						.remove(&pgno)
3331						.flatten()
3332						.map(PageBytes::into_vec)
3333						.unwrap_or_else(|| vec![0; page_size])
3334				});
3335			if page.len() < page_size {
3336				page.resize(page_size, 0);
3337			}
3338			page[page_offset..page_offset + copy_len]
3339				.copy_from_slice(&source[source_offset..source_offset + copy_len]);
3340			state.write_buffer.dirty.insert(pgno, page.into());
3341		}
3342
3343		let end_page = ((offset + amt) + page_size - 1) / page_size;
3344		state.db_size_pages = state.db_size_pages.max(end_page as u32);
3345		
3346		ctx.clear_last_error();
3347		SQLITE_OK
3348	})
3349}
3350
3351unsafe extern "C" fn io_truncate(p_file: *mut sqlite3_file, size: sqlite3_int64) -> c_int {
3352	vfs_catch_unwind!(SQLITE_IOERR_TRUNCATE, {
3353		if size < 0 {
3354			return SQLITE_IOERR_TRUNCATE;
3355		}
3356		let file = get_file(p_file);
3357		if let Some(aux) = get_aux_state(file) {
3358			let mut bytes = aux.state.bytes.lock();
3359			bytes.truncate(size as usize);
3360			bytes.shrink_to_fit();
3361			if size == 0 {
3362				aux.state.journal_records.store(0, Ordering::Relaxed);
3363			}
3364			return SQLITE_OK;
3365		}
3366		let ctx = &*file.ctx;
3367		if !ctx.truncate_main_file(size) {
3368			return SQLITE_OK;
3369		}
3370		match ctx.flush_dirty_pages() {
3371			Ok(_) => SQLITE_OK,
3372			Err(err) => {
3373				tracing::error!(
3374					actor_id = %ctx.actor_id,
3375					last_error = ?ctx.clone_last_error(),
3376					?err,
3377					"sqlite truncate commit failed"
3378				);
3379				handle_finalize_fence_error(ctx, &err);
3380				ctx.abandon_write_buffer();
3381				SQLITE_IOERR_TRUNCATE
3382			}
3383		}
3384	})
3385}
3386
3387/// xSync returns once `ctx.flush_dirty_pages()` resolves. Durability of those
3388/// bytes is delegated to depot's `sqlite_commit` reply. If pegboard-envoy ever
3389/// pre-acks before the FDB tx commit, xSync's durability contract is broken.
3390unsafe extern "C" fn io_sync(p_file: *mut sqlite3_file, _flags: c_int) -> c_int {
3391	vfs_catch_unwind!(SQLITE_IOERR_FSYNC, {
3392		let file = get_file(p_file);
3393		if get_aux_state(file).is_some() {
3394			return SQLITE_OK;
3395		}
3396		let ctx = &*file.ctx;
3397		if let Some(message) = ctx.take_transient_commit_error() {
3398			ctx.set_last_error(message);
3399			ctx.abandon_write_buffer();
3400			return SQLITE_IOERR_FSYNC;
3401		}
3402		match ctx.flush_dirty_pages() {
3403			Ok(_) => SQLITE_OK,
3404			Err(err) => {
3405				tracing::error!(
3406					actor_id = %ctx.actor_id,
3407					last_error = ?ctx.clone_last_error(),
3408					?err,
3409					"sqlite sync failed"
3410				);
3411				handle_finalize_fence_error(ctx, &err);
3412				ctx.abandon_write_buffer();
3413				SQLITE_IOERR_FSYNC
3414			}
3415		}
3416	})
3417}
3418
3419unsafe extern "C" fn io_file_size(p_file: *mut sqlite3_file, p_size: *mut sqlite3_int64) -> c_int {
3420	vfs_catch_unwind!(SQLITE_IOERR_FSTAT, {
3421		let file = get_file(p_file);
3422		if let Some(aux) = get_aux_state(file) {
3423			*p_size = aux.state.bytes.lock().len() as sqlite3_int64;
3424			return SQLITE_OK;
3425		}
3426		let ctx = &*file.ctx;
3427		let state = ctx.state.read();
3428		*p_size = (state.db_size_pages as usize * state.page_size) as sqlite3_int64;
3429		SQLITE_OK
3430	})
3431}
3432
3433// Lock callbacks are intentional no-ops. Pegboard guarantees a single actor
3434// process per actor_id, the database is opened with `locking_mode=EXCLUSIVE`,
3435// and only one SQLite connection runs against it, so SQLite's internal lock
3436// state machine is single-party and has nothing to coordinate with. Flipping
3437// to a non-EXCLUSIVE locking mode or a multi-connection setup would require
3438// implementing a real lock state ladder here.
3439unsafe extern "C" fn io_lock(_p_file: *mut sqlite3_file, _level: c_int) -> c_int {
3440	vfs_catch_unwind!(SQLITE_IOERR_LOCK, SQLITE_OK)
3441}
3442
3443unsafe extern "C" fn io_unlock(_p_file: *mut sqlite3_file, _level: c_int) -> c_int {
3444	vfs_catch_unwind!(SQLITE_IOERR_UNLOCK, SQLITE_OK)
3445}
3446
3447unsafe extern "C" fn io_check_reserved_lock(
3448	_p_file: *mut sqlite3_file,
3449	p_res_out: *mut c_int,
3450) -> c_int {
3451	vfs_catch_unwind!(SQLITE_IOERR, {
3452		*p_res_out = 0;
3453		SQLITE_OK
3454	})
3455}
3456
3457unsafe extern "C" fn io_file_control(
3458	p_file: *mut sqlite3_file,
3459	op: c_int,
3460	p_arg: *mut c_void,
3461) -> c_int {
3462	vfs_catch_unwind!(SQLITE_IOERR, {
3463		let file = get_file(p_file);
3464		if get_aux_state(file).is_some() {
3465			return SQLITE_NOTFOUND;
3466		}
3467		let ctx = &*file.ctx;
3468
3469		match op {
3470			SQLITE_FCNTL_BEGIN_ATOMIC_WRITE => {
3471				let mut state = ctx.state.write();
3472				state.write_buffer.in_atomic_write = true;
3473				state.write_buffer.saved_db_size = state.db_size_pages;
3474				state.write_buffer.dirty.clear();
3475				state.refused_write_pending = false;
3476				SQLITE_OK
3477			}
3478			SQLITE_FCNTL_COMMIT_ATOMIC_WRITE => match ctx.commit_atomic_write() {
3479				Ok(()) => {
3480					ctx.commit_atomic_count.fetch_add(1, Ordering::Relaxed);
3481					SQLITE_OK
3482				}
3483				Err(err) => {
3484					tracing::error!(
3485						actor_id = %ctx.actor_id,
3486						last_error = ?ctx.clone_last_error(),
3487						?err,
3488						"sqlite atomic write file control failed"
3489					);
3490					match &err {
3491						CommitBufferError::Other(message) => {
3492							ctx.defer_transient_commit_error(message.clone());
3493						}
3494						CommitBufferError::Storage(error) => {
3495							ctx.defer_transient_commit_error(error.to_string());
3496						}
3497						CommitBufferError::FenceMismatch(_)
3498						| CommitBufferError::Indeterminate(_) => {}
3499					}
3500					handle_finalize_fence_error(ctx, &err);
3501					SQLITE_IOERR
3502				}
3503			},
3504			SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE => {
3505				let mut state = ctx.state.write();
3506				state.write_buffer.dirty.clear();
3507				state.write_buffer.in_atomic_write = false;
3508				state.db_size_pages = state.write_buffer.saved_db_size;
3509				state.refused_write_pending = false;
3510				SQLITE_OK
3511			}
3512			// VACUUM announces the size of the database it is about to write over this one,
3513			// after building the copy and before writing any of it.
3514			SQLITE_FCNTL_OVERWRITE => {
3515				if p_arg.is_null() {
3516					return SQLITE_NOTFOUND;
3517				}
3518				let bytes = *(p_arg as *const i64);
3519				let pages = (bytes.max(0) as usize).div_ceil(ctx.page_size());
3520				if pages > ctx.config.commit_page_cap() {
3521					ctx.refuse_oversize_transaction()
3522				} else {
3523					SQLITE_NOTFOUND
3524				}
3525			}
3526			FCNTL_COMMIT_PAGE_CAP => {
3527				if p_arg.is_null() {
3528					return SQLITE_MISUSE;
3529				}
3530				*(p_arg as *mut i64) = ctx.config.commit_page_cap() as i64;
3531				SQLITE_OK
3532			}
3533			_ => SQLITE_NOTFOUND,
3534		}
3535	})
3536}
3537
3538unsafe extern "C" fn io_sector_size(_p_file: *mut sqlite3_file) -> c_int {
3539	vfs_catch_unwind!(DEFAULT_PAGE_SIZE as c_int, DEFAULT_PAGE_SIZE as c_int)
3540}
3541
3542unsafe extern "C" fn io_device_characteristics(p_file: *mut sqlite3_file) -> c_int {
3543	vfs_catch_unwind!(0, {
3544		let file = get_file(p_file);
3545		if get_aux_state(file).is_some() {
3546			0
3547		} else {
3548			
3549			SQLITE_IOCAP_BATCH_ATOMIC
3550		}
3551	})
3552}
3553
3554unsafe extern "C" fn vfs_open(
3555	p_vfs: *mut sqlite3_vfs,
3556	z_name: *const c_char,
3557	p_file: *mut sqlite3_file,
3558	flags: c_int,
3559	p_out_flags: *mut c_int,
3560) -> c_int {
3561	vfs_catch_unwind!(SQLITE_CANTOPEN, {
3562		let ctx = get_vfs_ctx(p_vfs);
3563		let delete_on_close = (flags & SQLITE_OPEN_DELETEONCLOSE) != 0;
3564		let path = if z_name.is_null() {
3565			if delete_on_close {
3566				next_temp_aux_path()
3567			} else {
3568				return SQLITE_CANTOPEN;
3569			}
3570		} else {
3571			match CStr::from_ptr(z_name).to_str() {
3572				Ok(path) => path.to_string(),
3573				Err(_) => return SQLITE_CANTOPEN,
3574			}
3575		};
3576		let is_main =
3577			path == ctx.actor_id && !delete_on_close && (flags & SQLITE_OPEN_MAIN_DB) != 0;
3578
3579		
3580
3581		let base = sqlite3_file {
3582			pMethods: ctx.io_methods.as_ref(),
3583		};
3584		
3585		let aux = if is_main {
3586			ptr::null_mut()
3587		} else {
3588			Box::into_raw(Box::new(AuxFileHandle {
3589				is_main_journal: (flags & SQLITE_OPEN_MAIN_JOURNAL) != 0
3590					&& path.strip_suffix("-journal") == Some(ctx.actor_id.as_str()),
3591				path: path.clone(),
3592				state: ctx.open_aux_file(&path),
3593				delete_on_close,
3594			}))
3595		};
3596		ptr::write(
3597			p_file.cast::<VfsFile>(),
3598			VfsFile {
3599				base,
3600				ctx: ctx as *const VfsContext,
3601				aux,
3602			},
3603		);
3604
3605		if !p_out_flags.is_null() {
3606			*p_out_flags = flags;
3607		}
3608
3609		SQLITE_OK
3610	})
3611}
3612
3613unsafe extern "C" fn vfs_delete(
3614	p_vfs: *mut sqlite3_vfs,
3615	z_name: *const c_char,
3616	_sync_dir: c_int,
3617) -> c_int {
3618	vfs_catch_unwind!(SQLITE_IOERR_DELETE, {
3619		if z_name.is_null() {
3620			return SQLITE_OK;
3621		}
3622
3623		let ctx = get_vfs_ctx(p_vfs);
3624		let path = match CStr::from_ptr(z_name).to_str() {
3625			Ok(path) => path,
3626			Err(_) => return SQLITE_OK,
3627		};
3628		if path == ctx.actor_id {
3629			// Main database deletion is unsupported because xDelete cannot remove persisted depot state.
3630			ctx.set_last_error("main database deletion is unsupported".to_string());
3631			return SQLITE_IOERR_DELETE;
3632		} else {
3633			
3634			ctx.delete_aux_file(path);
3635		}
3636		SQLITE_OK
3637	})
3638}
3639
3640unsafe extern "C" fn vfs_access(
3641	p_vfs: *mut sqlite3_vfs,
3642	z_name: *const c_char,
3643	_flags: c_int,
3644	p_res_out: *mut c_int,
3645) -> c_int {
3646	vfs_catch_unwind!(SQLITE_IOERR_ACCESS, {
3647		if z_name.is_null() {
3648			*p_res_out = 0;
3649			return SQLITE_OK;
3650		}
3651
3652		let ctx = get_vfs_ctx(p_vfs);
3653		let path = match CStr::from_ptr(z_name).to_str() {
3654			Ok(path) => path,
3655			Err(_) => {
3656				*p_res_out = 0;
3657				return SQLITE_OK;
3658			}
3659		};
3660
3661		*p_res_out = if path == ctx.actor_id || ctx.aux_file_exists(path) {
3662			1
3663		} else {
3664			0
3665		};
3666		SQLITE_OK
3667	})
3668}
3669
3670unsafe extern "C" fn vfs_full_pathname(
3671	_p_vfs: *mut sqlite3_vfs,
3672	z_name: *const c_char,
3673	n_out: c_int,
3674	z_out: *mut c_char,
3675) -> c_int {
3676	vfs_catch_unwind!(SQLITE_IOERR, {
3677		if z_name.is_null() || z_out.is_null() || n_out <= 0 {
3678			return SQLITE_IOERR;
3679		}
3680
3681		let name = CStr::from_ptr(z_name);
3682		let bytes = name.to_bytes_with_nul();
3683		if bytes.len() >= n_out as usize {
3684			return SQLITE_IOERR;
3685		}
3686
3687		ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), z_out, bytes.len());
3688		SQLITE_OK
3689	})
3690}
3691
3692unsafe extern "C" fn vfs_randomness(
3693	_p_vfs: *mut sqlite3_vfs,
3694	n_byte: c_int,
3695	z_out: *mut c_char,
3696) -> c_int {
3697	vfs_catch_unwind!(0, {
3698		let buf = slice::from_raw_parts_mut(z_out.cast::<u8>(), n_byte as usize);
3699		match getrandom::getrandom(buf) {
3700			Ok(()) => n_byte,
3701			Err(_) => 0,
3702		}
3703	})
3704}
3705
3706unsafe extern "C" fn vfs_sleep(_p_vfs: *mut sqlite3_vfs, microseconds: c_int) -> c_int {
3707	vfs_catch_unwind!(0, {
3708		std::thread::sleep(std::time::Duration::from_micros(microseconds as u64));
3709		microseconds
3710	})
3711}
3712
3713unsafe extern "C" fn vfs_current_time(_p_vfs: *mut sqlite3_vfs, p_time_out: *mut f64) -> c_int {
3714	vfs_catch_unwind!(SQLITE_IOERR, {
3715		let now = std::time::SystemTime::now()
3716			.duration_since(std::time::UNIX_EPOCH)
3717			.unwrap_or_default();
3718		*p_time_out = 2440587.5 + (now.as_secs_f64() / 86400.0);
3719		SQLITE_OK
3720	})
3721}
3722
3723unsafe extern "C" fn vfs_get_last_error(
3724	p_vfs: *mut sqlite3_vfs,
3725	n_byte: c_int,
3726	z_err_msg: *mut c_char,
3727) -> c_int {
3728	vfs_catch_unwind!(SQLITE_IOERR, {
3729		if n_byte <= 0 || z_err_msg.is_null() {
3730			return 0;
3731		}
3732
3733		let ctx = get_vfs_ctx(p_vfs);
3734		let Some(message) = ctx.clone_last_error() else {
3735			*z_err_msg = 0;
3736			return 0;
3737		};
3738
3739		let bytes = message.as_bytes();
3740		let max_len = (n_byte as usize).saturating_sub(1);
3741		let copy_len = bytes.len().min(max_len);
3742		let dst = z_err_msg.cast::<u8>();
3743		ptr::copy_nonoverlapping(bytes.as_ptr(), dst, copy_len);
3744		*dst.add(copy_len) = 0;
3745		0
3746	})
3747}
3748
3749impl SqliteVfs {
3750	pub(crate) fn take_last_error(&self) -> Option<String> {
3751		self.ctx.take_last_error()
3752	}
3753
3754	pub(crate) fn take_indeterminate_result(&self) -> Option<String> {
3755		self.ctx.take_indeterminate_result()
3756	}
3757
3758	pub(crate) fn take_transaction_too_large(&self) -> Option<SqliteTransactionTooLargeError> {
3759		self.ctx.take_transaction_too_large()
3760	}
3761
3762	pub(crate) fn take_storage_error(&self) -> Option<SqliteStorageError> {
3763		self.ctx.take_storage_error()
3764	}
3765
3766	fn clone_last_error(&self) -> Option<String> {
3767		self.ctx.clone_last_error()
3768	}
3769
3770	pub fn clone_fatal_error(&self) -> Option<String> {
3771		self.ctx.clone_fatal_error()
3772	}
3773
3774	/// Reads back an auxiliary file this VFS holds in memory. `VACUUM INTO` and other statements
3775	/// that name an output file resolve it through the connection's VFS, so the only way to get
3776	/// those bytes onto the host filesystem is to copy them out here.
3777	pub(crate) fn read_aux_file(&self, path: &str) -> Option<Vec<u8>> {
3778		self.ctx.read_aux_file(path)
3779	}
3780
3781	pub(crate) fn delete_aux_file(&self, path: &str) {
3782		self.ctx.delete_aux_file(path);
3783	}
3784
3785	pub(crate) fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
3786		self.ctx.sqlite_vfs_metrics()
3787	}
3788
3789	
3790
3791	
3792
3793	pub(crate) fn register_with_transport_and_initial_pages(
3794		name: &str,
3795		transport: SqliteTransportHandle,
3796		actor_id: String,
3797		runtime: Handle,
3798		config: VfsConfig,
3799		initial_pages: InitialPages,
3800		metrics: Option<Arc<dyn SqliteVfsMetrics>>,
3801	) -> std::result::Result<Self, String> {
3802		let mut io_methods: sqlite3_io_methods = unsafe { std::mem::zeroed() };
3803		io_methods.iVersion = 1;
3804		io_methods.xClose = Some(io_close);
3805		io_methods.xRead = Some(io_read);
3806		io_methods.xWrite = Some(io_write);
3807		io_methods.xTruncate = Some(io_truncate);
3808		io_methods.xSync = Some(io_sync);
3809		io_methods.xFileSize = Some(io_file_size);
3810		io_methods.xLock = Some(io_lock);
3811		io_methods.xUnlock = Some(io_unlock);
3812		io_methods.xCheckReservedLock = Some(io_check_reserved_lock);
3813		io_methods.xFileControl = Some(io_file_control);
3814		io_methods.xSectorSize = Some(io_sector_size);
3815		io_methods.xDeviceCharacteristics = Some(io_device_characteristics);
3816
3817		let generation = name
3818			.rsplit_once("-g")
3819			.and_then(|(_, generation)| generation.parse::<u64>().ok());
3820		let mut ctx = Box::new(VfsContext::new(
3821			actor_id,
3822			generation,
3823			runtime,
3824			transport,
3825			config,
3826			io_methods,
3827			initial_pages,
3828			metrics,
3829		)?);
3830		tracing::trace!(target: "sqlite_prefetch::trace", event = "open",
3831            stream_id = %format_args!("sqlite-{:p}", &*ctx), actor_id = %ctx.actor_id, generation = ctx.generation);
3832		let ctx_ptr = (&mut *ctx) as *mut VfsContext;
3833		let name_cstring = CString::new(name).map_err(|err| err.to_string())?;
3834
3835		let mut vfs: sqlite3_vfs = unsafe { std::mem::zeroed() };
3836		vfs.iVersion = 1;
3837		vfs.szOsFile = std::mem::size_of::<VfsFile>() as c_int;
3838		vfs.mxPathname = MAX_PATHNAME;
3839		vfs.zName = name_cstring.as_ptr();
3840		vfs.pAppData = ctx_ptr.cast::<c_void>();
3841		vfs.xOpen = Some(vfs_open);
3842		vfs.xDelete = Some(vfs_delete);
3843		vfs.xAccess = Some(vfs_access);
3844		vfs.xFullPathname = Some(vfs_full_pathname);
3845		vfs.xRandomness = Some(vfs_randomness);
3846		vfs.xSleep = Some(vfs_sleep);
3847		vfs.xCurrentTime = Some(vfs_current_time);
3848		vfs.xGetLastError = Some(vfs_get_last_error);
3849
3850		let registration = SqliteVfsRegistration::register(vfs)?;
3851
3852		Ok(Self {
3853			_registration: registration,
3854			_name: name_cstring,
3855			ctx,
3856		})
3857	}
3858
3859	pub fn name_ptr(&self) -> *const c_char {
3860		self._name.as_ptr()
3861	}
3862
3863	/// Bytes held by the VFS prefetch buffer and committed-page buffer, which hold only durable
3864	/// copies.
3865	pub fn cached_page_bytes(&self) -> u64 {
3866		let state = self.ctx.state.read();
3867		state
3868			.prefetch_buffer
3869			.entry_count()
3870			.saturating_add(state.committed_buffer.entry_count())
3871			.saturating_mul(state.page_size as u64)
3872	}
3873
3874	/// How long the worker waits without a command before releasing this database's caches.
3875	pub fn idle_release(&self) -> Option<std::time::Duration> {
3876		match self.ctx.config.idle_release_ms {
3877			0 => None,
3878			ms => Some(std::time::Duration::from_millis(ms)),
3879		}
3880	}
3881
3882	
3883
3884	fn ctx(&self) -> &VfsContext {
3885		&self.ctx
3886	}
3887
3888	fn commit_atomic_count(&self) -> u64 {
3889		self.ctx.commit_atomic_count.load(Ordering::Relaxed)
3890	}
3891}
3892
3893impl SqliteVfsRegistration {
3894	fn register(vfs: sqlite3_vfs) -> std::result::Result<Self, String> {
3895		let vfs_ptr = Box::into_raw(Box::new(vfs));
3896		let rc = unsafe { sqlite3_vfs_register(vfs_ptr, 0) };
3897		if rc != SQLITE_OK {
3898			unsafe {
3899				drop(Box::from_raw(vfs_ptr));
3900			}
3901			return Err(format!("sqlite3_vfs_register failed with code {rc}"));
3902		}
3903
3904		Ok(Self { vfs_ptr })
3905	}
3906}
3907
3908impl Drop for SqliteVfsRegistration {
3909	fn drop(&mut self) {
3910		unsafe {
3911			sqlite3_vfs_unregister(self.vfs_ptr);
3912			drop(Box::from_raw(self.vfs_ptr));
3913		}
3914	}
3915}
3916
3917impl NativeDatabase {
3918	pub fn as_ptr(&self) -> *mut sqlite3 {
3919		self.db
3920	}
3921
3922	pub fn take_last_kv_error(&self) -> Option<String> {
3923		self._vfs.take_last_error()
3924	}
3925
3926	pub fn sqlite_vfs_metrics(&self) -> SqliteVfsMetricsSnapshot {
3927		self._vfs.ctx.sqlite_vfs_metrics()
3928	}
3929
3930	pub fn round_trip_counts(&self) -> SqliteRoundTripCounts {
3931		self._vfs.ctx.round_trip_counts()
3932	}
3933
3934	pub(crate) fn begin_operation_profile(&self) -> SqliteOperationProfileGuard<'_> {
3935		self._vfs.ctx.begin_operation_profile();
3936		SqliteOperationProfileGuard {
3937			ctx: &self._vfs.ctx,
3938			active: true,
3939		}
3940	}
3941
3942	/// Bytes held by the VFS page buffers, which hold only durable copies.
3943	pub fn cached_page_bytes(&self) -> u64 {
3944		self._vfs.cached_page_bytes()
3945	}
3946
3947	/// Frees the pager's unpinned pages and the VFS page buffers for an idle database. Returns
3948	/// false without releasing while a write is buffered; the caller must also skip the release
3949	/// while an explicit transaction is open. Every released page is a durable copy that the
3950	/// next read fetches again.
3951	/// Holds every later commit in memory until `uncork_commits`, which sends them to depot as
3952	/// one commit. The transactions are committed for this connection meanwhile, but none is
3953	/// durable, so nothing may report them done until uncorking succeeds.
3954	pub fn cork_commits(&self) {
3955		self._vfs.ctx().cork();
3956	}
3957
3958	/// Sends the transactions committed since `cork_commits` to depot as one commit. Must run
3959	/// outside a transaction.
3960	/// Sends every commit made since `cork_commits` to depot as one commit.
3961	///
3962	/// Fails while the connection has an open transaction, because its pages
3963	/// may still sit in SQLite's page cache where the VFS cannot see them.
3964	pub fn uncork_commits(&self) -> Result<()> {
3965		// SAFETY: `as_ptr` is a live connection owned by this database.
3966		let autocommit = unsafe { sqlite3_get_autocommit(self.as_ptr()) };
3967		if autocommit == 0 {
3968			anyhow::bail!("cannot uncork sqlite commits inside a transaction");
3969		}
3970		self._vfs
3971			.ctx()
3972			.uncork()
3973			.map_err(|error| anyhow::anyhow!("uncork sqlite commits: {error:?}"))
3974	}
3975
3976	pub fn release_idle_memory(&self) -> bool {
3977		{
3978			let state = self._vfs.ctx.state.read();
3979			if state.write_buffer.in_atomic_write
3980				|| !state.write_buffer.dirty.is_empty()
3981				|| state.corked.is_some()
3982			{
3983				return false;
3984			}
3985		}
3986		unsafe {
3987			sqlite3_db_release_memory(self.db);
3988		}
3989		self._vfs.ctx.state.write().invalidate_page_buffers();
3990		self._vfs.ctx.report_prefetch_unused();
3991		true
3992	}
3993}
3994
3995impl Drop for NativeDatabase {
3996	fn drop(&mut self) {
3997		if !self.db.is_null() {
3998			let ctx = self._vfs.ctx();
3999			ctx.discard_refused_transaction();
4000			// A transaction still open at close never committed, so the flush below must carry
4001			// none of its spilled pages. SQLite rolls it back as the connection closes.
4002			if write_transaction_open(self.db) {
4003				ctx.discard_open_transaction();
4004			}
4005			// Corked pages join the write buffer, so the flush below carries them.
4006			{
4007				let mut state = ctx.state.write();
4008				if let Some(mut corked) = state.corked.take() {
4009					corked.append(&mut state.write_buffer.dirty);
4010					state.write_buffer.dirty = corked;
4011				}
4012			}
4013			let should_flush = {
4014				let state = ctx.state.read();
4015				state.write_buffer.in_atomic_write
4016					|| !state.write_buffer.dirty.is_empty()
4017					|| state.db_size_pages != state.committed_db_size_pages
4018			};
4019			if should_flush {
4020				let result = if ctx.state.read().write_buffer.in_atomic_write {
4021					ctx.commit_atomic_write_with_timeout(Some(NATIVE_DATABASE_DROP_FLUSH_TIMEOUT))
4022				} else {
4023					ctx.flush_dirty_pages_with_timeout(Some(NATIVE_DATABASE_DROP_FLUSH_TIMEOUT))
4024						.map(|wait| match wait {
4025							CommitWait::Completed(_) => CommitWait::Completed(()),
4026							CommitWait::TimedOut => CommitWait::TimedOut,
4027						})
4028				};
4029				// A flush that fails or runs out of time leaves the connection with commits it
4030				// cannot send. The connection still closes: keeping it open would leak SQLite's
4031				// connection and its share of the process-wide page cache, and the close below
4032				// must not try to send the same pages again.
4033				match result {
4034					Ok(CommitWait::Completed(())) => {}
4035					Ok(CommitWait::TimedOut) => {
4036						let discarded_pages = ctx.discard_unsent_commits();
4037						tracing::error!(
4038							actor_id = %ctx.actor_id,
4039							timeout_ms = NATIVE_DATABASE_DROP_FLUSH_TIMEOUT.as_millis(),
4040							discarded_pages,
4041							"timed out flushing sqlite database before close, discarding its unsent commits"
4042						);
4043					}
4044					Err(err) => {
4045						handle_non_finalize_commit_error(ctx, &err);
4046						let discarded_pages = ctx.discard_unsent_commits();
4047						tracing::error!(
4048							actor_id = %ctx.actor_id,
4049							?err,
4050							discarded_pages,
4051							"failed to flush sqlite database before close, discarding its unsent commits"
4052						);
4053					}
4054				}
4055			}
4056
4057			// Every committed transaction is now in Depot or discarded. SQLite's rollback of an
4058			// open transaction during close only restores that state, so nothing it writes is
4059			// sent. Every statement the connection's callers prepare is finalized before their
4060			// call returns, so the close is never deferred by a statement nobody owns.
4061			ctx.state.write().closing = true;
4062			let rc = unsafe { sqlite3_close_v2(self.db) };
4063			if rc != SQLITE_OK {
4064				tracing::warn!(
4065					rc,
4066					error = sqlite_error_message(self.db),
4067					"failed to close sqlite database"
4068				);
4069			}
4070			self.db = ptr::null_mut();
4071		}
4072	}
4073}
4074
4075/// Whether the connection holds a write transaction SQLite has not committed: an explicit
4076/// transaction after `BEGIN`, or a statement whose implicit transaction has not finished.
4077fn write_transaction_open(db: *mut sqlite3) -> bool {
4078	// SAFETY: `db` is a live connection owned by the caller.
4079	let autocommit = unsafe { sqlite3_get_autocommit(db) };
4080	let txn_state = unsafe { sqlite3_txn_state(db, ptr::null()) };
4081	autocommit == 0 || txn_state == SQLITE_TXN_WRITE
4082}
4083
4084pub fn open_database(vfs: SqliteVfs, file_name: &str) -> Result<NativeDatabase> {
4085	open_connection(
4086		Arc::new(vfs),
4087		file_name,
4088		SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,
4089	)
4090	.map_err(anyhow::Error::msg)
4091	.and_then(|connection| {
4092		configure_connection_for_database(connection.as_ptr(), &connection._vfs, file_name)?;
4093		Ok(connection)
4094	})
4095}
4096
4097pub fn open_connection(
4098	vfs: NativeVfsHandle,
4099	file_name: &str,
4100	flags: c_int,
4101) -> std::result::Result<NativeConnection, String> {
4102	let c_name = CString::new(file_name).map_err(|err| err.to_string())?;
4103	let mut db: *mut sqlite3 = ptr::null_mut();
4104
4105	let rc = unsafe { sqlite3_open_v2(c_name.as_ptr(), &mut db, flags, vfs.name_ptr()) };
4106	if rc != SQLITE_OK {
4107		let message = sqlite_error_message(db);
4108		tracing::error!(
4109			file_name,
4110			rc,
4111			%message,
4112			last_error = ?vfs.clone_last_error(),
4113			"failed to open sqlite database with custom VFS"
4114		);
4115		if !db.is_null() {
4116			unsafe {
4117				sqlite3_close(db);
4118			}
4119		}
4120		return Err(format!("sqlite3_open_v2 failed with code {rc}: {message}"));
4121	}
4122
4123	Ok(NativeDatabase { db, _vfs: vfs })
4124}
4125
4126#[derive(rivet_error::RivetError)]
4127#[error(
4128	"sqlite",
4129	"invalid_compile_options",
4130	"SQLite requires SQLITE_ENABLE_BATCH_ATOMIC_WRITE and SQLITE_DIRECT_OVERFLOW_READ=0. Rebuild with LIBSQLITE3_FLAGS=\"SQLITE_ENABLE_BATCH_ATOMIC_WRITE SQLITE_DIRECT_OVERFLOW_READ=0\"."
4131)]
4132struct SqliteInvalidCompileOptions;
4133
4134pub fn configure_connection_for_database(
4135	db: *mut sqlite3,
4136	vfs: &SqliteVfs,
4137	file_name: &str,
4138) -> Result<()> {
4139	// SAFETY: SQLite only reads its static compile-option table using these C strings.
4140	let batch_atomic = unsafe { sqlite3_compileoption_used(c"ENABLE_BATCH_ATOMIC_WRITE".as_ptr()) };
4141	let direct_overflow = unsafe { sqlite3_compileoption_used(c"DIRECT_OVERFLOW_READ".as_ptr()) };
4142	if batch_atomic == 0 || direct_overflow != 0 {
4143		return Err(SqliteInvalidCompileOptions.build());
4144	}
4145
4146	// SQLite interprets a negative cache_size as a KiB budget instead of a page count. With the
4147	// process-wide pager cache installed, cache_size reaches the cache only through `xCachesize`,
4148	// which ignores it: the process budget bounds the pages, and SQLite spills a transaction's
4149	// dirty pages only when the cache refuses a `createFlag` 1 fetch. cache_size then matters only
4150	// for SQLite's temporary-file flush heuristic, so it keeps its per-connection value.
4151	let cache_size_kib = sqlite_optimization_flags().pager_cache_size_kib;
4152	let cache_size_pragma = format!("PRAGMA cache_size = -{cache_size_kib};");
4153
4154	let pragmas = [
4155		"PRAGMA page_size = 4096;",
4156		"PRAGMA journal_mode = DELETE;",
4157		"PRAGMA synchronous = NORMAL;",
4158		"PRAGMA temp_store = MEMORY;",
4159		"PRAGMA auto_vacuum = NONE;",
4160		"PRAGMA locking_mode = EXCLUSIVE;",
4161		// An exclusive connection keeps its journal between transactions. Truncating it after
4162		// each one releases the memory a large transaction's journal held.
4163		"PRAGMA journal_size_limit = 0;",
4164		cache_size_pragma.as_str(),
4165	];
4166	for pragma in &pragmas {
4167		if let Err(err) = sqlite_exec(db, pragma) {
4168			tracing::error!(
4169				file_name,
4170				pragma,
4171				%err,
4172				last_error = ?vfs.clone_last_error(),
4173				"failed to configure sqlite database"
4174			);
4175			return Err(anyhow::Error::msg(err));
4176		}
4177	}
4178
4179	let rc = unsafe { sqlite3_set_authorizer(db, Some(authorize_journal_mode), ptr::null_mut()) };
4180	if rc != SQLITE_OK {
4181		anyhow::bail!("failed to install the sqlite authorizer: {rc}");
4182	}
4183
4184	Ok(())
4185}
4186
4187/// Keeps every connection on the `DELETE` rollback journal the VFS depends on. Commits reach
4188/// Depot only through the main file's sync, so a WAL journal would not be durable, a `MEMORY`
4189/// journal could roll back more pages than one commit carries, and `OFF` would let a failed
4190/// transaction's pages be committed. It also pins `journal_size_limit`, and keeps `synchronous`
4191/// at a level that syncs the main file on every commit, so a commit's pages never wait in the
4192/// write buffer where a close could not tell them from a later open transaction's. Reading any
4193/// of them stays allowed. A guarded connection replaces this authorizer with one that refuses
4194/// every journal mode change.
4195extern "C" fn authorize_journal_mode(
4196	_context: *mut c_void,
4197	action: c_int,
4198	first: *const c_char,
4199	second: *const c_char,
4200	_database: *const c_char,
4201	_trigger: *const c_char,
4202) -> c_int {
4203	if action != SQLITE_PRAGMA || first.is_null() || second.is_null() {
4204		return SQLITE_OK;
4205	}
4206	let name = unsafe { CStr::from_ptr(first) }.to_string_lossy();
4207	// The journal's record count resets when the journal is truncated after each transaction,
4208	// which this limit configures.
4209	if name.eq_ignore_ascii_case("journal_size_limit") {
4210		return SQLITE_DENY;
4211	}
4212	let value = unsafe { CStr::from_ptr(second) }.to_string_lossy();
4213	let value = value.trim_matches(|c| matches!(c, '"' | '\''));
4214	if name.eq_ignore_ascii_case("synchronous") {
4215		let syncs_every_commit = [
4216			"normal", "full", "extra", "1", "2", "3", "on", "yes", "true",
4217		]
4218		.iter()
4219		.any(|level| value.eq_ignore_ascii_case(level));
4220		return if syncs_every_commit {
4221			SQLITE_OK
4222		} else {
4223			SQLITE_DENY
4224		};
4225	}
4226	if !name.eq_ignore_ascii_case("journal_mode") {
4227		return SQLITE_OK;
4228	}
4229	if value.eq_ignore_ascii_case("delete") {
4230		SQLITE_OK
4231	} else {
4232		SQLITE_DENY
4233	}
4234}