Skip to main content

rivetkit_core/actor/sqlite/
mod.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::{
15	handle::EnvoyHandle,
16	utils::{RemoteSqliteConnectionSessionLostError, RemoteSqliteIndeterminateResultError},
17};
18use rivet_error::{ActorSpecifier, RivetError};
19use serde::Serialize;
20use serde_json::{Map as JsonMap, Value as JsonValue};
21#[cfg(feature = "sqlite-local")]
22use tokio::sync::Mutex as AsyncMutex;
23#[cfg(feature = "sqlite-local")]
24use tokio::task::JoinHandle;
25
26#[cfg(feature = "sqlite-local")]
27mod envoy_sqlite_transport;
28mod tx;
29
30pub use tx::{
31	DEFAULT_TRANSACTION_TIMEOUT, SqliteTransaction, TRANSACTION_COORDINATOR_QUEUE_CAPACITY,
32	TransactionConnectionLostError, TransactionCoordinatorClosedError, TransactionExpiredError,
33	TransactionInvalidArgumentError, TransactionQueueFullError, TransactionTerminalError,
34	TransactionUnknownError,
35};
36#[cfg(test)]
37use tx::{
38	TRANSACTION_TERMINAL_CAPACITY, TransactionCoordinatorState, TransactionTerminalState,
39	insert_terminal_state,
40};
41use tx::{TransactionCoordinator, run_detached_transaction_task};
42
43#[cfg(feature = "sqlite-local")]
44use crate::error::ActorLifecycle;
45use crate::error::SqliteRuntimeError;
46#[cfg(all(not(target_arch = "wasm32"), feature = "sqlite-local"))]
47use crate::runtime::RuntimeSpawner;
48
49#[cfg(feature = "sqlite-local")]
50use depot_client::{
51	database::{NativeDatabaseHandle, open_database_from_transport},
52	vfs::{SqliteVfsMetrics, SqliteVfsMetricsSnapshot},
53	worker::{
54		SQLITE_WORKER_QUEUE_CAPACITY, SqliteWorkerCloseTimeoutError, SqliteWorkerClosingError,
55		SqliteWorkerDeadError, SqliteWorkerFatalError, SqliteWorkerOverloadedError,
56	},
57};
58#[cfg(feature = "sqlite-local")]
59use envoy_sqlite_transport::EnvoySqliteTransport;
60
61#[cfg(not(feature = "sqlite-local"))]
62#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
63pub struct SqliteVfsMetricsSnapshot {
64	pub request_build_ns: u64,
65	pub serialize_ns: u64,
66	pub transport_ns: u64,
67	pub state_update_ns: u64,
68	pub total_ns: u64,
69	pub commit_count: u64,
70}
71
72#[derive(Clone)]
73pub struct SqliteRuntimeConfig {
74	pub handle: EnvoyHandle,
75	pub actor_id: String,
76	pub generation: Option<u64>,
77}
78
79#[derive(Clone, Debug)]
80pub struct SqliteBatchStatement {
81	pub sql: String,
82	pub params: Option<Vec<BindParam>>,
83}
84
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum SqliteBackend {
87	LocalNative,
88	RemoteEnvoy,
89}
90
91impl SqliteDb {
92	async fn exec_backend(&self, sql: String) -> Result<QueryResult> {
93		match self.backend {
94			SqliteBackend::LocalNative => self.local_exec(sql).await,
95			SqliteBackend::RemoteEnvoy => self.remote_exec(sql).await,
96		}
97	}
98
99	async fn exec_backend_in_session(
100		&self,
101		sql: String,
102		expected_session: Option<u64>,
103	) -> Result<(QueryResult, Option<u64>)> {
104		match self.backend {
105			SqliteBackend::LocalNative => self.local_exec(sql).await.map(|result| (result, None)),
106			SqliteBackend::RemoteEnvoy => self
107				.remote_exec_with_session(sql, expected_session)
108				.await
109				.map(|(result, session)| (result, Some(session))),
110		}
111	}
112
113	async fn query_backend(
114		&self,
115		sql: String,
116		params: Option<Vec<BindParam>>,
117	) -> Result<QueryResult> {
118		match self.backend {
119			SqliteBackend::LocalNative => self.local_query(sql, params).await,
120			SqliteBackend::RemoteEnvoy => self
121				.remote_execute(sql, params)
122				.await
123				.map(ExecuteResult::into_query_result),
124		}
125	}
126
127	async fn run_backend(&self, sql: String, params: Option<Vec<BindParam>>) -> Result<ExecResult> {
128		match self.backend {
129			SqliteBackend::LocalNative => self.local_run(sql, params).await,
130			SqliteBackend::RemoteEnvoy => self
131				.remote_execute(sql, params)
132				.await
133				.map(ExecuteResult::into_exec_result),
134		}
135	}
136
137	async fn execute_backend(
138		&self,
139		sql: String,
140		params: Option<Vec<BindParam>>,
141	) -> Result<ExecuteResult> {
142		match self.backend {
143			SqliteBackend::LocalNative => self.local_execute(sql, params).await,
144			SqliteBackend::RemoteEnvoy => self.remote_execute(sql, params).await,
145		}
146	}
147
148	async fn execute_backend_in_session(
149		&self,
150		sql: String,
151		params: Option<Vec<BindParam>>,
152		expected_session: Option<u64>,
153	) -> Result<(ExecuteResult, Option<u64>)> {
154		match self.backend {
155			SqliteBackend::LocalNative => self
156				.local_execute(sql, params)
157				.await
158				.map(|result| (result, None)),
159			SqliteBackend::RemoteEnvoy => self
160				.remote_execute_with_session(sql, params, expected_session)
161				.await
162				.map(|(result, session)| (result, Some(session))),
163		}
164	}
165}
166
167#[derive(Clone)]
168pub struct SqliteDb {
169	handle: Option<EnvoyHandle>,
170	actor_id: Option<String>,
171	actor_key: Option<String>,
172	generation: Option<u64>,
173	backend: SqliteBackend,
174	/// Mirrors the user's actor-config `db({...})` declaration. The envoy
175	/// always sets up sqlite storage under the hood, so handle/actor_id are
176	/// not a reliable signal for whether the user opted in; this flag is.
177	enabled: bool,
178	#[cfg(feature = "sqlite-local")]
179	// Forced-sync: native SQLite handles are used inside spawn_blocking and
180	// synchronous diagnostic accessors.
181	db: Arc<Mutex<Option<NativeDatabaseHandle>>>,
182	#[cfg(feature = "sqlite-local")]
183	open_lock: Arc<AsyncMutex<()>>,
184	#[cfg(feature = "sqlite-local")]
185	worker_failure_task: Arc<Mutex<Option<JoinHandle<()>>>>,
186	worker_fatal_reported: Arc<AtomicBool>,
187	transaction_coordinator: Arc<TransactionCoordinator>,
188	#[cfg(feature = "sqlite-local")]
189	vfs_metrics: Option<Arc<dyn SqliteVfsMetrics>>,
190}
191
192impl SqliteDb {
193	pub fn new(handle: EnvoyHandle, actor_id: impl Into<String>, enabled: bool) -> Result<Self> {
194		Self::new_with_remote_sqlite(handle, actor_id, None, None, enabled, false)
195	}
196
197	pub fn new_with_remote_sqlite(
198		handle: EnvoyHandle,
199		actor_id: impl Into<String>,
200		actor_key: Option<String>,
201		generation: Option<u64>,
202		enabled: bool,
203		remote_sqlite: bool,
204	) -> Result<Self> {
205		Ok(Self {
206			handle: Some(handle),
207			actor_id: Some(actor_id.into()),
208			actor_key,
209			generation,
210			backend: select_sqlite_backend(remote_sqlite)?,
211			enabled,
212			#[cfg(feature = "sqlite-local")]
213			db: Default::default(),
214			#[cfg(feature = "sqlite-local")]
215			open_lock: Default::default(),
216			#[cfg(feature = "sqlite-local")]
217			worker_failure_task: Default::default(),
218			worker_fatal_reported: Default::default(),
219			transaction_coordinator: Default::default(),
220			#[cfg(feature = "sqlite-local")]
221			vfs_metrics: None,
222		})
223	}
224
225	#[cfg(feature = "sqlite-local")]
226	pub(crate) fn set_vfs_metrics(&mut self, metrics: Arc<dyn SqliteVfsMetrics>) {
227		self.vfs_metrics = Some(metrics);
228	}
229
230	pub fn is_enabled(&self) -> bool {
231		self.enabled
232	}
233
234	pub fn backend(&self) -> SqliteBackend {
235		self.backend
236	}
237
238	pub async fn get_pages(
239		&self,
240		request: protocol::SqliteGetPagesRequest,
241	) -> Result<protocol::SqliteGetPagesResponse> {
242		self.handle()?.sqlite_get_pages(request).await
243	}
244
245	pub async fn commit(
246		&self,
247		request: protocol::SqliteCommitRequest,
248	) -> Result<protocol::SqliteCommitResponse> {
249		self.handle()?.sqlite_commit(request).await
250	}
251
252	pub async fn open(&self) -> Result<()> {
253		match self.backend {
254			SqliteBackend::LocalNative => {
255				#[cfg(feature = "sqlite-local")]
256				{
257					let _open_guard = self.open_lock.lock().await;
258					if self.db.lock().is_some() {
259						return Ok(());
260					}
261
262					let config = self.runtime_config()?;
263					let vfs_metrics = self.vfs_metrics.clone();
264					let rt_handle = tokio::runtime::Handle::try_current()
265						.context("open sqlite database requires a tokio runtime")?;
266					self.worker_fatal_reported.store(false, Ordering::Release);
267
268					let native_db = self.map_local_worker_result(
269						open_database_from_transport(
270							Arc::new(EnvoySqliteTransport::new(config.handle.clone())),
271							config.actor_id.clone(),
272							config
273								.generation
274								.ok_or_else(|| sqlite_not_configured("generation"))?,
275							rt_handle,
276							vfs_metrics,
277						)
278						.await,
279					)?;
280					self.start_worker_failure_monitor(native_db.clone(), config);
281					*self.db.lock() = Some(native_db);
282					if let Some(metrics) = self.vfs_metrics.as_ref() {
283						metrics.set_worker_active(true);
284					}
285					Ok(())
286				}
287
288				#[cfg(not(feature = "sqlite-local"))]
289				{
290					Err(SqliteRuntimeError::Unavailable.build())
291				}
292			}
293			SqliteBackend::RemoteEnvoy => {
294				self.remote_config()?;
295				Ok(())
296			}
297		}
298	}
299
300	#[cfg(feature = "sqlite-local")]
301	async fn local_exec(&self, sql: String) -> Result<QueryResult> {
302		self.open().await?;
303		self.map_local_worker_result(self.native_db_handle()?.exec(sql).await)
304	}
305
306	#[cfg(not(feature = "sqlite-local"))]
307	async fn local_exec(&self, _sql: String) -> Result<QueryResult> {
308		Err(SqliteRuntimeError::Unavailable.build())
309	}
310
311	#[cfg(feature = "sqlite-local")]
312	async fn local_query(
313		&self,
314		sql: String,
315		params: Option<Vec<BindParam>>,
316	) -> Result<QueryResult> {
317		self.open().await?;
318		self.map_local_worker_result(self.native_db_handle()?.query(sql, params).await)
319	}
320
321	#[cfg(not(feature = "sqlite-local"))]
322	async fn local_query(
323		&self,
324		_sql: String,
325		_params: Option<Vec<BindParam>>,
326	) -> Result<QueryResult> {
327		Err(SqliteRuntimeError::Unavailable.build())
328	}
329
330	#[cfg(feature = "sqlite-local")]
331	async fn local_run(&self, sql: String, params: Option<Vec<BindParam>>) -> Result<ExecResult> {
332		self.open().await?;
333		self.map_local_worker_result(self.native_db_handle()?.run(sql, params).await)
334	}
335
336	#[cfg(not(feature = "sqlite-local"))]
337	async fn local_run(&self, _sql: String, _params: Option<Vec<BindParam>>) -> Result<ExecResult> {
338		Err(SqliteRuntimeError::Unavailable.build())
339	}
340
341	#[cfg(feature = "sqlite-local")]
342	async fn local_execute(
343		&self,
344		sql: String,
345		params: Option<Vec<BindParam>>,
346	) -> Result<ExecuteResult> {
347		self.open().await?;
348		self.map_local_worker_result(self.native_db_handle()?.execute(sql, params).await)
349	}
350
351	#[cfg(not(feature = "sqlite-local"))]
352	async fn local_execute(
353		&self,
354		_sql: String,
355		_params: Option<Vec<BindParam>>,
356	) -> Result<ExecuteResult> {
357		Err(SqliteRuntimeError::Unavailable.build())
358	}
359
360	pub async fn exec(&self, sql: impl Into<String>) -> Result<QueryResult> {
361		let sql = sql.into();
362		let sql_for_log = sql.clone();
363		let result = match self.begin_regular_operation().await {
364			Ok(_guard) => self.exec_backend(sql).await,
365			Err(error) => Err(error),
366		};
367		match result {
368			Ok(result) => Ok(result),
369			Err(error) => {
370				let error = self.attach_actor(error);
371				self.log_operation_error("exec", &sql_for_log, 0, &error);
372				Err(error)
373			}
374		}
375	}
376
377	pub async fn query(
378		&self,
379		sql: impl Into<String>,
380		params: Option<Vec<BindParam>>,
381	) -> Result<QueryResult> {
382		let sql = sql.into();
383		let sql_for_log = sql.clone();
384		let binding_count = bind_param_count(&params);
385		let result = match self.begin_regular_operation().await {
386			Ok(_guard) => self.query_backend(sql, params).await,
387			Err(error) => Err(error),
388		};
389		match result {
390			Ok(result) => Ok(result),
391			Err(error) => {
392				let error = self.attach_actor(error);
393				self.log_operation_error("query", &sql_for_log, binding_count, &error);
394				Err(error)
395			}
396		}
397	}
398
399	pub async fn run(
400		&self,
401		sql: impl Into<String>,
402		params: Option<Vec<BindParam>>,
403	) -> Result<ExecResult> {
404		let sql = sql.into();
405		let sql_for_log = sql.clone();
406		let binding_count = bind_param_count(&params);
407		let result = match self.begin_regular_operation().await {
408			Ok(_guard) => self.run_backend(sql, params).await,
409			Err(error) => Err(error),
410		};
411		match result {
412			Ok(result) => Ok(result),
413			Err(error) => {
414				let error = self.attach_actor(error);
415				self.log_operation_error("run", &sql_for_log, binding_count, &error);
416				Err(error)
417			}
418		}
419	}
420
421	pub async fn execute(
422		&self,
423		sql: impl Into<String>,
424		params: Option<Vec<BindParam>>,
425	) -> Result<ExecuteResult> {
426		let sql = sql.into();
427		let sql_for_log = sql.clone();
428		let binding_count = bind_param_count(&params);
429		let result = match self.begin_regular_operation().await {
430			Ok(_guard) => self.execute_backend(sql, params).await,
431			Err(error) => Err(error),
432		};
433		match result {
434			Ok(result) => Ok(result),
435			Err(error) => {
436				let error = self.attach_actor(error);
437				self.log_operation_error("execute", &sql_for_log, binding_count, &error);
438				Err(error)
439			}
440		}
441	}
442
443	pub async fn execute_batch(
444		&self,
445		statements: Vec<SqliteBatchStatement>,
446	) -> Result<Vec<ExecuteResult>> {
447		let statement_count = statements.len();
448		let binding_count = statements
449			.iter()
450			.map(|statement| bind_param_count(&statement.params))
451			.sum();
452		let result = if self.backend == SqliteBackend::RemoteEnvoy {
453			match self.begin_regular_operation().await {
454				Ok(_guard) => self.remote_execute_batch(statements).await,
455				Err(error) => Err(error),
456			}
457		} else {
458			async {
459				let transaction = self.begin_transaction(None).await?;
460				let mut results = Vec::with_capacity(statements.len());
461				for statement in statements {
462					match transaction.execute(statement.sql, statement.params).await {
463						Ok(result) => results.push(result),
464						Err(error) => {
465							return match transaction.rollback().await {
466								Ok(()) => Err(error.context("execute sqlite batch statement")),
467								Err(rollback_error) => {
468									Err(error.context("execute sqlite batch statement").context(
469										rollback_error.context("rollback sqlite batch transaction"),
470									))
471								}
472							};
473						}
474					}
475				}
476				transaction
477					.commit()
478					.await
479					.context("commit sqlite batch transaction")?;
480				Ok(results)
481			}
482			.await
483		};
484
485		match result {
486			Ok(results) => Ok(results),
487			Err(error) => {
488				let error = self.attach_actor(error);
489				self.log_operation_error_with_count(
490					"execute_batch",
491					"<batch>",
492					binding_count,
493					statement_count,
494					&error,
495				);
496				Err(error)
497			}
498		}
499	}
500
501	pub async fn close(&self) -> Result<()> {
502		let db = self.clone();
503		run_detached_transaction_task(
504			async move {
505				let _gate = db.shutdown_transaction_coordinator().await;
506				db.close_backend().await
507			},
508			"sqlite close task failed",
509		)
510		.await
511	}
512
513	async fn close_backend(&self) -> Result<()> {
514		match self.backend {
515			SqliteBackend::LocalNative => {
516				#[cfg(feature = "sqlite-local")]
517				{
518					let native_db = self.db.lock().take();
519					if let Some(native_db) = native_db {
520						let result = self.map_local_worker_result(native_db.close().await);
521						self.abort_worker_failure_monitor();
522						if let Some(metrics) = self.vfs_metrics.as_ref() {
523							metrics.set_worker_active(false);
524						}
525						result?;
526					}
527				}
528				Ok(())
529			}
530			SqliteBackend::RemoteEnvoy => Ok(()),
531		}
532	}
533
534	pub(crate) async fn cleanup_for_shutdown(&self, reusable: bool) -> Result<()> {
535		// A remote database has no actor-local resources to release. Keep its
536		// coordinator usable across a sleep/wake cycle on the same ActorTask.
537		if reusable && self.backend == SqliteBackend::RemoteEnvoy {
538			return Ok(());
539		}
540		self.close().await
541	}
542
543	#[cfg(test)]
544	pub(crate) fn fresh_remote_for_test(&self) -> Self {
545		Self::new_with_remote_sqlite(
546			self.handle
547				.clone()
548				.expect("remote sqlite test database should have an envoy handle"),
549			self.actor_id
550				.clone()
551				.expect("remote sqlite test database should have an actor id"),
552			self.actor_key.clone(),
553			self.generation,
554			self.enabled,
555			true,
556		)
557		.expect("remote sqlite test database should be configured")
558	}
559
560	pub fn take_last_kv_error(&self) -> Option<String> {
561		if self.backend != SqliteBackend::LocalNative {
562			return None;
563		}
564
565		#[cfg(feature = "sqlite-local")]
566		{
567			return self
568				.db
569				.lock()
570				.as_ref()
571				.and_then(NativeDatabaseHandle::take_last_kv_error);
572		}
573
574		#[cfg(not(feature = "sqlite-local"))]
575		None
576	}
577
578	#[cfg(feature = "sqlite-local")]
579	fn native_db_handle(&self) -> Result<NativeDatabaseHandle> {
580		self.db
581			.lock()
582			.as_ref()
583			.cloned()
584			.ok_or_else(|| SqliteRuntimeError::Closed.build())
585	}
586
587	#[cfg(feature = "sqlite-local")]
588	fn map_local_worker_result<T>(&self, result: Result<T>) -> Result<T> {
589		match result {
590			Ok(value) => Ok(value),
591			Err(error) => {
592				if is_fatal_worker_error(&error) {
593					self.report_worker_fatal(&error);
594				}
595				Err(map_local_worker_error(error))
596			}
597		}
598	}
599
600	#[cfg(feature = "sqlite-local")]
601	fn report_worker_fatal(&self, error: &anyhow::Error) {
602		let Ok(config) = self.runtime_config() else {
603			return;
604		};
605		report_sqlite_worker_fatal(
606			&self.worker_fatal_reported,
607			config,
608			sqlite_worker_fatal_message(error),
609		);
610	}
611
612	#[cfg(feature = "sqlite-local")]
613	fn start_worker_failure_monitor(
614		&self,
615		native_db: NativeDatabaseHandle,
616		config: SqliteRuntimeConfig,
617	) {
618		self.abort_worker_failure_monitor();
619		let reported = Arc::clone(&self.worker_fatal_reported);
620		let task = RuntimeSpawner::spawn(async move {
621			if native_db.wait_for_worker_failure().await {
622				report_sqlite_worker_fatal(
623					&reported,
624					config,
625					"sqlite worker thread stopped unexpectedly".to_string(),
626				);
627			}
628		});
629		*self.worker_failure_task.lock() = Some(task);
630	}
631
632	#[cfg(feature = "sqlite-local")]
633	fn abort_worker_failure_monitor(&self) {
634		if let Some(task) = self.worker_failure_task.lock().take() {
635			task.abort();
636		}
637	}
638
639	pub fn metrics(&self) -> Option<SqliteVfsMetricsSnapshot> {
640		#[cfg(feature = "sqlite-local")]
641		{
642			self.db
643				.lock()
644				.as_ref()
645				.map(NativeDatabaseHandle::sqlite_vfs_metrics)
646		}
647
648		#[cfg(not(feature = "sqlite-local"))]
649		{
650			None
651		}
652	}
653
654	pub fn runtime_config(&self) -> Result<SqliteRuntimeConfig> {
655		Ok(SqliteRuntimeConfig {
656			handle: self.handle()?,
657			actor_id: self
658				.actor_id
659				.clone()
660				.ok_or_else(|| sqlite_not_configured("actor id"))?,
661			generation: self.generation,
662		})
663	}
664
665	fn log_operation_error(
666		&self,
667		operation: &'static str,
668		sql: &str,
669		binding_count: usize,
670		error: &anyhow::Error,
671	) {
672		self.log_operation_error_with_count(operation, sql, binding_count, 1, error);
673	}
674
675	fn log_operation_error_with_count(
676		&self,
677		operation: &'static str,
678		sql: &str,
679		binding_count: usize,
680		statement_count: usize,
681		error: &anyhow::Error,
682	) {
683		let structured = RivetError::extract(error);
684		let error_chain = error.chain().map(ToString::to_string).collect::<Vec<_>>();
685		tracing::error!(
686			actor_id = self.actor_id.as_deref().unwrap_or("<unknown>"),
687			generation = ?self.generation,
688			backend = ?self.backend,
689			operation,
690			sql,
691			binding_count,
692			statement_count,
693			group = structured.group(),
694			code = structured.code(),
695			error_message = %structured.message(),
696			metadata = ?structured.metadata(),
697			error_chain = ?error_chain,
698			"sqlite operation failed"
699		);
700	}
701
702	fn actor_specifier(&self) -> Option<ActorSpecifier> {
703		let mut specifier = ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?);
704		if let Some(key) = self.actor_key.as_ref() {
705			specifier = specifier.with_key(key.clone());
706		}
707		Some(specifier)
708	}
709
710	fn attach_actor(&self, error: anyhow::Error) -> anyhow::Error {
711		match self.actor_specifier() {
712			Some(actor) => error.context(actor),
713			None => error,
714		}
715	}
716
717	fn remote_config(&self) -> Result<RemoteSqliteConfig> {
718		let config = self.runtime_config()?;
719		let generation = config
720			.generation
721			.ok_or_else(|| sqlite_not_configured("generation"))?;
722		Ok(RemoteSqliteConfig {
723			namespace_id: config.handle.namespace().to_owned(),
724			handle: config.handle,
725			actor_id: config.actor_id,
726			generation,
727		})
728	}
729
730	async fn remote_exec(&self, sql: String) -> Result<QueryResult> {
731		let config = self.remote_config()?;
732		let response = config
733			.handle
734			.remote_sqlite_exec(protocol::SqliteExecRequest {
735				namespace_id: config.namespace_id,
736				actor_id: config.actor_id,
737				generation: config.generation,
738				sql,
739			})
740			.await
741			.map_err(remote_request_error)?;
742
743		match response {
744			protocol::SqliteExecResponse::SqliteExecOk(ok) => {
745				Ok(query_result_from_protocol(ok.result))
746			}
747			protocol::SqliteExecResponse::SqliteErrorResponse(error) => {
748				Err(self.remote_sqlite_error_response(error))
749			}
750		}
751	}
752
753	async fn remote_exec_with_session(
754		&self,
755		sql: String,
756		expected_session: Option<u64>,
757	) -> Result<(QueryResult, u64)> {
758		let config = self.remote_config()?;
759		let (response, session) = config
760			.handle
761			.remote_sqlite_exec_with_session(
762				protocol::SqliteExecRequest {
763					namespace_id: config.namespace_id,
764					actor_id: config.actor_id,
765					generation: config.generation,
766					sql,
767				},
768				expected_session,
769			)
770			.await?;
771
772		match response {
773			protocol::SqliteExecResponse::SqliteExecOk(ok) => {
774				Ok((query_result_from_protocol(ok.result), session))
775			}
776			protocol::SqliteExecResponse::SqliteErrorResponse(error) => {
777				Err(self.remote_sqlite_error_response(error))
778			}
779		}
780	}
781
782	async fn remote_execute(
783		&self,
784		sql: String,
785		params: Option<Vec<BindParam>>,
786	) -> Result<ExecuteResult> {
787		let config = self.remote_config()?;
788		let response = config
789			.handle
790			.remote_sqlite_execute(protocol::SqliteExecuteRequest {
791				namespace_id: config.namespace_id,
792				actor_id: config.actor_id,
793				generation: config.generation,
794				sql,
795				params: params.map(protocol_bind_params),
796			})
797			.await
798			.map_err(remote_request_error)?;
799
800		match response {
801			protocol::SqliteExecuteResponse::SqliteExecuteOk(ok) => {
802				Ok(execute_result_from_protocol(ok.result))
803			}
804			protocol::SqliteExecuteResponse::SqliteErrorResponse(error) => {
805				Err(self.remote_sqlite_error_response(error))
806			}
807		}
808	}
809
810	async fn remote_execute_batch(
811		&self,
812		statements: Vec<SqliteBatchStatement>,
813	) -> Result<Vec<ExecuteResult>> {
814		let config = self.remote_config()?;
815		let response = config
816			.handle
817			.remote_sqlite_execute_batch(protocol::SqliteExecuteBatchRequest {
818				namespace_id: config.namespace_id,
819				actor_id: config.actor_id,
820				generation: config.generation,
821				statements: statements
822					.into_iter()
823					.map(|statement| protocol::SqliteBatchStatement {
824						sql: statement.sql,
825						params: statement.params.map(protocol_bind_params),
826					})
827					.collect(),
828			})
829			.await
830			.map_err(remote_request_error)?;
831
832		match response {
833			protocol::SqliteExecuteBatchResponse::SqliteExecuteBatchOk(ok) => Ok(ok
834				.results
835				.into_iter()
836				.map(execute_result_from_protocol)
837				.collect()),
838			protocol::SqliteExecuteBatchResponse::SqliteErrorResponse(error) => {
839				Err(self.remote_sqlite_error_response(error))
840			}
841		}
842	}
843
844	async fn remote_execute_with_session(
845		&self,
846		sql: String,
847		params: Option<Vec<BindParam>>,
848		expected_session: Option<u64>,
849	) -> Result<(ExecuteResult, u64)> {
850		let config = self.remote_config()?;
851		let (response, session) = config
852			.handle
853			.remote_sqlite_execute_with_session(
854				protocol::SqliteExecuteRequest {
855					namespace_id: config.namespace_id,
856					actor_id: config.actor_id,
857					generation: config.generation,
858					sql,
859					params: params.map(protocol_bind_params),
860				},
861				expected_session,
862			)
863			.await?;
864
865		match response {
866			protocol::SqliteExecuteResponse::SqliteExecuteOk(ok) => {
867				Ok((execute_result_from_protocol(ok.result), session))
868			}
869			protocol::SqliteExecuteResponse::SqliteErrorResponse(error) => {
870				Err(self.remote_sqlite_error_response(error))
871			}
872		}
873	}
874
875	pub(crate) async fn query_rows_cbor(
876		&self,
877		sql: &str,
878		params: Option<&[u8]>,
879	) -> Result<Vec<u8>> {
880		let bind_params = bind_params_from_cbor(sql, params)?;
881		let result = self.query(sql.to_owned(), bind_params).await?;
882		encode_json_as_cbor(&query_result_to_json_rows(&result))
883	}
884
885	pub(crate) async fn exec_rows_cbor(&self, sql: &str) -> Result<Vec<u8>> {
886		let result = self.exec(sql.to_owned()).await?;
887		encode_json_as_cbor(&query_result_to_json_rows(&result))
888	}
889
890	pub(crate) async fn run_cbor(&self, sql: &str, params: Option<&[u8]>) -> Result<ExecResult> {
891		let bind_params = bind_params_from_cbor(sql, params)?;
892		self.run(sql.to_owned(), bind_params).await
893	}
894
895	pub(crate) async fn execute_rows_cbor(
896		&self,
897		sql: &str,
898		params: Option<&[u8]>,
899	) -> Result<Vec<u8>> {
900		let bind_params = bind_params_from_cbor(sql, params)?;
901		let result = self.execute(sql.to_owned(), bind_params).await?;
902		encode_json_as_cbor(&query_result_to_json_rows(&QueryResult {
903			columns: result.columns,
904			rows: result.rows,
905		}))
906	}
907
908	fn handle(&self) -> Result<EnvoyHandle> {
909		self.handle
910			.clone()
911			.ok_or_else(|| sqlite_not_configured("handle"))
912	}
913
914	fn remote_sqlite_error_response(&self, error: protocol::SqliteErrorResponse) -> anyhow::Error {
915		if is_head_fence_mismatch_response(&error) {
916			if let Ok(config) = self.runtime_config() {
917				report_sqlite_worker_fatal(
918					&self.worker_fatal_reported,
919					config,
920					format!("remote sqlite fatal storage error: {}", error.message),
921				);
922			}
923			return SqliteRuntimeError::Closed.build();
924		}
925
926		remote_sqlite_error_response(error.message)
927	}
928}
929
930fn report_sqlite_worker_fatal(reported: &AtomicBool, config: SqliteRuntimeConfig, message: String) {
931	if reported.swap(true, Ordering::AcqRel) {
932		return;
933	}
934	// A dead worker means SQLite's sole native connection is no longer a valid
935	// actor subsystem. Core reports that through envoy lifecycle instead of
936	// letting the actor continue to serve requests with a broken database.
937	config.handle.stop_actor(
938		config.actor_id,
939		config
940			.generation
941			.and_then(|generation| generation.try_into().ok()),
942		Some(message),
943	);
944}
945
946struct RemoteSqliteConfig {
947	handle: EnvoyHandle,
948	namespace_id: String,
949	actor_id: String,
950	generation: u64,
951}
952
953fn select_sqlite_backend(remote_sqlite: bool) -> Result<SqliteBackend> {
954	if remote_sqlite {
955		return Ok(SqliteBackend::RemoteEnvoy);
956	}
957
958	#[cfg(feature = "sqlite-local")]
959	{
960		Ok(SqliteBackend::LocalNative)
961	}
962
963	#[cfg(not(feature = "sqlite-local"))]
964	{
965		Err(SqliteRuntimeError::Unavailable.build())
966	}
967}
968
969fn bind_param_count(params: &Option<Vec<BindParam>>) -> usize {
970	params.as_ref().map_or(0, Vec::len)
971}
972
973#[cfg(feature = "sqlite-local")]
974fn is_fatal_worker_error(error: &anyhow::Error) -> bool {
975	error.downcast_ref::<SqliteWorkerFatalError>().is_some()
976		|| error.downcast_ref::<SqliteWorkerDeadError>().is_some()
977		|| error
978			.downcast_ref::<SqliteWorkerCloseTimeoutError>()
979			.is_some()
980}
981
982#[cfg(feature = "sqlite-local")]
983fn sqlite_worker_fatal_message(error: &anyhow::Error) -> String {
984	if let Some(error) = error.downcast_ref::<SqliteWorkerFatalError>() {
985		return format!("sqlite fatal storage error: {}", error.message());
986	}
987
988	format!("sqlite worker failed: {error}")
989}
990
991#[cfg(feature = "sqlite-local")]
992fn map_local_worker_error(error: anyhow::Error) -> anyhow::Error {
993	if error
994		.downcast_ref::<SqliteWorkerOverloadedError>()
995		.is_some()
996	{
997		return ActorLifecycle::Overloaded {
998			channel: "sqlite_worker".to_string(),
999			capacity: SQLITE_WORKER_QUEUE_CAPACITY,
1000			operation: "execute sqlite command".to_string(),
1001		}
1002		.build();
1003	}
1004
1005	if error.downcast_ref::<SqliteWorkerClosingError>().is_some()
1006		|| error.downcast_ref::<SqliteWorkerDeadError>().is_some()
1007		|| error.downcast_ref::<SqliteWorkerFatalError>().is_some()
1008	{
1009		return SqliteRuntimeError::Closed.build();
1010	}
1011
1012	error
1013}
1014
1015fn protocol_bind_params(params: Vec<BindParam>) -> Vec<protocol::SqliteBindParam> {
1016	params.into_iter().map(protocol_bind_param).collect()
1017}
1018
1019fn protocol_bind_param(param: BindParam) -> protocol::SqliteBindParam {
1020	match param {
1021		BindParam::Null => protocol::SqliteBindParam::SqliteValueNull,
1022		BindParam::Integer(value) => {
1023			protocol::SqliteBindParam::SqliteValueInteger(protocol::SqliteValueInteger { value })
1024		}
1025		BindParam::Float(value) => {
1026			protocol::SqliteBindParam::SqliteValueFloat(protocol::SqliteValueFloat {
1027				value: value.to_bits().to_be_bytes(),
1028			})
1029		}
1030		BindParam::Text(value) => {
1031			protocol::SqliteBindParam::SqliteValueText(protocol::SqliteValueText { value })
1032		}
1033		BindParam::Blob(value) => {
1034			protocol::SqliteBindParam::SqliteValueBlob(protocol::SqliteValueBlob { value })
1035		}
1036	}
1037}
1038
1039fn query_result_from_protocol(result: protocol::SqliteQueryResult) -> QueryResult {
1040	QueryResult {
1041		columns: result.columns,
1042		rows: result
1043			.rows
1044			.into_iter()
1045			.map(|row| row.into_iter().map(column_value_from_protocol).collect())
1046			.collect(),
1047	}
1048}
1049
1050fn execute_result_from_protocol(result: protocol::SqliteExecuteResult) -> ExecuteResult {
1051	ExecuteResult {
1052		columns: result.columns,
1053		rows: result
1054			.rows
1055			.into_iter()
1056			.map(|row| row.into_iter().map(column_value_from_protocol).collect())
1057			.collect(),
1058		changes: result.changes,
1059		last_insert_row_id: result.last_insert_row_id,
1060	}
1061}
1062
1063fn column_value_from_protocol(value: protocol::SqliteColumnValue) -> ColumnValue {
1064	match value {
1065		protocol::SqliteColumnValue::SqliteValueNull => ColumnValue::Null,
1066		protocol::SqliteColumnValue::SqliteValueInteger(value) => ColumnValue::Integer(value.value),
1067		protocol::SqliteColumnValue::SqliteValueFloat(value) => {
1068			ColumnValue::Float(f64::from_bits(u64::from_be_bytes(value.value)))
1069		}
1070		protocol::SqliteColumnValue::SqliteValueText(value) => ColumnValue::Text(value.value),
1071		protocol::SqliteColumnValue::SqliteValueBlob(value) => ColumnValue::Blob(value.value),
1072	}
1073}
1074
1075fn remote_request_error(error: anyhow::Error) -> anyhow::Error {
1076	if let Some(indeterminate) = error.downcast_ref::<RemoteSqliteIndeterminateResultError>() {
1077		return SqliteRuntimeError::RemoteIndeterminateResult {
1078			operation: indeterminate.operation.to_owned(),
1079		}
1080		.build();
1081	}
1082
1083	if let Some(compatibility) =
1084		error.downcast_ref::<protocol::versioned::ProtocolCompatibilityError>()
1085	{
1086		if compatibility.feature
1087			== protocol::versioned::ProtocolCompatibilityFeature::RemoteSqliteExecution
1088		{
1089			return SqliteRuntimeError::RemoteUnavailable {
1090				reason: compatibility.to_string(),
1091			}
1092			.build();
1093		}
1094	}
1095
1096	error
1097}
1098
1099fn remote_sqlite_error_response(message: String) -> anyhow::Error {
1100	if message.contains("unavailable") || message.contains("unsupported") {
1101		return SqliteRuntimeError::RemoteUnavailable { reason: message }.build();
1102	}
1103
1104	SqliteRuntimeError::RemoteExecutionFailed { message }.build()
1105}
1106
1107fn is_head_fence_mismatch_response(error: &protocol::SqliteErrorResponse) -> bool {
1108	is_head_fence_mismatch(&error.group, &error.code)
1109}
1110impl std::fmt::Debug for SqliteDb {
1111	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1112		f.debug_struct("SqliteDb")
1113			.field("configured", &self.handle.is_some())
1114			.field("actor_id", &self.actor_id)
1115			.finish()
1116	}
1117}
1118
1119fn bind_params_from_cbor(sql: &str, params: Option<&[u8]>) -> Result<Option<Vec<BindParam>>> {
1120	let Some(params) = params else {
1121		return Ok(None);
1122	};
1123	if params.is_empty() {
1124		return Ok(None);
1125	}
1126
1127	let value = ciborium::from_reader::<JsonValue, _>(Cursor::new(params))
1128		.context("decode sqlite bind params as cbor json")?;
1129	match value {
1130		JsonValue::Array(values) => values
1131			.iter()
1132			.map(json_to_bind_param)
1133			.collect::<Result<Vec<_>>>()
1134			.map(Some),
1135		JsonValue::Object(properties) => {
1136			let ordered_names = extract_named_sqlite_parameters(sql);
1137			if ordered_names.is_empty() {
1138				return properties
1139					.values()
1140					.map(json_to_bind_param)
1141					.collect::<Result<Vec<_>>>()
1142					.map(Some);
1143			}
1144
1145			ordered_names
1146				.iter()
1147				.map(|name| {
1148					get_named_sqlite_binding(&properties, name)
1149						.ok_or_else(|| {
1150							SqliteRuntimeError::InvalidBindParameter {
1151								name: name.clone(),
1152								reason: "missing parameter".to_owned(),
1153							}
1154							.build()
1155						})
1156						.and_then(json_to_bind_param)
1157				})
1158				.collect::<Result<Vec<_>>>()
1159				.map(Some)
1160		}
1161		JsonValue::Null => Ok(None),
1162		other => Err(SqliteRuntimeError::InvalidBindParameter {
1163			name: "params".to_owned(),
1164			reason: format!("expected array or object, got {}", json_type_name(&other)),
1165		}
1166		.build()),
1167	}
1168}
1169
1170fn json_to_bind_param(value: &JsonValue) -> Result<BindParam> {
1171	match value {
1172		JsonValue::Null => Ok(BindParam::Null),
1173		JsonValue::Bool(value) => Ok(BindParam::Integer(i64::from(*value))),
1174		JsonValue::Number(value) => {
1175			if let Some(value) = value.as_i64() {
1176				return Ok(BindParam::Integer(value));
1177			}
1178			if let Some(value) = value.as_u64() {
1179				let value = i64::try_from(value)
1180					.context("sqlite integer bind parameter exceeds i64 range")?;
1181				return Ok(BindParam::Integer(value));
1182			}
1183			value.as_f64().map(BindParam::Float).ok_or_else(|| {
1184				SqliteRuntimeError::InvalidBindParameter {
1185					name: "number".to_owned(),
1186					reason: "unsupported numeric value".to_owned(),
1187				}
1188				.build()
1189			})
1190		}
1191		JsonValue::String(value) => Ok(BindParam::Text(value.clone())),
1192		other => Err(SqliteRuntimeError::InvalidBindParameter {
1193			name: "value".to_owned(),
1194			reason: format!("unsupported type {}", json_type_name(other)),
1195		}
1196		.build()),
1197	}
1198}
1199
1200fn sqlite_not_configured(component: &str) -> anyhow::Error {
1201	SqliteRuntimeError::NotConfigured {
1202		component: component.to_owned(),
1203	}
1204	.build()
1205}
1206
1207fn extract_named_sqlite_parameters(sql: &str) -> Vec<String> {
1208	let mut ordered_names = Vec::new();
1209	let mut seen = HashSet::new();
1210	let bytes = sql.as_bytes();
1211	let mut idx = 0;
1212
1213	while idx < bytes.len() {
1214		let byte = bytes[idx];
1215		if !matches!(byte, b':' | b'@' | b'$') {
1216			idx += 1;
1217			continue;
1218		}
1219
1220		let start = idx;
1221		idx += 1;
1222		if idx >= bytes.len() || !is_sqlite_param_start(bytes[idx]) {
1223			continue;
1224		}
1225		idx += 1;
1226		while idx < bytes.len() && is_sqlite_param_continue(bytes[idx]) {
1227			idx += 1;
1228		}
1229
1230		let name = &sql[start..idx];
1231		if seen.insert(name.to_owned()) {
1232			ordered_names.push(name.to_owned());
1233		}
1234	}
1235
1236	ordered_names
1237}
1238
1239fn is_sqlite_param_start(byte: u8) -> bool {
1240	byte == b'_' || byte.is_ascii_alphabetic()
1241}
1242
1243fn is_sqlite_param_continue(byte: u8) -> bool {
1244	byte == b'_' || byte.is_ascii_alphanumeric()
1245}
1246
1247fn get_named_sqlite_binding<'a>(
1248	bindings: &'a JsonMap<String, JsonValue>,
1249	name: &str,
1250) -> Option<&'a JsonValue> {
1251	if let Some(value) = bindings.get(name) {
1252		return Some(value);
1253	}
1254
1255	let bare_name = name.get(1..)?;
1256	if let Some(value) = bindings.get(bare_name) {
1257		return Some(value);
1258	}
1259
1260	for prefix in [":", "@", "$"] {
1261		let candidate = format!("{prefix}{bare_name}");
1262		if let Some(value) = bindings.get(&candidate) {
1263			return Some(value);
1264		}
1265	}
1266
1267	None
1268}
1269
1270fn query_result_to_json_rows(result: &QueryResult) -> JsonValue {
1271	JsonValue::Array(
1272		result
1273			.rows
1274			.iter()
1275			.map(|row| {
1276				let mut object = JsonMap::new();
1277				for (index, column) in result.columns.iter().enumerate() {
1278					let value = row
1279						.get(index)
1280						.map(column_value_to_json)
1281						.unwrap_or(JsonValue::Null);
1282					object.insert(column.clone(), value);
1283				}
1284				JsonValue::Object(object)
1285			})
1286			.collect(),
1287	)
1288}
1289
1290fn column_value_to_json(value: &ColumnValue) -> JsonValue {
1291	match value {
1292		ColumnValue::Null => JsonValue::Null,
1293		ColumnValue::Integer(value) => JsonValue::from(*value),
1294		ColumnValue::Float(value) => JsonValue::from(*value),
1295		ColumnValue::Text(value) => JsonValue::String(value.clone()),
1296		ColumnValue::Blob(value) => {
1297			JsonValue::Array(value.iter().map(|byte| JsonValue::from(*byte)).collect())
1298		}
1299	}
1300}
1301
1302fn encode_json_as_cbor(value: &impl Serialize) -> Result<Vec<u8>> {
1303	let mut encoded = Vec::new();
1304	ciborium::into_writer(value, &mut encoded).context("encode sqlite rows as cbor")?;
1305	Ok(encoded)
1306}
1307
1308fn json_type_name(value: &JsonValue) -> &'static str {
1309	match value {
1310		JsonValue::Null => "null",
1311		JsonValue::Bool(_) => "boolean",
1312		JsonValue::Number(_) => "number",
1313		JsonValue::String(_) => "string",
1314		JsonValue::Array(_) => "array",
1315		JsonValue::Object(_) => "object",
1316	}
1317}
1318
1319#[cfg(test)]
1320#[path = "../../../tests/sqlite.rs"]
1321mod tests;