Skip to main content

rivetkit_core/actor/sqlite/
mod.rs

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