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