Skip to main content

rivetkit_core/actor/sqlite/
mod.rs

1use std::collections::HashSet;
2use std::future::Future;
3use std::io::Cursor;
4use std::sync::{
5	Arc,
6	atomic::{AtomicBool, Ordering},
7};
8
9use anyhow::{Context, Result};
10use depot_client_types::is_head_fence_mismatch;
11pub use depot_client_types::{BindParam, ColumnValue, ExecResult, ExecuteResult, QueryResult};
12#[cfg(feature = "sqlite-local")]
13use parking_lot::Mutex;
14use rivet_envoy_client::protocol;
15use rivet_envoy_client::{
16	handle::EnvoyHandle,
17	utils::{RemoteSqliteConnectionSessionLostError, RemoteSqliteIndeterminateResultError},
18};
19use rivet_error::{ActorSpecifier, RivetError};
20use serde::Serialize;
21use serde_json::{Map as JsonMap, Value as JsonValue};
22#[cfg(feature = "sqlite-local")]
23use tokio::sync::Mutex as AsyncMutex;
24#[cfg(feature = "sqlite-local")]
25use tokio::task::JoinHandle;
26use tracing::Instrument as _;
27
28use crate::telemetry::SqliteOperation;
29
30#[cfg(feature = "sqlite-local")]
31mod envoy_sqlite_transport;
32#[cfg(feature = "sqlite-local")]
33mod profiling;
34mod tx;
35
36pub use tx::{
37	DEFAULT_TRANSACTION_TIMEOUT, SqliteTransaction, TRANSACTION_COORDINATOR_QUEUE_CAPACITY,
38	TransactionConnectionLostError, TransactionCoordinatorClosedError, TransactionExpiredError,
39	TransactionInvalidArgumentError, TransactionQueueFullError, TransactionTerminalError,
40	TransactionUnknownError,
41};
42#[cfg(test)]
43use tx::{
44	TRANSACTION_TERMINAL_CAPACITY, TransactionCoordinatorState, TransactionTerminalState,
45	insert_terminal_state,
46};
47use tx::{TransactionCoordinator, run_detached_transaction_task};
48
49#[cfg(feature = "sqlite-local")]
50use crate::error::ActorLifecycle;
51use crate::error::SqliteRuntimeError;
52#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite-local"))]
53use crate::runtime::RuntimeSpawner;
54
55#[cfg(feature = "sqlite-local")]
56use depot_client::{
57	database::{NativeDatabaseHandle, open_database_from_transport},
58	vfs::{SqliteVfsMetrics, SqliteVfsMetricsSnapshot},
59	worker::{
60		SQLITE_WORKER_QUEUE_CAPACITY, SqliteWorkerCloseTimeoutError, SqliteWorkerClosingError,
61		SqliteWorkerDeadError, SqliteWorkerFatalError, SqliteWorkerOverloadedError,
62		SqliteWorkerResult,
63	},
64};
65#[cfg(feature = "sqlite-local")]
66use envoy_sqlite_transport::EnvoySqliteTransport;
67
68#[cfg(not(feature = "sqlite-local"))]
69#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
70pub struct SqliteVfsMetricsSnapshot {
71	pub request_build_ns: u64,
72	pub serialize_ns: u64,
73	pub transport_ns: u64,
74	pub state_update_ns: u64,
75	pub total_ns: u64,
76	pub commit_count: u64,
77}
78
79#[derive(Clone)]
80pub struct SqliteRuntimeConfig {
81	pub handle: EnvoyHandle,
82	pub actor_id: String,
83	pub generation: Option<u64>,
84}
85
86#[derive(Clone, Debug)]
87pub struct SqliteBatchStatement {
88	pub sql: String,
89	pub params: Option<Vec<BindParam>>,
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum SqliteBackend {
94	LocalNative,
95	RemoteEnvoy,
96}
97
98#[cfg(feature = "sqlite-local")]
99struct ProfiledBackendResult<T> {
100	result: Result<T>,
101	profile: Option<depot_client::vfs::SqliteOperationProfile>,
102}
103
104impl SqliteDb {
105	#[cfg(feature = "sqlite-local")]
106	async fn exec_backend_profiled(&self, sql: String) -> ProfiledBackendResult<QueryResult> {
107		match self.backend {
108			SqliteBackend::LocalNative => match self.local_exec_profiled(sql).await {
109				Ok(result) => ProfiledBackendResult {
110					result: result.result,
111					profile: Some(result.profile),
112				},
113				Err(error) => ProfiledBackendResult {
114					result: Err(error),
115					profile: None,
116				},
117			},
118			SqliteBackend::RemoteEnvoy => ProfiledBackendResult {
119				result: self.remote_exec(sql).await,
120				profile: None,
121			},
122		}
123	}
124
125	#[cfg(feature = "sqlite-local")]
126	async fn execute_backend_profiled(
127		&self,
128		sql: String,
129		params: Option<Vec<BindParam>>,
130	) -> ProfiledBackendResult<ExecuteResult> {
131		match self.backend {
132			SqliteBackend::LocalNative => match self.local_execute_profiled(sql, params).await {
133				Ok(result) => ProfiledBackendResult {
134					result: result.result,
135					profile: Some(result.profile),
136				},
137				Err(error) => ProfiledBackendResult {
138					result: Err(error),
139					profile: None,
140				},
141			},
142			SqliteBackend::RemoteEnvoy => ProfiledBackendResult {
143				result: self.remote_execute(sql, params).await,
144				profile: None,
145			},
146		}
147	}
148
149	async fn exec_backend(&self, sql: String) -> Result<QueryResult> {
150		match self.backend {
151			SqliteBackend::LocalNative => self.local_exec(sql).await,
152			SqliteBackend::RemoteEnvoy => self.remote_exec(sql).await,
153		}
154	}
155
156	async fn exec_backend_in_session(
157		&self,
158		sql: String,
159		expected_session: Option<u64>,
160	) -> Result<(QueryResult, Option<u64>)> {
161		match self.backend {
162			SqliteBackend::LocalNative => self.local_exec(sql).await.map(|result| (result, None)),
163			SqliteBackend::RemoteEnvoy => self
164				.remote_exec_with_session(sql, expected_session)
165				.await
166				.map(|(result, session)| (result, Some(session))),
167		}
168	}
169
170	async fn query_backend(
171		&self,
172		sql: String,
173		params: Option<Vec<BindParam>>,
174	) -> Result<QueryResult> {
175		match self.backend {
176			SqliteBackend::LocalNative => self
177				.local_execute(sql, params)
178				.await
179				.map(ExecuteResult::into_query_result),
180			SqliteBackend::RemoteEnvoy => self
181				.remote_execute(sql, params)
182				.await
183				.map(ExecuteResult::into_query_result),
184		}
185	}
186
187	async fn run_backend(&self, sql: String, params: Option<Vec<BindParam>>) -> Result<ExecResult> {
188		match self.backend {
189			SqliteBackend::LocalNative => self
190				.local_execute(sql, params)
191				.await
192				.map(ExecuteResult::into_exec_result),
193			SqliteBackend::RemoteEnvoy => self
194				.remote_execute(sql, params)
195				.await
196				.map(ExecuteResult::into_exec_result),
197		}
198	}
199
200	async fn execute_backend(
201		&self,
202		sql: String,
203		params: Option<Vec<BindParam>>,
204	) -> Result<ExecuteResult> {
205		match self.backend {
206			SqliteBackend::LocalNative => self.local_execute(sql, params).await,
207			SqliteBackend::RemoteEnvoy => self.remote_execute(sql, params).await,
208		}
209	}
210
211	async fn execute_backend_in_session(
212		&self,
213		sql: String,
214		params: Option<Vec<BindParam>>,
215		expected_session: Option<u64>,
216	) -> Result<(ExecuteResult, Option<u64>)> {
217		match self.backend {
218			SqliteBackend::LocalNative => self
219				.local_execute(sql, params)
220				.await
221				.map(|result| (result, None)),
222			SqliteBackend::RemoteEnvoy => self
223				.remote_execute_with_session(sql, params, expected_session)
224				.await
225				.map(|(result, session)| (result, Some(session))),
226		}
227	}
228}
229
230#[derive(Clone)]
231pub struct SqliteDb {
232	handle: Option<EnvoyHandle>,
233	actor_id: Option<String>,
234	actor_key: Option<String>,
235	generation: Option<u64>,
236	backend: SqliteBackend,
237	/// Mirrors the user's actor-config `db({...})` declaration. The envoy
238	/// always sets up sqlite storage under the hood, so handle/actor_id are
239	/// not a reliable signal for whether the user opted in; this flag is.
240	enabled: bool,
241	invocation_telemetry: Option<crate::ActorInvocationTelemetry>,
242	#[cfg(feature = "sqlite-local")]
243	// Forced-sync: native SQLite handles are used inside spawn_blocking and
244	// synchronous diagnostic accessors.
245	db: Arc<Mutex<Option<NativeDatabaseHandle>>>,
246	#[cfg(feature = "sqlite-local")]
247	open_lock: Arc<AsyncMutex<()>>,
248	#[cfg(feature = "sqlite-local")]
249	worker_failure_task: Arc<Mutex<Option<JoinHandle<()>>>>,
250	worker_fatal_reported: Arc<AtomicBool>,
251	transaction_coordinator: Arc<TransactionCoordinator>,
252	#[cfg(feature = "sqlite-local")]
253	vfs_metrics: Option<Arc<dyn SqliteVfsMetrics>>,
254	#[cfg(feature = "sqlite-local")]
255	profiling: Arc<profiling::SqliteProfilingState>,
256}
257
258impl Default for SqliteDb {
259	fn default() -> Self {
260		Self {
261			handle: None,
262			actor_id: None,
263			actor_key: None,
264			generation: None,
265			backend: if cfg!(feature = "sqlite-local") {
266				SqliteBackend::LocalNative
267			} else {
268				SqliteBackend::RemoteEnvoy
269			},
270			enabled: false,
271			invocation_telemetry: None,
272			#[cfg(feature = "sqlite-local")]
273			db: Default::default(),
274			#[cfg(feature = "sqlite-local")]
275			open_lock: Default::default(),
276			#[cfg(feature = "sqlite-local")]
277			worker_failure_task: Default::default(),
278			worker_fatal_reported: Default::default(),
279			transaction_coordinator: Default::default(),
280			#[cfg(feature = "sqlite-local")]
281			vfs_metrics: None,
282			#[cfg(feature = "sqlite-local")]
283			profiling: Default::default(),
284		}
285	}
286}
287
288impl SqliteDb {
289	pub fn new(handle: EnvoyHandle, actor_id: impl Into<String>, enabled: bool) -> Result<Self> {
290		Self::new_with_remote_sqlite(handle, actor_id, None, None, enabled, false)
291	}
292
293	pub fn new_with_remote_sqlite(
294		handle: EnvoyHandle,
295		actor_id: impl Into<String>,
296		actor_key: Option<String>,
297		generation: Option<u64>,
298		enabled: bool,
299		remote_sqlite: bool,
300	) -> Result<Self> {
301		Ok(Self {
302			handle: Some(handle),
303			actor_id: Some(actor_id.into()),
304			actor_key,
305			generation,
306			backend: select_sqlite_backend(remote_sqlite)?,
307			enabled,
308			invocation_telemetry: None,
309			#[cfg(feature = "sqlite-local")]
310			db: Default::default(),
311			#[cfg(feature = "sqlite-local")]
312			open_lock: Default::default(),
313			#[cfg(feature = "sqlite-local")]
314			worker_failure_task: Default::default(),
315			worker_fatal_reported: Default::default(),
316			transaction_coordinator: Default::default(),
317			#[cfg(feature = "sqlite-local")]
318			vfs_metrics: None,
319			#[cfg(feature = "sqlite-local")]
320			profiling: Default::default(),
321		})
322	}
323
324	#[cfg(feature = "sqlite-local")]
325	pub(crate) fn set_vfs_metrics(&mut self, metrics: Arc<dyn SqliteVfsMetrics>) {
326		self.vfs_metrics = Some(metrics);
327	}
328
329	#[cfg(feature = "sqlite-local")]
330	pub(crate) fn set_profiling_config(&mut self, config: crate::SqliteProfilingConfig) {
331		self.profiling = Arc::new(profiling::SqliteProfilingState::new(config));
332	}
333
334	#[cfg(all(test, feature = "sqlite-local"))]
335	pub(crate) fn from_native_database_for_test(
336		actor_id: impl Into<String>,
337		generation: u64,
338		native_db: NativeDatabaseHandle,
339		metrics: Arc<dyn SqliteVfsMetrics>,
340		profiling: crate::SqliteProfilingConfig,
341	) -> Self {
342		Self {
343			actor_id: Some(actor_id.into()),
344			generation: Some(generation),
345			enabled: true,
346			db: Arc::new(Mutex::new(Some(native_db))),
347			vfs_metrics: Some(metrics),
348			profiling: Arc::new(profiling::SqliteProfilingState::new(profiling)),
349			..Self::default()
350		}
351	}
352
353	pub fn is_enabled(&self) -> bool {
354		self.enabled
355	}
356
357	pub fn backend(&self) -> SqliteBackend {
358		self.backend
359	}
360
361	#[doc(hidden)]
362	pub fn with_invocation_telemetry(
363		mut self,
364		telemetry: Option<crate::ActorInvocationTelemetry>,
365	) -> Self {
366		self.invocation_telemetry = telemetry;
367		self
368	}
369
370	/// Runs one SQLite operation inside a `rivet.sqlite.*` span when the
371	/// current invocation is traced.
372	pub(super) async fn traced<T>(
373		&self,
374		operation: SqliteOperation,
375		future: impl Future<Output = Result<T>>,
376	) -> Result<T> {
377		let Some(mut span) = self
378			.invocation_telemetry
379			.as_ref()
380			.and_then(|telemetry| telemetry.start_sqlite(operation))
381		else {
382			return future.await;
383		};
384		let result = future.instrument(span.span()).await;
385		span.finish(result.as_ref().err());
386		result
387	}
388
389	pub async fn get_pages(
390		&self,
391		request: protocol::SqliteGetPagesRequest,
392	) -> Result<protocol::SqliteGetPagesResponse> {
393		self.handle()?.sqlite_get_pages(request).await
394	}
395
396	pub async fn commit(
397		&self,
398		request: protocol::SqliteCommitRequest,
399	) -> Result<protocol::SqliteCommitResponse> {
400		self.handle()?.sqlite_commit(request).await
401	}
402
403	pub async fn open(&self) -> Result<()> {
404		match self.backend {
405			SqliteBackend::LocalNative => {
406				#[cfg(feature = "sqlite-local")]
407				{
408					let _open_guard = self.open_lock.lock().await;
409					if self.db.lock().is_some() {
410						return Ok(());
411					}
412
413					let config = self.runtime_config()?;
414					let vfs_metrics = self.vfs_metrics.clone();
415					let rt_handle = tokio::runtime::Handle::try_current()
416						.context("open sqlite database requires a tokio runtime")?;
417					self.worker_fatal_reported.store(false, Ordering::Release);
418
419					let native_db = self.map_local_worker_result(
420						open_database_from_transport(
421							Arc::new(EnvoySqliteTransport::new(config.handle.clone())),
422							config.actor_id.clone(),
423							config
424								.generation
425								.ok_or_else(|| sqlite_not_configured("generation"))?,
426							rt_handle,
427							vfs_metrics,
428						)
429						.await,
430					)?;
431					self.start_worker_failure_monitor(native_db.clone(), config);
432					*self.db.lock() = Some(native_db);
433					if let Some(metrics) = self.vfs_metrics.as_ref() {
434						metrics.set_worker_active(true);
435					}
436					Ok(())
437				}
438
439				#[cfg(not(feature = "sqlite-local"))]
440				{
441					Err(SqliteRuntimeError::Unavailable.build())
442				}
443			}
444			SqliteBackend::RemoteEnvoy => {
445				self.remote_config()?;
446				Ok(())
447			}
448		}
449	}
450
451	#[cfg(feature = "sqlite-local")]
452	async fn local_exec(&self, sql: String) -> Result<QueryResult> {
453		self.open().await?;
454		self.map_local_worker_result(self.native_db_handle()?.exec(sql).await)
455	}
456
457	#[cfg(feature = "sqlite-local")]
458	async fn local_exec_profiled(&self, sql: String) -> Result<SqliteWorkerResult<QueryResult>> {
459		self.open().await?;
460		self.map_local_worker_result(self.native_db_handle()?.exec_profiled(sql).await)
461	}
462
463	#[cfg(not(feature = "sqlite-local"))]
464	async fn local_exec(&self, _sql: String) -> Result<QueryResult> {
465		Err(SqliteRuntimeError::Unavailable.build())
466	}
467
468	#[cfg(feature = "sqlite-local")]
469	async fn local_execute(
470		&self,
471		sql: String,
472		params: Option<Vec<BindParam>>,
473	) -> Result<ExecuteResult> {
474		self.open().await?;
475		self.map_local_worker_result(self.native_db_handle()?.execute(sql, params).await)
476	}
477
478	#[cfg(feature = "sqlite-local")]
479	async fn local_execute_profiled(
480		&self,
481		sql: String,
482		params: Option<Vec<BindParam>>,
483	) -> Result<SqliteWorkerResult<ExecuteResult>> {
484		self.open().await?;
485		self.map_local_worker_result(self.native_db_handle()?.execute_profiled(sql, params).await)
486	}
487
488	#[cfg(not(feature = "sqlite-local"))]
489	async fn local_execute(
490		&self,
491		_sql: String,
492		_params: Option<Vec<BindParam>>,
493	) -> Result<ExecuteResult> {
494		Err(SqliteRuntimeError::Unavailable.build())
495	}
496
497	#[cfg(feature = "sqlite-local")]
498	fn observe_statement_profile(
499		&self,
500		sql: &str,
501		started_at: crate::time::Instant,
502		transaction_wait: std::time::Duration,
503		profile: Option<depot_client::vfs::SqliteOperationProfile>,
504		outcome: &'static str,
505		transaction_mode: &'static str,
506	) -> Option<profiling::StatementObservation> {
507		if self.backend != SqliteBackend::LocalNative {
508			return None;
509		}
510		let fingerprint = self.profiling.statement_fingerprint(sql)?;
511		let observation = profiling::StatementObservation {
512			fingerprint,
513			total_ns: duration_ns(started_at.elapsed()),
514			transaction_wait_ns: duration_ns(transaction_wait),
515			profile: profile.unwrap_or_default(),
516		};
517		if let Some(metrics) = &self.vfs_metrics {
518			let metric = depot_client::vfs::SqliteOperationMetric {
519				operation_type: "statement",
520				fingerprint: observation.fingerprint.display.clone(),
521				fingerprint_source: "query",
522				transaction_mode,
523				storage_transport: "proxy",
524				outcome,
525				sql_bytes: sql.len().try_into().unwrap_or(u64::MAX),
526				total_ns: observation.total_ns,
527				transaction_wait_ns: observation.transaction_wait_ns,
528				profile: observation.profile.clone(),
529			};
530			if metrics.observe_operation_profile(&metric)
531				&& self.profiling.mark_cataloged(&metric.fingerprint)
532			{
533				metrics.record_fingerprint_catalog(
534					"statement",
535					&metric.fingerprint,
536					sql,
537					profiling::FINGERPRINT_FORMAT_VERSION,
538				);
539			}
540			metrics.emit_operation_diagnostic_event(
541				self.actor_id.as_deref().unwrap_or("unknown"),
542				self.generation,
543				&metric,
544			);
545		}
546		Some(observation)
547	}
548
549	pub async fn exec(&self, sql: impl Into<String>) -> Result<QueryResult> {
550		let sql = sql.into();
551		self.traced(SqliteOperation::Exec, self.exec_untraced(sql))
552			.await
553	}
554
555	async fn exec_untraced(&self, sql: String) -> Result<QueryResult> {
556		let sql_for_log = sql.clone();
557		#[cfg(feature = "sqlite-local")]
558		let started_at = self
559			.profiling
560			.config
561			.enabled
562			.then(crate::time::Instant::now);
563		#[cfg(feature = "sqlite-local")]
564		let transaction_wait_started_at = started_at.map(|_| crate::time::Instant::now());
565		let guard = self.begin_regular_operation().await;
566		#[cfg(feature = "sqlite-local")]
567		let transaction_wait = transaction_wait_started_at.map(|started| started.elapsed());
568		#[cfg(feature = "sqlite-local")]
569		let (result, profile) = match guard {
570			Ok(_guard) if started_at.is_some() => {
571				let profiled = self.exec_backend_profiled(sql).await;
572				(profiled.result, profiled.profile)
573			}
574			Ok(_guard) => (self.exec_backend(sql).await, None),
575			Err(error) => (Err(error), None),
576		};
577		#[cfg(not(feature = "sqlite-local"))]
578		let result = match guard {
579			Ok(_guard) => self.exec_backend(sql).await,
580			Err(error) => Err(error),
581		};
582		#[cfg(feature = "sqlite-local")]
583		if let Some(started_at) = started_at {
584			self.observe_statement_profile(
585				&sql_for_log,
586				started_at,
587				transaction_wait.unwrap_or_default(),
588				profile,
589				if result.is_ok() { "success" } else { "error" },
590				"autocommit",
591			);
592		}
593		match result {
594			Ok(result) => Ok(result),
595			Err(error) => {
596				let error = self.attach_actor(error);
597				self.log_operation_error("exec", &sql_for_log, 0, &error);
598				Err(error)
599			}
600		}
601	}
602
603	pub async fn query(
604		&self,
605		sql: impl Into<String>,
606		params: Option<Vec<BindParam>>,
607	) -> Result<QueryResult> {
608		let sql = sql.into();
609		self.traced(SqliteOperation::Query, self.query_untraced(sql, params))
610			.await
611	}
612
613	async fn query_untraced(
614		&self,
615		sql: String,
616		params: Option<Vec<BindParam>>,
617	) -> Result<QueryResult> {
618		let sql_for_log = sql.clone();
619		let binding_count = bind_param_count(&params);
620		#[cfg(feature = "sqlite-local")]
621		let started_at = self
622			.profiling
623			.config
624			.enabled
625			.then(crate::time::Instant::now);
626		#[cfg(feature = "sqlite-local")]
627		let transaction_wait_started_at = started_at.map(|_| crate::time::Instant::now());
628		let guard = self.begin_regular_operation().await;
629		#[cfg(feature = "sqlite-local")]
630		let transaction_wait = transaction_wait_started_at.map(|started| started.elapsed());
631		#[cfg(feature = "sqlite-local")]
632		let (result, profile) = match guard {
633			Ok(_guard) if started_at.is_some() => {
634				let profiled = self.execute_backend_profiled(sql, params).await;
635				(
636					profiled.result.map(|result| QueryResult {
637						columns: result.columns,
638						rows: result.rows,
639					}),
640					profiled.profile,
641				)
642			}
643			Ok(_guard) => (self.query_backend(sql, params).await, None),
644			Err(error) => (Err(error), None),
645		};
646		#[cfg(not(feature = "sqlite-local"))]
647		let result = match guard {
648			Ok(_guard) => self.query_backend(sql, params).await,
649			Err(error) => Err(error),
650		};
651		#[cfg(feature = "sqlite-local")]
652		if let Some(started_at) = started_at {
653			self.observe_statement_profile(
654				&sql_for_log,
655				started_at,
656				transaction_wait.unwrap_or_default(),
657				profile,
658				if result.is_ok() { "success" } else { "error" },
659				"autocommit",
660			);
661		}
662		match result {
663			Ok(result) => Ok(result),
664			Err(error) => {
665				let error = self.attach_actor(error);
666				self.log_operation_error("query", &sql_for_log, binding_count, &error);
667				Err(error)
668			}
669		}
670	}
671
672	pub async fn run(
673		&self,
674		sql: impl Into<String>,
675		params: Option<Vec<BindParam>>,
676	) -> Result<ExecResult> {
677		let sql = sql.into();
678		self.traced(SqliteOperation::Run, self.run_untraced(sql, params))
679			.await
680	}
681
682	async fn run_untraced(
683		&self,
684		sql: String,
685		params: Option<Vec<BindParam>>,
686	) -> Result<ExecResult> {
687		let sql_for_log = sql.clone();
688		let binding_count = bind_param_count(&params);
689		#[cfg(feature = "sqlite-local")]
690		let started_at = self
691			.profiling
692			.config
693			.enabled
694			.then(crate::time::Instant::now);
695		#[cfg(feature = "sqlite-local")]
696		let transaction_wait_started_at = started_at.map(|_| crate::time::Instant::now());
697		let guard = self.begin_regular_operation().await;
698		#[cfg(feature = "sqlite-local")]
699		let transaction_wait = transaction_wait_started_at.map(|started| started.elapsed());
700		#[cfg(feature = "sqlite-local")]
701		let (result, profile) = match guard {
702			Ok(_guard) if started_at.is_some() => {
703				let profiled = self.execute_backend_profiled(sql, params).await;
704				(
705					profiled.result.map(|result| ExecResult {
706						changes: result.changes,
707					}),
708					profiled.profile,
709				)
710			}
711			Ok(_guard) => (self.run_backend(sql, params).await, None),
712			Err(error) => (Err(error), None),
713		};
714		#[cfg(not(feature = "sqlite-local"))]
715		let result = match guard {
716			Ok(_guard) => self.run_backend(sql, params).await,
717			Err(error) => Err(error),
718		};
719		#[cfg(feature = "sqlite-local")]
720		if let Some(started_at) = started_at {
721			self.observe_statement_profile(
722				&sql_for_log,
723				started_at,
724				transaction_wait.unwrap_or_default(),
725				profile,
726				if result.is_ok() { "success" } else { "error" },
727				"autocommit",
728			);
729		}
730		match result {
731			Ok(result) => Ok(result),
732			Err(error) => {
733				let error = self.attach_actor(error);
734				self.log_operation_error("run", &sql_for_log, binding_count, &error);
735				Err(error)
736			}
737		}
738	}
739
740	pub async fn execute(
741		&self,
742		sql: impl Into<String>,
743		params: Option<Vec<BindParam>>,
744	) -> Result<ExecuteResult> {
745		let sql = sql.into();
746		self.traced(SqliteOperation::Execute, self.execute_untraced(sql, params))
747			.await
748	}
749
750	async fn execute_untraced(
751		&self,
752		sql: String,
753		params: Option<Vec<BindParam>>,
754	) -> Result<ExecuteResult> {
755		let sql_for_log = sql.clone();
756		let binding_count = bind_param_count(&params);
757		#[cfg(feature = "sqlite-local")]
758		let started_at = self
759			.profiling
760			.config
761			.enabled
762			.then(crate::time::Instant::now);
763		#[cfg(feature = "sqlite-local")]
764		let transaction_wait_started_at = started_at.map(|_| crate::time::Instant::now());
765		let guard = self.begin_regular_operation().await;
766		#[cfg(feature = "sqlite-local")]
767		let transaction_wait = transaction_wait_started_at.map(|started| started.elapsed());
768		#[cfg(feature = "sqlite-local")]
769		let (result, profile) = match guard {
770			Ok(_guard) if started_at.is_some() => {
771				let profiled = self.execute_backend_profiled(sql, params).await;
772				(profiled.result, profiled.profile)
773			}
774			Ok(_guard) => (self.execute_backend(sql, params).await, None),
775			Err(error) => (Err(error), None),
776		};
777		#[cfg(not(feature = "sqlite-local"))]
778		let result = match guard {
779			Ok(_guard) => self.execute_backend(sql, params).await,
780			Err(error) => Err(error),
781		};
782		#[cfg(feature = "sqlite-local")]
783		if let Some(started_at) = started_at {
784			self.observe_statement_profile(
785				&sql_for_log,
786				started_at,
787				transaction_wait.unwrap_or_default(),
788				profile,
789				if result.is_ok() { "success" } else { "error" },
790				"autocommit",
791			);
792		}
793		match result {
794			Ok(result) => Ok(result),
795			Err(error) => {
796				let error = self.attach_actor(error);
797				self.log_operation_error("execute", &sql_for_log, binding_count, &error);
798				Err(error)
799			}
800		}
801	}
802
803	pub async fn execute_batch(
804		&self,
805		statements: Vec<SqliteBatchStatement>,
806	) -> Result<Vec<ExecuteResult>> {
807		self.traced(
808			SqliteOperation::ExecuteBatch,
809			self.execute_batch_untraced(statements),
810		)
811		.await
812	}
813
814	async fn execute_batch_untraced(
815		&self,
816		statements: Vec<SqliteBatchStatement>,
817	) -> Result<Vec<ExecuteResult>> {
818		let statement_count = statements.len();
819		let binding_count = statements
820			.iter()
821			.map(|statement| bind_param_count(&statement.params))
822			.sum();
823		let result = if self.backend == SqliteBackend::RemoteEnvoy {
824			match self.begin_regular_operation().await {
825				Ok(_guard) => self.remote_execute_batch(statements).await,
826				Err(error) => Err(error),
827			}
828		} else {
829			async {
830				let transaction = self
831					.clone()
832					.with_invocation_telemetry(None)
833					.begin_transaction(None)
834					.await?;
835				let mut results = Vec::with_capacity(statements.len());
836				for statement in statements {
837					match transaction.execute(statement.sql, statement.params).await {
838						Ok(result) => results.push(result),
839						Err(error) => {
840							return match transaction.rollback().await {
841								Ok(()) => Err(error.context("execute sqlite batch statement")),
842								Err(rollback_error) => {
843									Err(error.context("execute sqlite batch statement").context(
844										rollback_error.context("rollback sqlite batch transaction"),
845									))
846								}
847							};
848						}
849					}
850				}
851				transaction
852					.commit()
853					.await
854					.context("commit sqlite batch transaction")?;
855				Ok(results)
856			}
857			.await
858		};
859
860		match result {
861			Ok(results) => Ok(results),
862			Err(error) => {
863				let error = self.attach_actor(error);
864				self.log_operation_error_with_count(
865					"execute_batch",
866					"<batch>",
867					binding_count,
868					statement_count,
869					&error,
870				);
871				Err(error)
872			}
873		}
874	}
875
876	pub async fn close(&self) -> Result<()> {
877		let db = self.clone();
878		run_detached_transaction_task(
879			async move {
880				let _gate = db.shutdown_transaction_coordinator().await;
881				db.close_backend(false).await
882			},
883			"sqlite close task failed",
884		)
885		.await
886	}
887
888	/// Closes SQLite without returning before the native worker terminates.
889	/// Any returned error is reported only after the worker has relinquished SQLite.
890	pub async fn close_and_wait(&self) -> Result<()> {
891		// Keep this strict path in the caller task. Wrapping it in a detached task
892		// would make a task-level join failure ambiguous: the caller could no longer
893		// prove that the worker had relinquished SQLite before replacing the actor.
894		let _gate = self.shutdown_transaction_coordinator().await;
895		self.close_backend(true).await
896	}
897
898	async fn close_backend(&self, _wait_for_worker: bool) -> Result<()> {
899		match self.backend {
900			SqliteBackend::LocalNative => {
901				#[cfg(feature = "sqlite-local")]
902				{
903					let native_db = self.db.lock().clone();
904					if let Some(native_db) = native_db {
905						let close_result = if _wait_for_worker {
906							native_db.close_and_wait().await
907						} else {
908							native_db.close().await
909						};
910						let close_timed_out = close_result.as_ref().err().is_some_and(|error| {
911							error
912								.downcast_ref::<SqliteWorkerCloseTimeoutError>()
913								.is_some()
914						});
915						let release_handle = _wait_for_worker || !close_timed_out;
916						let result = self.map_local_worker_result(close_result);
917						self.abort_worker_failure_monitor();
918						if let Some(metrics) = self.vfs_metrics.as_ref() {
919							metrics.set_worker_active(false);
920						}
921						// Keep the handle after a bounded close timeout. A later strict close
922						// must still be able to observe the worker reaching CLOSED/DEAD before
923						// another actor generation can take ownership of SQLite.
924						if release_handle {
925							self.db.lock().take();
926						}
927						result?;
928					}
929				}
930				Ok(())
931			}
932			SqliteBackend::RemoteEnvoy => Ok(()),
933		}
934	}
935
936	pub(crate) async fn cleanup_for_shutdown(&self, reusable: bool) -> Result<()> {
937		// A remote database has no actor-local resources to release. Keep its
938		// coordinator usable across a sleep/wake cycle on the same ActorTask.
939		if reusable && self.backend == SqliteBackend::RemoteEnvoy {
940			return Ok(());
941		}
942		self.close().await
943	}
944
945	#[cfg(test)]
946	pub(crate) fn fresh_remote_for_test(&self) -> Self {
947		Self::new_with_remote_sqlite(
948			self.handle
949				.clone()
950				.expect("remote sqlite test database should have an envoy handle"),
951			self.actor_id
952				.clone()
953				.expect("remote sqlite test database should have an actor id"),
954			self.actor_key.clone(),
955			self.generation,
956			self.enabled,
957			true,
958		)
959		.expect("remote sqlite test database should be configured")
960	}
961
962	pub fn take_last_kv_error(&self) -> Option<String> {
963		if self.backend != SqliteBackend::LocalNative {
964			return None;
965		}
966
967		#[cfg(feature = "sqlite-local")]
968		{
969			return self
970				.db
971				.lock()
972				.as_ref()
973				.and_then(NativeDatabaseHandle::take_last_kv_error);
974		}
975
976		#[cfg(not(feature = "sqlite-local"))]
977		None
978	}
979
980	#[cfg(feature = "sqlite-local")]
981	fn native_db_handle(&self) -> Result<NativeDatabaseHandle> {
982		self.db
983			.lock()
984			.as_ref()
985			.cloned()
986			.ok_or_else(|| SqliteRuntimeError::Closed.build())
987	}
988
989	#[cfg(feature = "sqlite-local")]
990	fn map_local_worker_result<T>(&self, result: Result<T>) -> Result<T> {
991		match result {
992			Ok(value) => Ok(value),
993			Err(error) => {
994				if is_fatal_worker_error(&error) {
995					self.report_worker_fatal(&error);
996				}
997				Err(map_local_worker_error(error))
998			}
999		}
1000	}
1001
1002	#[cfg(feature = "sqlite-local")]
1003	fn report_worker_fatal(&self, error: &anyhow::Error) {
1004		let Ok(config) = self.runtime_config() else {
1005			return;
1006		};
1007		report_sqlite_worker_fatal(
1008			&self.worker_fatal_reported,
1009			config,
1010			sqlite_worker_fatal_message(error),
1011		);
1012	}
1013
1014	#[cfg(feature = "sqlite-local")]
1015	fn start_worker_failure_monitor(
1016		&self,
1017		native_db: NativeDatabaseHandle,
1018		config: SqliteRuntimeConfig,
1019	) {
1020		self.abort_worker_failure_monitor();
1021		let reported = Arc::clone(&self.worker_fatal_reported);
1022		let task = RuntimeSpawner::spawn(async move {
1023			if native_db.wait_for_worker_failure().await {
1024				report_sqlite_worker_fatal(
1025					&reported,
1026					config,
1027					"sqlite worker thread stopped unexpectedly".to_string(),
1028				);
1029			}
1030		});
1031		*self.worker_failure_task.lock() = Some(task);
1032	}
1033
1034	#[cfg(feature = "sqlite-local")]
1035	fn abort_worker_failure_monitor(&self) {
1036		if let Some(task) = self.worker_failure_task.lock().take() {
1037			task.abort();
1038		}
1039	}
1040
1041	pub fn metrics(&self) -> Option<SqliteVfsMetricsSnapshot> {
1042		#[cfg(feature = "sqlite-local")]
1043		{
1044			self.db
1045				.lock()
1046				.as_ref()
1047				.map(NativeDatabaseHandle::sqlite_vfs_metrics)
1048		}
1049
1050		#[cfg(not(feature = "sqlite-local"))]
1051		{
1052			None
1053		}
1054	}
1055
1056	pub fn runtime_config(&self) -> Result<SqliteRuntimeConfig> {
1057		Ok(SqliteRuntimeConfig {
1058			handle: self.handle()?,
1059			actor_id: self
1060				.actor_id
1061				.clone()
1062				.ok_or_else(|| sqlite_not_configured("actor id"))?,
1063			generation: self.generation,
1064		})
1065	}
1066
1067	fn log_operation_error(
1068		&self,
1069		operation: &'static str,
1070		sql: &str,
1071		binding_count: usize,
1072		error: &anyhow::Error,
1073	) {
1074		self.log_operation_error_with_count(operation, sql, binding_count, 1, error);
1075	}
1076
1077	fn log_operation_error_with_count(
1078		&self,
1079		operation: &'static str,
1080		sql: &str,
1081		binding_count: usize,
1082		statement_count: usize,
1083		error: &anyhow::Error,
1084	) {
1085		let structured = RivetError::extract(error);
1086		let error_chain = error.chain().map(ToString::to_string).collect::<Vec<_>>();
1087		tracing::error!(
1088			actor_id = self.actor_id.as_deref().unwrap_or("<unknown>"),
1089			generation = ?self.generation,
1090			backend = ?self.backend,
1091			operation,
1092			sql,
1093			binding_count,
1094			statement_count,
1095			group = structured.group(),
1096			code = structured.code(),
1097			error_message = %structured.message(),
1098			metadata = ?structured.metadata(),
1099			error_chain = ?error_chain,
1100			"sqlite operation failed"
1101		);
1102	}
1103
1104	fn actor_specifier(&self) -> Option<ActorSpecifier> {
1105		let mut specifier = ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?);
1106		if let Some(key) = self.actor_key.as_ref() {
1107			specifier = specifier.with_key(key.clone());
1108		}
1109		Some(specifier)
1110	}
1111
1112	fn attach_actor(&self, error: anyhow::Error) -> anyhow::Error {
1113		match self.actor_specifier() {
1114			Some(actor) => error.context(actor),
1115			None => error,
1116		}
1117	}
1118
1119	fn remote_config(&self) -> Result<RemoteSqliteConfig> {
1120		let config = self.runtime_config()?;
1121		let generation = config
1122			.generation
1123			.ok_or_else(|| sqlite_not_configured("generation"))?;
1124		Ok(RemoteSqliteConfig {
1125			namespace_id: config.handle.namespace().to_owned(),
1126			handle: config.handle,
1127			actor_id: config.actor_id,
1128			generation,
1129		})
1130	}
1131
1132	async fn remote_exec(&self, sql: String) -> Result<QueryResult> {
1133		let config = self.remote_config()?;
1134		let response = config
1135			.handle
1136			.remote_sqlite_exec(protocol::SqliteExecRequest {
1137				namespace_id: config.namespace_id,
1138				actor_id: config.actor_id,
1139				generation: config.generation,
1140				sql,
1141			})
1142			.await
1143			.map_err(remote_request_error)?;
1144
1145		match response {
1146			protocol::SqliteExecResponse::SqliteExecOk(ok) => {
1147				Ok(query_result_from_protocol(ok.result))
1148			}
1149			protocol::SqliteExecResponse::SqliteErrorResponse(error) => {
1150				Err(self.remote_sqlite_error_response(error))
1151			}
1152		}
1153	}
1154
1155	async fn remote_exec_with_session(
1156		&self,
1157		sql: String,
1158		expected_session: Option<u64>,
1159	) -> Result<(QueryResult, u64)> {
1160		let config = self.remote_config()?;
1161		let (response, session) = config
1162			.handle
1163			.remote_sqlite_exec_with_session(
1164				protocol::SqliteExecRequest {
1165					namespace_id: config.namespace_id,
1166					actor_id: config.actor_id,
1167					generation: config.generation,
1168					sql,
1169				},
1170				expected_session,
1171			)
1172			.await?;
1173
1174		match response {
1175			protocol::SqliteExecResponse::SqliteExecOk(ok) => {
1176				Ok((query_result_from_protocol(ok.result), session))
1177			}
1178			protocol::SqliteExecResponse::SqliteErrorResponse(error) => {
1179				Err(self.remote_sqlite_error_response(error))
1180			}
1181		}
1182	}
1183
1184	async fn remote_execute(
1185		&self,
1186		sql: String,
1187		params: Option<Vec<BindParam>>,
1188	) -> Result<ExecuteResult> {
1189		let config = self.remote_config()?;
1190		let response = config
1191			.handle
1192			.remote_sqlite_execute(protocol::SqliteExecuteRequest {
1193				namespace_id: config.namespace_id,
1194				actor_id: config.actor_id,
1195				generation: config.generation,
1196				sql,
1197				params: params.map(protocol_bind_params),
1198			})
1199			.await
1200			.map_err(remote_request_error)?;
1201
1202		match response {
1203			protocol::SqliteExecuteResponse::SqliteExecuteOk(ok) => {
1204				Ok(execute_result_from_protocol(ok.result))
1205			}
1206			protocol::SqliteExecuteResponse::SqliteErrorResponse(error) => {
1207				Err(self.remote_sqlite_error_response(error))
1208			}
1209		}
1210	}
1211
1212	async fn remote_execute_batch(
1213		&self,
1214		statements: Vec<SqliteBatchStatement>,
1215	) -> Result<Vec<ExecuteResult>> {
1216		let config = self.remote_config()?;
1217		let response = config
1218			.handle
1219			.remote_sqlite_execute_batch(protocol::SqliteExecuteBatchRequest {
1220				namespace_id: config.namespace_id,
1221				actor_id: config.actor_id,
1222				generation: config.generation,
1223				statements: statements
1224					.into_iter()
1225					.map(|statement| protocol::SqliteBatchStatement {
1226						sql: statement.sql,
1227						params: statement.params.map(protocol_bind_params),
1228					})
1229					.collect(),
1230			})
1231			.await
1232			.map_err(remote_request_error)?;
1233
1234		match response {
1235			protocol::SqliteExecuteBatchResponse::SqliteExecuteBatchOk(ok) => Ok(ok
1236				.results
1237				.into_iter()
1238				.map(execute_result_from_protocol)
1239				.collect()),
1240			protocol::SqliteExecuteBatchResponse::SqliteErrorResponse(error) => {
1241				Err(self.remote_sqlite_error_response(error))
1242			}
1243		}
1244	}
1245
1246	async fn remote_execute_with_session(
1247		&self,
1248		sql: String,
1249		params: Option<Vec<BindParam>>,
1250		expected_session: Option<u64>,
1251	) -> Result<(ExecuteResult, u64)> {
1252		let config = self.remote_config()?;
1253		let (response, session) = config
1254			.handle
1255			.remote_sqlite_execute_with_session(
1256				protocol::SqliteExecuteRequest {
1257					namespace_id: config.namespace_id,
1258					actor_id: config.actor_id,
1259					generation: config.generation,
1260					sql,
1261					params: params.map(protocol_bind_params),
1262				},
1263				expected_session,
1264			)
1265			.await?;
1266
1267		match response {
1268			protocol::SqliteExecuteResponse::SqliteExecuteOk(ok) => {
1269				Ok((execute_result_from_protocol(ok.result), session))
1270			}
1271			protocol::SqliteExecuteResponse::SqliteErrorResponse(error) => {
1272				Err(self.remote_sqlite_error_response(error))
1273			}
1274		}
1275	}
1276
1277	pub(crate) async fn query_rows_cbor(
1278		&self,
1279		sql: &str,
1280		params: Option<&[u8]>,
1281	) -> Result<Vec<u8>> {
1282		let bind_params = bind_params_from_cbor(sql, params)?;
1283		let result = self.query(sql.to_owned(), bind_params).await?;
1284		encode_json_as_cbor(&query_result_to_json_rows(&result))
1285	}
1286
1287	pub(crate) async fn exec_rows_cbor(&self, sql: &str) -> Result<Vec<u8>> {
1288		let result = self.exec(sql.to_owned()).await?;
1289		encode_json_as_cbor(&query_result_to_json_rows(&result))
1290	}
1291
1292	pub(crate) async fn run_cbor(&self, sql: &str, params: Option<&[u8]>) -> Result<ExecResult> {
1293		let bind_params = bind_params_from_cbor(sql, params)?;
1294		self.run(sql.to_owned(), bind_params).await
1295	}
1296
1297	pub(crate) async fn execute_rows_cbor(
1298		&self,
1299		sql: &str,
1300		params: Option<&[u8]>,
1301	) -> Result<Vec<u8>> {
1302		let bind_params = bind_params_from_cbor(sql, params)?;
1303		let result = self.execute(sql.to_owned(), bind_params).await?;
1304		encode_json_as_cbor(&query_result_to_json_rows(&QueryResult {
1305			columns: result.columns,
1306			rows: result.rows,
1307		}))
1308	}
1309
1310	fn handle(&self) -> Result<EnvoyHandle> {
1311		self.handle
1312			.clone()
1313			.ok_or_else(|| sqlite_not_configured("handle"))
1314	}
1315
1316	fn remote_sqlite_error_response(&self, error: protocol::SqliteErrorResponse) -> anyhow::Error {
1317		if is_head_fence_mismatch_response(&error) {
1318			if let Ok(config) = self.runtime_config() {
1319				report_sqlite_worker_fatal(
1320					&self.worker_fatal_reported,
1321					config,
1322					format!("remote sqlite fatal storage error: {}", error.message),
1323				);
1324			}
1325			return SqliteRuntimeError::Closed.build();
1326		}
1327
1328		remote_sqlite_error_response(error.message)
1329	}
1330}
1331
1332fn report_sqlite_worker_fatal(reported: &AtomicBool, config: SqliteRuntimeConfig, message: String) {
1333	if reported.swap(true, Ordering::AcqRel) {
1334		return;
1335	}
1336	// A dead worker means SQLite's sole native connection is no longer a valid
1337	// actor subsystem. Core reports that through envoy lifecycle instead of
1338	// letting the actor continue to serve requests with a broken database.
1339	// This is a crash, not a deliberate destroy, so it goes out as a sleep
1340	// intent: the next generation opens a fresh worker over the same durable
1341	// state.
1342	config.handle.sleep_actor(
1343		config.actor_id,
1344		config
1345			.generation
1346			.and_then(|generation| generation.try_into().ok()),
1347		Some(message),
1348	);
1349}
1350
1351struct RemoteSqliteConfig {
1352	handle: EnvoyHandle,
1353	namespace_id: String,
1354	actor_id: String,
1355	generation: u64,
1356}
1357
1358fn select_sqlite_backend(remote_sqlite: bool) -> Result<SqliteBackend> {
1359	if remote_sqlite {
1360		return Ok(SqliteBackend::RemoteEnvoy);
1361	}
1362
1363	#[cfg(feature = "sqlite-local")]
1364	{
1365		Ok(SqliteBackend::LocalNative)
1366	}
1367
1368	#[cfg(not(feature = "sqlite-local"))]
1369	{
1370		Err(SqliteRuntimeError::Unavailable.build())
1371	}
1372}
1373
1374fn bind_param_count(params: &Option<Vec<BindParam>>) -> usize {
1375	params.as_ref().map_or(0, Vec::len)
1376}
1377
1378#[cfg(feature = "sqlite-local")]
1379fn duration_ns(duration: std::time::Duration) -> u64 {
1380	duration.as_nanos().try_into().unwrap_or(u64::MAX)
1381}
1382
1383#[cfg(feature = "sqlite-local")]
1384fn is_fatal_worker_error(error: &anyhow::Error) -> bool {
1385	error.downcast_ref::<SqliteWorkerFatalError>().is_some()
1386		|| error.downcast_ref::<SqliteWorkerDeadError>().is_some()
1387		|| error
1388			.downcast_ref::<SqliteWorkerCloseTimeoutError>()
1389			.is_some()
1390}
1391
1392#[cfg(feature = "sqlite-local")]
1393fn sqlite_worker_fatal_message(error: &anyhow::Error) -> String {
1394	if let Some(error) = error.downcast_ref::<SqliteWorkerFatalError>() {
1395		return format!("sqlite fatal storage error: {}", error.message());
1396	}
1397
1398	format!("sqlite worker failed: {error}")
1399}
1400
1401#[cfg(feature = "sqlite-local")]
1402fn map_local_worker_error(error: anyhow::Error) -> anyhow::Error {
1403	if error
1404		.downcast_ref::<SqliteWorkerOverloadedError>()
1405		.is_some()
1406	{
1407		return ActorLifecycle::Overloaded {
1408			channel: "sqlite_worker".to_string(),
1409			capacity: SQLITE_WORKER_QUEUE_CAPACITY,
1410			operation: "execute sqlite command".to_string(),
1411		}
1412		.build();
1413	}
1414
1415	if error.downcast_ref::<SqliteWorkerClosingError>().is_some()
1416		|| error.downcast_ref::<SqliteWorkerDeadError>().is_some()
1417		|| error.downcast_ref::<SqliteWorkerFatalError>().is_some()
1418	{
1419		return SqliteRuntimeError::Closed.build();
1420	}
1421
1422	error
1423}
1424
1425fn protocol_bind_params(params: Vec<BindParam>) -> Vec<protocol::SqliteBindParam> {
1426	params.into_iter().map(protocol_bind_param).collect()
1427}
1428
1429fn protocol_bind_param(param: BindParam) -> protocol::SqliteBindParam {
1430	match param {
1431		BindParam::Null => protocol::SqliteBindParam::SqliteValueNull,
1432		BindParam::Integer(value) => {
1433			protocol::SqliteBindParam::SqliteValueInteger(protocol::SqliteValueInteger { value })
1434		}
1435		BindParam::Float(value) => {
1436			protocol::SqliteBindParam::SqliteValueFloat(protocol::SqliteValueFloat {
1437				value: value.to_bits().to_be_bytes(),
1438			})
1439		}
1440		BindParam::Text(value) => {
1441			protocol::SqliteBindParam::SqliteValueText(protocol::SqliteValueText { value })
1442		}
1443		BindParam::Blob(value) => {
1444			protocol::SqliteBindParam::SqliteValueBlob(protocol::SqliteValueBlob { value })
1445		}
1446	}
1447}
1448
1449fn query_result_from_protocol(result: protocol::SqliteQueryResult) -> QueryResult {
1450	QueryResult {
1451		columns: result.columns,
1452		rows: result
1453			.rows
1454			.into_iter()
1455			.map(|row| row.into_iter().map(column_value_from_protocol).collect())
1456			.collect(),
1457	}
1458}
1459
1460fn execute_result_from_protocol(result: protocol::SqliteExecuteResult) -> ExecuteResult {
1461	ExecuteResult {
1462		columns: result.columns,
1463		rows: result
1464			.rows
1465			.into_iter()
1466			.map(|row| row.into_iter().map(column_value_from_protocol).collect())
1467			.collect(),
1468		changes: result.changes,
1469		last_insert_row_id: result.last_insert_row_id,
1470	}
1471}
1472
1473fn column_value_from_protocol(value: protocol::SqliteColumnValue) -> ColumnValue {
1474	match value {
1475		protocol::SqliteColumnValue::SqliteValueNull => ColumnValue::Null,
1476		protocol::SqliteColumnValue::SqliteValueInteger(value) => ColumnValue::Integer(value.value),
1477		protocol::SqliteColumnValue::SqliteValueFloat(value) => {
1478			ColumnValue::Float(f64::from_bits(u64::from_be_bytes(value.value)))
1479		}
1480		protocol::SqliteColumnValue::SqliteValueText(value) => ColumnValue::Text(value.value),
1481		protocol::SqliteColumnValue::SqliteValueBlob(value) => ColumnValue::Blob(value.value),
1482	}
1483}
1484
1485fn remote_request_error(error: anyhow::Error) -> anyhow::Error {
1486	if let Some(indeterminate) = error.downcast_ref::<RemoteSqliteIndeterminateResultError>() {
1487		return SqliteRuntimeError::RemoteIndeterminateResult {
1488			operation: indeterminate.operation.to_owned(),
1489		}
1490		.build();
1491	}
1492
1493	if let Some(compatibility) =
1494		error.downcast_ref::<protocol::versioned::ProtocolCompatibilityError>()
1495	{
1496		if compatibility.feature
1497			== protocol::versioned::ProtocolCompatibilityFeature::RemoteSqliteExecution
1498		{
1499			return SqliteRuntimeError::RemoteUnavailable {
1500				reason: compatibility.to_string(),
1501			}
1502			.build();
1503		}
1504	}
1505
1506	error
1507}
1508
1509fn remote_sqlite_error_response(message: String) -> anyhow::Error {
1510	if message.contains("unavailable") || message.contains("unsupported") {
1511		return SqliteRuntimeError::RemoteUnavailable { reason: message }.build();
1512	}
1513
1514	SqliteRuntimeError::RemoteExecutionFailed { message }.build()
1515}
1516
1517fn is_head_fence_mismatch_response(error: &protocol::SqliteErrorResponse) -> bool {
1518	is_head_fence_mismatch(&error.group, &error.code)
1519}
1520impl std::fmt::Debug for SqliteDb {
1521	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1522		f.debug_struct("SqliteDb")
1523			.field("configured", &self.handle.is_some())
1524			.field("actor_id", &self.actor_id)
1525			.finish()
1526	}
1527}
1528
1529fn bind_params_from_cbor(sql: &str, params: Option<&[u8]>) -> Result<Option<Vec<BindParam>>> {
1530	let Some(params) = params else {
1531		return Ok(None);
1532	};
1533	if params.is_empty() {
1534		return Ok(None);
1535	}
1536
1537	let value = ciborium::from_reader::<JsonValue, _>(Cursor::new(params))
1538		.context("decode sqlite bind params as cbor json")?;
1539	match value {
1540		JsonValue::Array(values) => values
1541			.iter()
1542			.map(json_to_bind_param)
1543			.collect::<Result<Vec<_>>>()
1544			.map(Some),
1545		JsonValue::Object(properties) => {
1546			let ordered_names = extract_named_sqlite_parameters(sql);
1547			if ordered_names.is_empty() {
1548				return properties
1549					.values()
1550					.map(json_to_bind_param)
1551					.collect::<Result<Vec<_>>>()
1552					.map(Some);
1553			}
1554
1555			ordered_names
1556				.iter()
1557				.map(|name| {
1558					get_named_sqlite_binding(&properties, name)
1559						.ok_or_else(|| {
1560							SqliteRuntimeError::InvalidBindParameter {
1561								name: name.clone(),
1562								reason: "missing parameter".to_owned(),
1563							}
1564							.build()
1565						})
1566						.and_then(json_to_bind_param)
1567				})
1568				.collect::<Result<Vec<_>>>()
1569				.map(Some)
1570		}
1571		JsonValue::Null => Ok(None),
1572		other => Err(SqliteRuntimeError::InvalidBindParameter {
1573			name: "params".to_owned(),
1574			reason: format!("expected array or object, got {}", json_type_name(&other)),
1575		}
1576		.build()),
1577	}
1578}
1579
1580fn json_to_bind_param(value: &JsonValue) -> Result<BindParam> {
1581	match value {
1582		JsonValue::Null => Ok(BindParam::Null),
1583		JsonValue::Bool(value) => Ok(BindParam::Integer(i64::from(*value))),
1584		JsonValue::Number(value) => {
1585			if let Some(value) = value.as_i64() {
1586				return Ok(BindParam::Integer(value));
1587			}
1588			if let Some(value) = value.as_u64() {
1589				let value = i64::try_from(value)
1590					.context("sqlite integer bind parameter exceeds i64 range")?;
1591				return Ok(BindParam::Integer(value));
1592			}
1593			value.as_f64().map(BindParam::Float).ok_or_else(|| {
1594				SqliteRuntimeError::InvalidBindParameter {
1595					name: "number".to_owned(),
1596					reason: "unsupported numeric value".to_owned(),
1597				}
1598				.build()
1599			})
1600		}
1601		JsonValue::String(value) => Ok(BindParam::Text(value.clone())),
1602		other => Err(SqliteRuntimeError::InvalidBindParameter {
1603			name: "value".to_owned(),
1604			reason: format!("unsupported type {}", json_type_name(other)),
1605		}
1606		.build()),
1607	}
1608}
1609
1610fn sqlite_not_configured(component: &str) -> anyhow::Error {
1611	SqliteRuntimeError::NotConfigured {
1612		component: component.to_owned(),
1613	}
1614	.build()
1615}
1616
1617fn extract_named_sqlite_parameters(sql: &str) -> Vec<String> {
1618	let mut ordered_names = Vec::new();
1619	let mut seen = HashSet::new();
1620	let bytes = sql.as_bytes();
1621	let mut idx = 0;
1622
1623	while idx < bytes.len() {
1624		let byte = bytes[idx];
1625		if !matches!(byte, b':' | b'@' | b'$') {
1626			idx += 1;
1627			continue;
1628		}
1629
1630		let start = idx;
1631		idx += 1;
1632		if idx >= bytes.len() || !is_sqlite_param_start(bytes[idx]) {
1633			continue;
1634		}
1635		idx += 1;
1636		while idx < bytes.len() && is_sqlite_param_continue(bytes[idx]) {
1637			idx += 1;
1638		}
1639
1640		let name = &sql[start..idx];
1641		if seen.insert(name.to_owned()) {
1642			ordered_names.push(name.to_owned());
1643		}
1644	}
1645
1646	ordered_names
1647}
1648
1649fn is_sqlite_param_start(byte: u8) -> bool {
1650	byte == b'_' || byte.is_ascii_alphabetic()
1651}
1652
1653fn is_sqlite_param_continue(byte: u8) -> bool {
1654	byte == b'_' || byte.is_ascii_alphanumeric()
1655}
1656
1657fn get_named_sqlite_binding<'a>(
1658	bindings: &'a JsonMap<String, JsonValue>,
1659	name: &str,
1660) -> Option<&'a JsonValue> {
1661	if let Some(value) = bindings.get(name) {
1662		return Some(value);
1663	}
1664
1665	let bare_name = name.get(1..)?;
1666	if let Some(value) = bindings.get(bare_name) {
1667		return Some(value);
1668	}
1669
1670	for prefix in [":", "@", "$"] {
1671		let candidate = format!("{prefix}{bare_name}");
1672		if let Some(value) = bindings.get(&candidate) {
1673			return Some(value);
1674		}
1675	}
1676
1677	None
1678}
1679
1680fn query_result_to_json_rows(result: &QueryResult) -> JsonValue {
1681	JsonValue::Array(
1682		result
1683			.rows
1684			.iter()
1685			.map(|row| {
1686				let mut object = JsonMap::new();
1687				for (index, column) in result.columns.iter().enumerate() {
1688					let value = row
1689						.get(index)
1690						.map(column_value_to_json)
1691						.unwrap_or(JsonValue::Null);
1692					object.insert(column.clone(), value);
1693				}
1694				JsonValue::Object(object)
1695			})
1696			.collect(),
1697	)
1698}
1699
1700fn column_value_to_json(value: &ColumnValue) -> JsonValue {
1701	match value {
1702		ColumnValue::Null => JsonValue::Null,
1703		ColumnValue::Integer(value) => JsonValue::from(*value),
1704		ColumnValue::Float(value) => JsonValue::from(*value),
1705		ColumnValue::Text(value) => JsonValue::String(value.clone()),
1706		ColumnValue::Blob(value) => {
1707			JsonValue::Array(value.iter().map(|byte| JsonValue::from(*byte)).collect())
1708		}
1709	}
1710}
1711
1712fn encode_json_as_cbor(value: &impl Serialize) -> Result<Vec<u8>> {
1713	let mut encoded = Vec::new();
1714	ciborium::into_writer(value, &mut encoded).context("encode sqlite rows as cbor")?;
1715	Ok(encoded)
1716}
1717
1718fn json_type_name(value: &JsonValue) -> &'static str {
1719	match value {
1720		JsonValue::Null => "null",
1721		JsonValue::Bool(_) => "boolean",
1722		JsonValue::Number(_) => "number",
1723		JsonValue::String(_) => "string",
1724		JsonValue::Array(_) => "array",
1725		JsonValue::Object(_) => "object",
1726	}
1727}
1728
1729#[cfg(test)]
1730#[path = "../../../tests/sqlite.rs"]
1731mod tests;