Skip to main content

rivetkit_core/actor/
sqlite.rs

1use std::collections::HashSet;
2use std::io::Cursor;
3use std::sync::{
4	Arc,
5	atomic::{AtomicBool, Ordering},
6};
7
8use anyhow::{Context, Result};
9use depot_client_types::is_head_fence_mismatch;
10pub use depot_client_types::{BindParam, ColumnValue, ExecResult, ExecuteResult, QueryResult};
11#[cfg(feature = "sqlite-local")]
12use parking_lot::Mutex;
13use rivet_envoy_client::protocol;
14use rivet_envoy_client::{handle::EnvoyHandle, utils::RemoteSqliteIndeterminateResultError};
15use rivet_error::{ActorSpecifier, RivetError};
16use serde::Serialize;
17use serde_json::{Map as JsonMap, Value as JsonValue};
18#[cfg(feature = "sqlite-local")]
19use tokio::sync::Mutex as AsyncMutex;
20#[cfg(feature = "sqlite-local")]
21use tokio::task::JoinHandle;
22
23#[cfg(feature = "sqlite-local")]
24mod envoy_sqlite_transport;
25
26#[cfg(feature = "sqlite-local")]
27use crate::error::ActorLifecycle;
28use crate::error::SqliteRuntimeError;
29#[cfg(feature = "sqlite-local")]
30use crate::runtime::RuntimeSpawner;
31
32#[cfg(feature = "sqlite-local")]
33use depot_client::{
34	database::{NativeDatabaseHandle, open_database_from_transport},
35	vfs::{SqliteVfsMetrics, SqliteVfsMetricsSnapshot},
36	worker::{
37		SQLITE_WORKER_QUEUE_CAPACITY, SqliteWorkerCloseTimeoutError, SqliteWorkerClosingError,
38		SqliteWorkerDeadError, SqliteWorkerFatalError, SqliteWorkerOverloadedError,
39	},
40};
41#[cfg(feature = "sqlite-local")]
42use envoy_sqlite_transport::EnvoySqliteTransport;
43
44#[cfg(not(feature = "sqlite-local"))]
45#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub struct SqliteVfsMetricsSnapshot {
47	pub request_build_ns: u64,
48	pub serialize_ns: u64,
49	pub transport_ns: u64,
50	pub state_update_ns: u64,
51	pub total_ns: u64,
52	pub commit_count: u64,
53}
54
55#[derive(Clone)]
56pub struct SqliteRuntimeConfig {
57	pub handle: EnvoyHandle,
58	pub actor_id: String,
59	pub generation: Option<u64>,
60}
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub enum SqliteBackend {
64	LocalNative,
65	RemoteEnvoy,
66	Unavailable,
67}
68
69impl Default for SqliteBackend {
70	fn default() -> Self {
71		Self::Unavailable
72	}
73}
74
75#[derive(Clone, Default)]
76pub struct SqliteDb {
77	handle: Option<EnvoyHandle>,
78	actor_id: Option<String>,
79	actor_key: Option<String>,
80	generation: Option<u64>,
81	backend: SqliteBackend,
82	/// Mirrors the user's actor-config `db({...})` declaration. The envoy
83	/// always sets up sqlite storage under the hood, so handle/actor_id are
84	/// not a reliable signal for whether the user opted in; this flag is.
85	enabled: bool,
86	#[cfg(feature = "sqlite-local")]
87	// Forced-sync: native SQLite handles are used inside spawn_blocking and
88	// synchronous diagnostic accessors.
89	db: Arc<Mutex<Option<NativeDatabaseHandle>>>,
90	#[cfg(feature = "sqlite-local")]
91	open_lock: Arc<AsyncMutex<()>>,
92	#[cfg(feature = "sqlite-local")]
93	worker_failure_task: Arc<Mutex<Option<JoinHandle<()>>>>,
94	worker_fatal_reported: Arc<AtomicBool>,
95	#[cfg(feature = "sqlite-local")]
96	vfs_metrics: Option<Arc<dyn SqliteVfsMetrics>>,
97}
98
99impl SqliteDb {
100	pub fn new(handle: EnvoyHandle, actor_id: impl Into<String>, enabled: bool) -> Self {
101		Self::new_with_remote_sqlite(handle, actor_id, None, None, enabled, false)
102	}
103
104	pub fn new_with_remote_sqlite(
105		handle: EnvoyHandle,
106		actor_id: impl Into<String>,
107		actor_key: Option<String>,
108		generation: Option<u64>,
109		enabled: bool,
110		remote_sqlite: bool,
111	) -> Self {
112		Self {
113			handle: Some(handle),
114			actor_id: Some(actor_id.into()),
115			actor_key,
116			generation,
117			backend: select_sqlite_backend(enabled, remote_sqlite),
118			enabled,
119			#[cfg(feature = "sqlite-local")]
120			db: Default::default(),
121			#[cfg(feature = "sqlite-local")]
122			open_lock: Default::default(),
123			#[cfg(feature = "sqlite-local")]
124			worker_failure_task: Default::default(),
125			worker_fatal_reported: Default::default(),
126			#[cfg(feature = "sqlite-local")]
127			vfs_metrics: None,
128		}
129	}
130
131	#[cfg(feature = "sqlite-local")]
132	pub(crate) fn set_vfs_metrics(&mut self, metrics: Arc<dyn SqliteVfsMetrics>) {
133		self.vfs_metrics = Some(metrics);
134	}
135
136	pub fn is_enabled(&self) -> bool {
137		self.enabled
138	}
139
140	pub fn backend(&self) -> SqliteBackend {
141		self.backend
142	}
143
144	pub async fn get_pages(
145		&self,
146		request: protocol::SqliteGetPagesRequest,
147	) -> Result<protocol::SqliteGetPagesResponse> {
148		self.handle()?.sqlite_get_pages(request).await
149	}
150
151	pub async fn commit(
152		&self,
153		request: protocol::SqliteCommitRequest,
154	) -> Result<protocol::SqliteCommitResponse> {
155		self.handle()?.sqlite_commit(request).await
156	}
157
158	pub async fn open(&self) -> Result<()> {
159		match self.backend {
160			SqliteBackend::LocalNative => {
161				#[cfg(feature = "sqlite-local")]
162				{
163					let _open_guard = self.open_lock.lock().await;
164					if self.db.lock().is_some() {
165						return Ok(());
166					}
167
168					let config = self.runtime_config()?;
169					let vfs_metrics = self.vfs_metrics.clone();
170					let rt_handle = tokio::runtime::Handle::try_current()
171						.context("open sqlite database requires a tokio runtime")?;
172					self.worker_fatal_reported.store(false, Ordering::Release);
173
174					let native_db = self.map_local_worker_result(
175						open_database_from_transport(
176							Arc::new(EnvoySqliteTransport::new(config.handle.clone())),
177							config.actor_id.clone(),
178							config
179								.generation
180								.ok_or_else(|| sqlite_not_configured("generation"))?,
181							rt_handle,
182							vfs_metrics,
183						)
184						.await,
185					)?;
186					self.start_worker_failure_monitor(native_db.clone(), config);
187					*self.db.lock() = Some(native_db);
188					if let Some(metrics) = self.vfs_metrics.as_ref() {
189						metrics.set_worker_active(true);
190					}
191					Ok(())
192				}
193
194				#[cfg(not(feature = "sqlite-local"))]
195				{
196					Err(SqliteRuntimeError::Unavailable.build())
197				}
198			}
199			SqliteBackend::RemoteEnvoy => {
200				self.remote_config()?;
201				Ok(())
202			}
203			SqliteBackend::Unavailable => Err(SqliteRuntimeError::Unavailable.build()),
204		}
205	}
206
207	#[cfg(feature = "sqlite-local")]
208	async fn local_exec(&self, sql: String) -> Result<QueryResult> {
209		self.open().await?;
210		self.map_local_worker_result(self.native_db_handle()?.exec(sql).await)
211	}
212
213	#[cfg(not(feature = "sqlite-local"))]
214	async fn local_exec(&self, _sql: String) -> Result<QueryResult> {
215		Err(SqliteRuntimeError::Unavailable.build())
216	}
217
218	#[cfg(feature = "sqlite-local")]
219	async fn local_query(
220		&self,
221		sql: String,
222		params: Option<Vec<BindParam>>,
223	) -> Result<QueryResult> {
224		self.open().await?;
225		self.map_local_worker_result(self.native_db_handle()?.query(sql, params).await)
226	}
227
228	#[cfg(not(feature = "sqlite-local"))]
229	async fn local_query(
230		&self,
231		_sql: String,
232		_params: Option<Vec<BindParam>>,
233	) -> Result<QueryResult> {
234		Err(SqliteRuntimeError::Unavailable.build())
235	}
236
237	#[cfg(feature = "sqlite-local")]
238	async fn local_run(&self, sql: String, params: Option<Vec<BindParam>>) -> Result<ExecResult> {
239		self.open().await?;
240		self.map_local_worker_result(self.native_db_handle()?.run(sql, params).await)
241	}
242
243	#[cfg(not(feature = "sqlite-local"))]
244	async fn local_run(&self, _sql: String, _params: Option<Vec<BindParam>>) -> Result<ExecResult> {
245		Err(SqliteRuntimeError::Unavailable.build())
246	}
247
248	#[cfg(feature = "sqlite-local")]
249	async fn local_execute(
250		&self,
251		sql: String,
252		params: Option<Vec<BindParam>>,
253	) -> Result<ExecuteResult> {
254		self.open().await?;
255		self.map_local_worker_result(self.native_db_handle()?.execute(sql, params).await)
256	}
257
258	#[cfg(not(feature = "sqlite-local"))]
259	async fn local_execute(
260		&self,
261		_sql: String,
262		_params: Option<Vec<BindParam>>,
263	) -> Result<ExecuteResult> {
264		Err(SqliteRuntimeError::Unavailable.build())
265	}
266
267	pub async fn exec(&self, sql: impl Into<String>) -> Result<QueryResult> {
268		let sql = sql.into();
269		let sql_for_log = sql.clone();
270		let result = match self.backend {
271			SqliteBackend::LocalNative => self.local_exec(sql).await,
272			SqliteBackend::RemoteEnvoy => self.remote_exec(sql).await,
273			SqliteBackend::Unavailable => Err(SqliteRuntimeError::Unavailable.build()),
274		};
275		match result {
276			Ok(result) => Ok(result),
277			Err(error) => {
278				let error = self.attach_actor(error);
279				self.log_operation_error("exec", &sql_for_log, 0, &error);
280				Err(error)
281			}
282		}
283	}
284
285	pub async fn query(
286		&self,
287		sql: impl Into<String>,
288		params: Option<Vec<BindParam>>,
289	) -> Result<QueryResult> {
290		let sql = sql.into();
291		let sql_for_log = sql.clone();
292		let binding_count = bind_param_count(&params);
293		let result = match self.backend {
294			SqliteBackend::LocalNative => self.local_query(sql, params).await,
295			SqliteBackend::RemoteEnvoy => self
296				.remote_execute(sql, params)
297				.await
298				.map(ExecuteResult::into_query_result),
299			SqliteBackend::Unavailable => Err(SqliteRuntimeError::Unavailable.build()),
300		};
301		match result {
302			Ok(result) => Ok(result),
303			Err(error) => {
304				let error = self.attach_actor(error);
305				self.log_operation_error("query", &sql_for_log, binding_count, &error);
306				Err(error)
307			}
308		}
309	}
310
311	pub async fn run(
312		&self,
313		sql: impl Into<String>,
314		params: Option<Vec<BindParam>>,
315	) -> Result<ExecResult> {
316		let sql = sql.into();
317		let sql_for_log = sql.clone();
318		let binding_count = bind_param_count(&params);
319		let result = match self.backend {
320			SqliteBackend::LocalNative => self.local_run(sql, params).await,
321			SqliteBackend::RemoteEnvoy => self
322				.remote_execute(sql, params)
323				.await
324				.map(ExecuteResult::into_exec_result),
325			SqliteBackend::Unavailable => Err(SqliteRuntimeError::Unavailable.build()),
326		};
327		match result {
328			Ok(result) => Ok(result),
329			Err(error) => {
330				let error = self.attach_actor(error);
331				self.log_operation_error("run", &sql_for_log, binding_count, &error);
332				Err(error)
333			}
334		}
335	}
336
337	pub async fn execute(
338		&self,
339		sql: impl Into<String>,
340		params: Option<Vec<BindParam>>,
341	) -> Result<ExecuteResult> {
342		let sql = sql.into();
343		let sql_for_log = sql.clone();
344		let binding_count = bind_param_count(&params);
345		let result = match self.backend {
346			SqliteBackend::LocalNative => self.local_execute(sql, params).await,
347			SqliteBackend::RemoteEnvoy => self.remote_execute(sql, params).await,
348			SqliteBackend::Unavailable => Err(SqliteRuntimeError::Unavailable.build()),
349		};
350		match result {
351			Ok(result) => Ok(result),
352			Err(error) => {
353				let error = self.attach_actor(error);
354				self.log_operation_error("execute", &sql_for_log, binding_count, &error);
355				Err(error)
356			}
357		}
358	}
359
360	pub async fn close(&self) -> Result<()> {
361		match self.backend {
362			SqliteBackend::LocalNative => {
363				#[cfg(feature = "sqlite-local")]
364				{
365					let native_db = self.db.lock().take();
366					if let Some(native_db) = native_db {
367						let result = self.map_local_worker_result(native_db.close().await);
368						self.abort_worker_failure_monitor();
369						if let Some(metrics) = self.vfs_metrics.as_ref() {
370							metrics.set_worker_active(false);
371						}
372						result?;
373					}
374				}
375				Ok(())
376			}
377			SqliteBackend::RemoteEnvoy | SqliteBackend::Unavailable => Ok(()),
378		}
379	}
380
381	pub(crate) async fn cleanup(&self) -> Result<()> {
382		self.close().await
383	}
384
385	pub fn take_last_kv_error(&self) -> Option<String> {
386		if self.backend != SqliteBackend::LocalNative {
387			return None;
388		}
389
390		#[cfg(feature = "sqlite-local")]
391		{
392			return self
393				.db
394				.lock()
395				.as_ref()
396				.and_then(NativeDatabaseHandle::take_last_kv_error);
397		}
398
399		#[cfg(not(feature = "sqlite-local"))]
400		None
401	}
402
403	#[cfg(feature = "sqlite-local")]
404	fn native_db_handle(&self) -> Result<NativeDatabaseHandle> {
405		self.db
406			.lock()
407			.as_ref()
408			.cloned()
409			.ok_or_else(|| SqliteRuntimeError::Closed.build())
410	}
411
412	#[cfg(feature = "sqlite-local")]
413	fn map_local_worker_result<T>(&self, result: Result<T>) -> Result<T> {
414		match result {
415			Ok(value) => Ok(value),
416			Err(error) => {
417				if is_fatal_worker_error(&error) {
418					self.report_worker_fatal(&error);
419				}
420				Err(map_local_worker_error(error))
421			}
422		}
423	}
424
425	#[cfg(feature = "sqlite-local")]
426	fn report_worker_fatal(&self, error: &anyhow::Error) {
427		let Ok(config) = self.runtime_config() else {
428			return;
429		};
430		report_sqlite_worker_fatal(
431			&self.worker_fatal_reported,
432			config,
433			sqlite_worker_fatal_message(error),
434		);
435	}
436
437	#[cfg(feature = "sqlite-local")]
438	fn start_worker_failure_monitor(
439		&self,
440		native_db: NativeDatabaseHandle,
441		config: SqliteRuntimeConfig,
442	) {
443		self.abort_worker_failure_monitor();
444		let reported = Arc::clone(&self.worker_fatal_reported);
445		let task = RuntimeSpawner::spawn(async move {
446			if native_db.wait_for_worker_failure().await {
447				report_sqlite_worker_fatal(
448					&reported,
449					config,
450					"sqlite worker thread stopped unexpectedly".to_string(),
451				);
452			}
453		});
454		*self.worker_failure_task.lock() = Some(task);
455	}
456
457	#[cfg(feature = "sqlite-local")]
458	fn abort_worker_failure_monitor(&self) {
459		if let Some(task) = self.worker_failure_task.lock().take() {
460			task.abort();
461		}
462	}
463
464	pub fn metrics(&self) -> Option<SqliteVfsMetricsSnapshot> {
465		#[cfg(feature = "sqlite-local")]
466		{
467			self.db
468				.lock()
469				.as_ref()
470				.map(NativeDatabaseHandle::sqlite_vfs_metrics)
471		}
472
473		#[cfg(not(feature = "sqlite-local"))]
474		{
475			None
476		}
477	}
478
479	pub fn runtime_config(&self) -> Result<SqliteRuntimeConfig> {
480		Ok(SqliteRuntimeConfig {
481			handle: self.handle()?,
482			actor_id: self
483				.actor_id
484				.clone()
485				.ok_or_else(|| sqlite_not_configured("actor id"))?,
486			generation: self.generation,
487		})
488	}
489
490	fn log_operation_error(
491		&self,
492		operation: &'static str,
493		sql: &str,
494		binding_count: usize,
495		error: &anyhow::Error,
496	) {
497		let structured = RivetError::extract(error);
498		let error_chain = error.chain().map(ToString::to_string).collect::<Vec<_>>();
499		tracing::error!(
500			actor_id = self.actor_id.as_deref().unwrap_or("<unknown>"),
501			generation = ?self.generation,
502			backend = ?self.backend,
503			operation,
504			sql,
505			binding_count,
506			group = structured.group(),
507			code = structured.code(),
508			error_message = %structured.message(),
509			metadata = ?structured.metadata(),
510			error_chain = ?error_chain,
511			"sqlite operation failed"
512		);
513	}
514
515	fn actor_specifier(&self) -> Option<ActorSpecifier> {
516		let mut specifier = ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?);
517		if let Some(key) = self.actor_key.as_ref() {
518			specifier = specifier.with_key(key.clone());
519		}
520		Some(specifier)
521	}
522
523	fn attach_actor(&self, error: anyhow::Error) -> anyhow::Error {
524		match self.actor_specifier() {
525			Some(actor) => error.context(actor),
526			None => error,
527		}
528	}
529
530	fn remote_config(&self) -> Result<RemoteSqliteConfig> {
531		let config = self.runtime_config()?;
532		let generation = config
533			.generation
534			.ok_or_else(|| sqlite_not_configured("generation"))?;
535		Ok(RemoteSqliteConfig {
536			namespace_id: config.handle.namespace().to_owned(),
537			handle: config.handle,
538			actor_id: config.actor_id,
539			generation,
540		})
541	}
542
543	async fn remote_exec(&self, sql: String) -> Result<QueryResult> {
544		let config = self.remote_config()?;
545		let response = config
546			.handle
547			.remote_sqlite_exec(protocol::SqliteExecRequest {
548				namespace_id: config.namespace_id,
549				actor_id: config.actor_id,
550				generation: config.generation,
551				sql,
552			})
553			.await
554			.map_err(remote_request_error)?;
555
556		match response {
557			protocol::SqliteExecResponse::SqliteExecOk(ok) => {
558				Ok(query_result_from_protocol(ok.result))
559			}
560			protocol::SqliteExecResponse::SqliteErrorResponse(error) => {
561				Err(self.remote_sqlite_error_response(error))
562			}
563		}
564	}
565
566	async fn remote_execute(
567		&self,
568		sql: String,
569		params: Option<Vec<BindParam>>,
570	) -> Result<ExecuteResult> {
571		let config = self.remote_config()?;
572		let response = config
573			.handle
574			.remote_sqlite_execute(protocol::SqliteExecuteRequest {
575				namespace_id: config.namespace_id,
576				actor_id: config.actor_id,
577				generation: config.generation,
578				sql,
579				params: params.map(protocol_bind_params),
580			})
581			.await
582			.map_err(remote_request_error)?;
583
584		match response {
585			protocol::SqliteExecuteResponse::SqliteExecuteOk(ok) => {
586				Ok(execute_result_from_protocol(ok.result))
587			}
588			protocol::SqliteExecuteResponse::SqliteErrorResponse(error) => {
589				Err(self.remote_sqlite_error_response(error))
590			}
591		}
592	}
593
594	pub(crate) async fn query_rows_cbor(
595		&self,
596		sql: &str,
597		params: Option<&[u8]>,
598	) -> Result<Vec<u8>> {
599		let bind_params = bind_params_from_cbor(sql, params)?;
600		let result = self.query(sql.to_owned(), bind_params).await?;
601		encode_json_as_cbor(&query_result_to_json_rows(&result))
602	}
603
604	pub(crate) async fn exec_rows_cbor(&self, sql: &str) -> Result<Vec<u8>> {
605		let result = self.exec(sql.to_owned()).await?;
606		encode_json_as_cbor(&query_result_to_json_rows(&result))
607	}
608
609	pub(crate) async fn run_cbor(&self, sql: &str, params: Option<&[u8]>) -> Result<ExecResult> {
610		let bind_params = bind_params_from_cbor(sql, params)?;
611		self.run(sql.to_owned(), bind_params).await
612	}
613
614	pub(crate) async fn execute_rows_cbor(
615		&self,
616		sql: &str,
617		params: Option<&[u8]>,
618	) -> Result<Vec<u8>> {
619		let bind_params = bind_params_from_cbor(sql, params)?;
620		let result = self.execute(sql.to_owned(), bind_params).await?;
621		encode_json_as_cbor(&query_result_to_json_rows(&QueryResult {
622			columns: result.columns,
623			rows: result.rows,
624		}))
625	}
626
627	fn handle(&self) -> Result<EnvoyHandle> {
628		self.handle
629			.clone()
630			.ok_or_else(|| sqlite_not_configured("handle"))
631	}
632
633	fn remote_sqlite_error_response(&self, error: protocol::SqliteErrorResponse) -> anyhow::Error {
634		if is_head_fence_mismatch_response(&error) {
635			if let Ok(config) = self.runtime_config() {
636				report_sqlite_worker_fatal(
637					&self.worker_fatal_reported,
638					config,
639					format!("remote sqlite fatal storage error: {}", error.message),
640				);
641			}
642			return SqliteRuntimeError::Closed.build();
643		}
644
645		remote_sqlite_error_response(error.message)
646	}
647}
648
649fn report_sqlite_worker_fatal(reported: &AtomicBool, config: SqliteRuntimeConfig, message: String) {
650	if reported.swap(true, Ordering::AcqRel) {
651		return;
652	}
653	// A dead worker means SQLite's sole native connection is no longer a valid
654	// actor subsystem. Core reports that through envoy lifecycle instead of
655	// letting the actor continue to serve requests with a broken database.
656	config.handle.stop_actor(
657		config.actor_id,
658		config
659			.generation
660			.and_then(|generation| generation.try_into().ok()),
661		Some(message),
662	);
663}
664
665struct RemoteSqliteConfig {
666	handle: EnvoyHandle,
667	namespace_id: String,
668	actor_id: String,
669	generation: u64,
670}
671
672fn select_sqlite_backend(enabled: bool, remote_sqlite: bool) -> SqliteBackend {
673	if enabled && remote_sqlite {
674		return SqliteBackend::RemoteEnvoy;
675	}
676
677	#[cfg(feature = "sqlite-local")]
678	{
679		SqliteBackend::LocalNative
680	}
681
682	#[cfg(not(feature = "sqlite-local"))]
683	{
684		SqliteBackend::Unavailable
685	}
686}
687
688fn bind_param_count(params: &Option<Vec<BindParam>>) -> usize {
689	params.as_ref().map_or(0, Vec::len)
690}
691
692#[cfg(feature = "sqlite-local")]
693fn is_fatal_worker_error(error: &anyhow::Error) -> bool {
694	error.downcast_ref::<SqliteWorkerFatalError>().is_some()
695		|| error.downcast_ref::<SqliteWorkerDeadError>().is_some()
696		|| error
697			.downcast_ref::<SqliteWorkerCloseTimeoutError>()
698			.is_some()
699}
700
701#[cfg(feature = "sqlite-local")]
702fn sqlite_worker_fatal_message(error: &anyhow::Error) -> String {
703	if let Some(error) = error.downcast_ref::<SqliteWorkerFatalError>() {
704		return format!("sqlite fatal storage error: {}", error.message());
705	}
706
707	format!("sqlite worker failed: {error}")
708}
709
710#[cfg(feature = "sqlite-local")]
711fn map_local_worker_error(error: anyhow::Error) -> anyhow::Error {
712	if error
713		.downcast_ref::<SqliteWorkerOverloadedError>()
714		.is_some()
715	{
716		return ActorLifecycle::Overloaded {
717			channel: "sqlite_worker".to_string(),
718			capacity: SQLITE_WORKER_QUEUE_CAPACITY,
719			operation: "execute sqlite command".to_string(),
720		}
721		.build();
722	}
723
724	if error.downcast_ref::<SqliteWorkerClosingError>().is_some()
725		|| error.downcast_ref::<SqliteWorkerDeadError>().is_some()
726		|| error.downcast_ref::<SqliteWorkerFatalError>().is_some()
727	{
728		return SqliteRuntimeError::Closed.build();
729	}
730
731	error
732}
733
734fn protocol_bind_params(params: Vec<BindParam>) -> Vec<protocol::SqliteBindParam> {
735	params.into_iter().map(protocol_bind_param).collect()
736}
737
738fn protocol_bind_param(param: BindParam) -> protocol::SqliteBindParam {
739	match param {
740		BindParam::Null => protocol::SqliteBindParam::SqliteValueNull,
741		BindParam::Integer(value) => {
742			protocol::SqliteBindParam::SqliteValueInteger(protocol::SqliteValueInteger { value })
743		}
744		BindParam::Float(value) => {
745			protocol::SqliteBindParam::SqliteValueFloat(protocol::SqliteValueFloat {
746				value: value.to_bits().to_be_bytes(),
747			})
748		}
749		BindParam::Text(value) => {
750			protocol::SqliteBindParam::SqliteValueText(protocol::SqliteValueText { value })
751		}
752		BindParam::Blob(value) => {
753			protocol::SqliteBindParam::SqliteValueBlob(protocol::SqliteValueBlob { value })
754		}
755	}
756}
757
758fn query_result_from_protocol(result: protocol::SqliteQueryResult) -> QueryResult {
759	QueryResult {
760		columns: result.columns,
761		rows: result
762			.rows
763			.into_iter()
764			.map(|row| row.into_iter().map(column_value_from_protocol).collect())
765			.collect(),
766	}
767}
768
769fn execute_result_from_protocol(result: protocol::SqliteExecuteResult) -> ExecuteResult {
770	ExecuteResult {
771		columns: result.columns,
772		rows: result
773			.rows
774			.into_iter()
775			.map(|row| row.into_iter().map(column_value_from_protocol).collect())
776			.collect(),
777		changes: result.changes,
778		last_insert_row_id: result.last_insert_row_id,
779	}
780}
781
782fn column_value_from_protocol(value: protocol::SqliteColumnValue) -> ColumnValue {
783	match value {
784		protocol::SqliteColumnValue::SqliteValueNull => ColumnValue::Null,
785		protocol::SqliteColumnValue::SqliteValueInteger(value) => ColumnValue::Integer(value.value),
786		protocol::SqliteColumnValue::SqliteValueFloat(value) => {
787			ColumnValue::Float(f64::from_bits(u64::from_be_bytes(value.value)))
788		}
789		protocol::SqliteColumnValue::SqliteValueText(value) => ColumnValue::Text(value.value),
790		protocol::SqliteColumnValue::SqliteValueBlob(value) => ColumnValue::Blob(value.value),
791	}
792}
793
794fn remote_request_error(error: anyhow::Error) -> anyhow::Error {
795	if let Some(indeterminate) = error.downcast_ref::<RemoteSqliteIndeterminateResultError>() {
796		return SqliteRuntimeError::RemoteIndeterminateResult {
797			operation: indeterminate.operation.to_owned(),
798		}
799		.build();
800	}
801
802	if let Some(compatibility) =
803		error.downcast_ref::<protocol::versioned::ProtocolCompatibilityError>()
804	{
805		if compatibility.feature
806			== protocol::versioned::ProtocolCompatibilityFeature::RemoteSqliteExecution
807		{
808			return SqliteRuntimeError::RemoteUnavailable {
809				reason: compatibility.to_string(),
810			}
811			.build();
812		}
813	}
814
815	error
816}
817
818fn remote_sqlite_error_response(message: String) -> anyhow::Error {
819	if message.contains("unavailable") || message.contains("unsupported") {
820		return SqliteRuntimeError::RemoteUnavailable { reason: message }.build();
821	}
822
823	SqliteRuntimeError::RemoteExecutionFailed { message }.build()
824}
825
826fn is_head_fence_mismatch_response(error: &protocol::SqliteErrorResponse) -> bool {
827	is_head_fence_mismatch(&error.group, &error.code)
828}
829impl std::fmt::Debug for SqliteDb {
830	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
831		f.debug_struct("SqliteDb")
832			.field("configured", &self.handle.is_some())
833			.field("actor_id", &self.actor_id)
834			.finish()
835	}
836}
837
838fn bind_params_from_cbor(sql: &str, params: Option<&[u8]>) -> Result<Option<Vec<BindParam>>> {
839	let Some(params) = params else {
840		return Ok(None);
841	};
842	if params.is_empty() {
843		return Ok(None);
844	}
845
846	let value = ciborium::from_reader::<JsonValue, _>(Cursor::new(params))
847		.context("decode sqlite bind params as cbor json")?;
848	match value {
849		JsonValue::Array(values) => values
850			.iter()
851			.map(json_to_bind_param)
852			.collect::<Result<Vec<_>>>()
853			.map(Some),
854		JsonValue::Object(properties) => {
855			let ordered_names = extract_named_sqlite_parameters(sql);
856			if ordered_names.is_empty() {
857				return properties
858					.values()
859					.map(json_to_bind_param)
860					.collect::<Result<Vec<_>>>()
861					.map(Some);
862			}
863
864			ordered_names
865				.iter()
866				.map(|name| {
867					get_named_sqlite_binding(&properties, name)
868						.ok_or_else(|| {
869							SqliteRuntimeError::InvalidBindParameter {
870								name: name.clone(),
871								reason: "missing parameter".to_owned(),
872							}
873							.build()
874						})
875						.and_then(json_to_bind_param)
876				})
877				.collect::<Result<Vec<_>>>()
878				.map(Some)
879		}
880		JsonValue::Null => Ok(None),
881		other => Err(SqliteRuntimeError::InvalidBindParameter {
882			name: "params".to_owned(),
883			reason: format!("expected array or object, got {}", json_type_name(&other)),
884		}
885		.build()),
886	}
887}
888
889fn json_to_bind_param(value: &JsonValue) -> Result<BindParam> {
890	match value {
891		JsonValue::Null => Ok(BindParam::Null),
892		JsonValue::Bool(value) => Ok(BindParam::Integer(i64::from(*value))),
893		JsonValue::Number(value) => {
894			if let Some(value) = value.as_i64() {
895				return Ok(BindParam::Integer(value));
896			}
897			if let Some(value) = value.as_u64() {
898				let value = i64::try_from(value)
899					.context("sqlite integer bind parameter exceeds i64 range")?;
900				return Ok(BindParam::Integer(value));
901			}
902			value.as_f64().map(BindParam::Float).ok_or_else(|| {
903				SqliteRuntimeError::InvalidBindParameter {
904					name: "number".to_owned(),
905					reason: "unsupported numeric value".to_owned(),
906				}
907				.build()
908			})
909		}
910		JsonValue::String(value) => Ok(BindParam::Text(value.clone())),
911		other => Err(SqliteRuntimeError::InvalidBindParameter {
912			name: "value".to_owned(),
913			reason: format!("unsupported type {}", json_type_name(other)),
914		}
915		.build()),
916	}
917}
918
919fn sqlite_not_configured(component: &str) -> anyhow::Error {
920	SqliteRuntimeError::NotConfigured {
921		component: component.to_owned(),
922	}
923	.build()
924}
925
926fn extract_named_sqlite_parameters(sql: &str) -> Vec<String> {
927	let mut ordered_names = Vec::new();
928	let mut seen = HashSet::new();
929	let bytes = sql.as_bytes();
930	let mut idx = 0;
931
932	while idx < bytes.len() {
933		let byte = bytes[idx];
934		if !matches!(byte, b':' | b'@' | b'$') {
935			idx += 1;
936			continue;
937		}
938
939		let start = idx;
940		idx += 1;
941		if idx >= bytes.len() || !is_sqlite_param_start(bytes[idx]) {
942			continue;
943		}
944		idx += 1;
945		while idx < bytes.len() && is_sqlite_param_continue(bytes[idx]) {
946			idx += 1;
947		}
948
949		let name = &sql[start..idx];
950		if seen.insert(name.to_owned()) {
951			ordered_names.push(name.to_owned());
952		}
953	}
954
955	ordered_names
956}
957
958fn is_sqlite_param_start(byte: u8) -> bool {
959	byte == b'_' || byte.is_ascii_alphabetic()
960}
961
962fn is_sqlite_param_continue(byte: u8) -> bool {
963	byte == b'_' || byte.is_ascii_alphanumeric()
964}
965
966fn get_named_sqlite_binding<'a>(
967	bindings: &'a JsonMap<String, JsonValue>,
968	name: &str,
969) -> Option<&'a JsonValue> {
970	if let Some(value) = bindings.get(name) {
971		return Some(value);
972	}
973
974	let bare_name = name.get(1..)?;
975	if let Some(value) = bindings.get(bare_name) {
976		return Some(value);
977	}
978
979	for prefix in [":", "@", "$"] {
980		let candidate = format!("{prefix}{bare_name}");
981		if let Some(value) = bindings.get(&candidate) {
982			return Some(value);
983		}
984	}
985
986	None
987}
988
989fn query_result_to_json_rows(result: &QueryResult) -> JsonValue {
990	JsonValue::Array(
991		result
992			.rows
993			.iter()
994			.map(|row| {
995				let mut object = JsonMap::new();
996				for (index, column) in result.columns.iter().enumerate() {
997					let value = row
998						.get(index)
999						.map(column_value_to_json)
1000						.unwrap_or(JsonValue::Null);
1001					object.insert(column.clone(), value);
1002				}
1003				JsonValue::Object(object)
1004			})
1005			.collect(),
1006	)
1007}
1008
1009fn column_value_to_json(value: &ColumnValue) -> JsonValue {
1010	match value {
1011		ColumnValue::Null => JsonValue::Null,
1012		ColumnValue::Integer(value) => JsonValue::from(*value),
1013		ColumnValue::Float(value) => JsonValue::from(*value),
1014		ColumnValue::Text(value) => JsonValue::String(value.clone()),
1015		ColumnValue::Blob(value) => {
1016			JsonValue::Array(value.iter().map(|byte| JsonValue::from(*byte)).collect())
1017		}
1018	}
1019}
1020
1021fn encode_json_as_cbor(value: &impl Serialize) -> Result<Vec<u8>> {
1022	let mut encoded = Vec::new();
1023	ciborium::into_writer(value, &mut encoded).context("encode sqlite rows as cbor")?;
1024	Ok(encoded)
1025}
1026
1027fn json_type_name(value: &JsonValue) -> &'static str {
1028	match value {
1029		JsonValue::Null => "null",
1030		JsonValue::Bool(_) => "boolean",
1031		JsonValue::Number(_) => "number",
1032		JsonValue::String(_) => "string",
1033		JsonValue::Array(_) => "array",
1034		JsonValue::Object(_) => "object",
1035	}
1036}
1037
1038#[cfg(test)]
1039#[path = "../../tests/sqlite.rs"]
1040mod tests;