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().await
800			},
801			"sqlite close task failed",
802		)
803		.await
804	}
805
806	async fn close_backend(&self) -> Result<()> {
807		match self.backend {
808			SqliteBackend::LocalNative => {
809				#[cfg(feature = "sqlite-local")]
810				{
811					let native_db = self.db.lock().take();
812					if let Some(native_db) = native_db {
813						let result = self.map_local_worker_result(native_db.close().await);
814						self.abort_worker_failure_monitor();
815						if let Some(metrics) = self.vfs_metrics.as_ref() {
816							metrics.set_worker_active(false);
817						}
818						result?;
819					}
820				}
821				Ok(())
822			}
823			SqliteBackend::RemoteEnvoy => Ok(()),
824		}
825	}
826
827	pub(crate) async fn cleanup_for_shutdown(&self, reusable: bool) -> Result<()> {
828		// A remote database has no actor-local resources to release. Keep its
829		// coordinator usable across a sleep/wake cycle on the same ActorTask.
830		if reusable && self.backend == SqliteBackend::RemoteEnvoy {
831			return Ok(());
832		}
833		self.close().await
834	}
835
836	#[cfg(test)]
837	pub(crate) fn fresh_remote_for_test(&self) -> Self {
838		Self::new_with_remote_sqlite(
839			self.handle
840				.clone()
841				.expect("remote sqlite test database should have an envoy handle"),
842			self.actor_id
843				.clone()
844				.expect("remote sqlite test database should have an actor id"),
845			self.actor_key.clone(),
846			self.generation,
847			self.enabled,
848			true,
849		)
850		.expect("remote sqlite test database should be configured")
851	}
852
853	pub fn take_last_kv_error(&self) -> Option<String> {
854		if self.backend != SqliteBackend::LocalNative {
855			return None;
856		}
857
858		#[cfg(feature = "sqlite-local")]
859		{
860			return self
861				.db
862				.lock()
863				.as_ref()
864				.and_then(NativeDatabaseHandle::take_last_kv_error);
865		}
866
867		#[cfg(not(feature = "sqlite-local"))]
868		None
869	}
870
871	#[cfg(feature = "sqlite-local")]
872	fn native_db_handle(&self) -> Result<NativeDatabaseHandle> {
873		self.db
874			.lock()
875			.as_ref()
876			.cloned()
877			.ok_or_else(|| SqliteRuntimeError::Closed.build())
878	}
879
880	#[cfg(feature = "sqlite-local")]
881	fn map_local_worker_result<T>(&self, result: Result<T>) -> Result<T> {
882		match result {
883			Ok(value) => Ok(value),
884			Err(error) => {
885				if is_fatal_worker_error(&error) {
886					self.report_worker_fatal(&error);
887				}
888				Err(map_local_worker_error(error))
889			}
890		}
891	}
892
893	#[cfg(feature = "sqlite-local")]
894	fn report_worker_fatal(&self, error: &anyhow::Error) {
895		let Ok(config) = self.runtime_config() else {
896			return;
897		};
898		report_sqlite_worker_fatal(
899			&self.worker_fatal_reported,
900			config,
901			sqlite_worker_fatal_message(error),
902		);
903	}
904
905	#[cfg(feature = "sqlite-local")]
906	fn start_worker_failure_monitor(
907		&self,
908		native_db: NativeDatabaseHandle,
909		config: SqliteRuntimeConfig,
910	) {
911		self.abort_worker_failure_monitor();
912		let reported = Arc::clone(&self.worker_fatal_reported);
913		let task = RuntimeSpawner::spawn(async move {
914			if native_db.wait_for_worker_failure().await {
915				report_sqlite_worker_fatal(
916					&reported,
917					config,
918					"sqlite worker thread stopped unexpectedly".to_string(),
919				);
920			}
921		});
922		*self.worker_failure_task.lock() = Some(task);
923	}
924
925	#[cfg(feature = "sqlite-local")]
926	fn abort_worker_failure_monitor(&self) {
927		if let Some(task) = self.worker_failure_task.lock().take() {
928			task.abort();
929		}
930	}
931
932	pub fn metrics(&self) -> Option<SqliteVfsMetricsSnapshot> {
933		#[cfg(feature = "sqlite-local")]
934		{
935			self.db
936				.lock()
937				.as_ref()
938				.map(NativeDatabaseHandle::sqlite_vfs_metrics)
939		}
940
941		#[cfg(not(feature = "sqlite-local"))]
942		{
943			None
944		}
945	}
946
947	pub fn runtime_config(&self) -> Result<SqliteRuntimeConfig> {
948		Ok(SqliteRuntimeConfig {
949			handle: self.handle()?,
950			actor_id: self
951				.actor_id
952				.clone()
953				.ok_or_else(|| sqlite_not_configured("actor id"))?,
954			generation: self.generation,
955		})
956	}
957
958	fn log_operation_error(
959		&self,
960		operation: &'static str,
961		sql: &str,
962		binding_count: usize,
963		error: &anyhow::Error,
964	) {
965		self.log_operation_error_with_count(operation, sql, binding_count, 1, error);
966	}
967
968	fn log_operation_error_with_count(
969		&self,
970		operation: &'static str,
971		sql: &str,
972		binding_count: usize,
973		statement_count: usize,
974		error: &anyhow::Error,
975	) {
976		let structured = RivetError::extract(error);
977		let error_chain = error.chain().map(ToString::to_string).collect::<Vec<_>>();
978		tracing::error!(
979			actor_id = self.actor_id.as_deref().unwrap_or("<unknown>"),
980			generation = ?self.generation,
981			backend = ?self.backend,
982			operation,
983			sql,
984			binding_count,
985			statement_count,
986			group = structured.group(),
987			code = structured.code(),
988			error_message = %structured.message(),
989			metadata = ?structured.metadata(),
990			error_chain = ?error_chain,
991			"sqlite operation failed"
992		);
993	}
994
995	fn actor_specifier(&self) -> Option<ActorSpecifier> {
996		let mut specifier = ActorSpecifier::new(self.actor_id.as_ref()?.clone(), self.generation?);
997		if let Some(key) = self.actor_key.as_ref() {
998			specifier = specifier.with_key(key.clone());
999		}
1000		Some(specifier)
1001	}
1002
1003	fn attach_actor(&self, error: anyhow::Error) -> anyhow::Error {
1004		match self.actor_specifier() {
1005			Some(actor) => error.context(actor),
1006			None => error,
1007		}
1008	}
1009
1010	fn remote_config(&self) -> Result<RemoteSqliteConfig> {
1011		let config = self.runtime_config()?;
1012		let generation = config
1013			.generation
1014			.ok_or_else(|| sqlite_not_configured("generation"))?;
1015		Ok(RemoteSqliteConfig {
1016			namespace_id: config.handle.namespace().to_owned(),
1017			handle: config.handle,
1018			actor_id: config.actor_id,
1019			generation,
1020		})
1021	}
1022
1023	async fn remote_exec(&self, sql: String) -> Result<QueryResult> {
1024		let config = self.remote_config()?;
1025		let response = config
1026			.handle
1027			.remote_sqlite_exec(protocol::SqliteExecRequest {
1028				namespace_id: config.namespace_id,
1029				actor_id: config.actor_id,
1030				generation: config.generation,
1031				sql,
1032			})
1033			.await
1034			.map_err(remote_request_error)?;
1035
1036		match response {
1037			protocol::SqliteExecResponse::SqliteExecOk(ok) => {
1038				Ok(query_result_from_protocol(ok.result))
1039			}
1040			protocol::SqliteExecResponse::SqliteErrorResponse(error) => {
1041				Err(self.remote_sqlite_error_response(error))
1042			}
1043		}
1044	}
1045
1046	async fn remote_exec_with_session(
1047		&self,
1048		sql: String,
1049		expected_session: Option<u64>,
1050	) -> Result<(QueryResult, u64)> {
1051		let config = self.remote_config()?;
1052		let (response, session) = config
1053			.handle
1054			.remote_sqlite_exec_with_session(
1055				protocol::SqliteExecRequest {
1056					namespace_id: config.namespace_id,
1057					actor_id: config.actor_id,
1058					generation: config.generation,
1059					sql,
1060				},
1061				expected_session,
1062			)
1063			.await?;
1064
1065		match response {
1066			protocol::SqliteExecResponse::SqliteExecOk(ok) => {
1067				Ok((query_result_from_protocol(ok.result), session))
1068			}
1069			protocol::SqliteExecResponse::SqliteErrorResponse(error) => {
1070				Err(self.remote_sqlite_error_response(error))
1071			}
1072		}
1073	}
1074
1075	async fn remote_execute(
1076		&self,
1077		sql: String,
1078		params: Option<Vec<BindParam>>,
1079	) -> Result<ExecuteResult> {
1080		let config = self.remote_config()?;
1081		let response = config
1082			.handle
1083			.remote_sqlite_execute(protocol::SqliteExecuteRequest {
1084				namespace_id: config.namespace_id,
1085				actor_id: config.actor_id,
1086				generation: config.generation,
1087				sql,
1088				params: params.map(protocol_bind_params),
1089			})
1090			.await
1091			.map_err(remote_request_error)?;
1092
1093		match response {
1094			protocol::SqliteExecuteResponse::SqliteExecuteOk(ok) => {
1095				Ok(execute_result_from_protocol(ok.result))
1096			}
1097			protocol::SqliteExecuteResponse::SqliteErrorResponse(error) => {
1098				Err(self.remote_sqlite_error_response(error))
1099			}
1100		}
1101	}
1102
1103	async fn remote_execute_batch(
1104		&self,
1105		statements: Vec<SqliteBatchStatement>,
1106	) -> Result<Vec<ExecuteResult>> {
1107		let config = self.remote_config()?;
1108		let response = config
1109			.handle
1110			.remote_sqlite_execute_batch(protocol::SqliteExecuteBatchRequest {
1111				namespace_id: config.namespace_id,
1112				actor_id: config.actor_id,
1113				generation: config.generation,
1114				statements: statements
1115					.into_iter()
1116					.map(|statement| protocol::SqliteBatchStatement {
1117						sql: statement.sql,
1118						params: statement.params.map(protocol_bind_params),
1119					})
1120					.collect(),
1121			})
1122			.await
1123			.map_err(remote_request_error)?;
1124
1125		match response {
1126			protocol::SqliteExecuteBatchResponse::SqliteExecuteBatchOk(ok) => Ok(ok
1127				.results
1128				.into_iter()
1129				.map(execute_result_from_protocol)
1130				.collect()),
1131			protocol::SqliteExecuteBatchResponse::SqliteErrorResponse(error) => {
1132				Err(self.remote_sqlite_error_response(error))
1133			}
1134		}
1135	}
1136
1137	async fn remote_execute_with_session(
1138		&self,
1139		sql: String,
1140		params: Option<Vec<BindParam>>,
1141		expected_session: Option<u64>,
1142	) -> Result<(ExecuteResult, u64)> {
1143		let config = self.remote_config()?;
1144		let (response, session) = config
1145			.handle
1146			.remote_sqlite_execute_with_session(
1147				protocol::SqliteExecuteRequest {
1148					namespace_id: config.namespace_id,
1149					actor_id: config.actor_id,
1150					generation: config.generation,
1151					sql,
1152					params: params.map(protocol_bind_params),
1153				},
1154				expected_session,
1155			)
1156			.await?;
1157
1158		match response {
1159			protocol::SqliteExecuteResponse::SqliteExecuteOk(ok) => {
1160				Ok((execute_result_from_protocol(ok.result), session))
1161			}
1162			protocol::SqliteExecuteResponse::SqliteErrorResponse(error) => {
1163				Err(self.remote_sqlite_error_response(error))
1164			}
1165		}
1166	}
1167
1168	pub(crate) async fn query_rows_cbor(
1169		&self,
1170		sql: &str,
1171		params: Option<&[u8]>,
1172	) -> Result<Vec<u8>> {
1173		let bind_params = bind_params_from_cbor(sql, params)?;
1174		let result = self.query(sql.to_owned(), bind_params).await?;
1175		encode_json_as_cbor(&query_result_to_json_rows(&result))
1176	}
1177
1178	pub(crate) async fn exec_rows_cbor(&self, sql: &str) -> Result<Vec<u8>> {
1179		let result = self.exec(sql.to_owned()).await?;
1180		encode_json_as_cbor(&query_result_to_json_rows(&result))
1181	}
1182
1183	pub(crate) async fn run_cbor(&self, sql: &str, params: Option<&[u8]>) -> Result<ExecResult> {
1184		let bind_params = bind_params_from_cbor(sql, params)?;
1185		self.run(sql.to_owned(), bind_params).await
1186	}
1187
1188	pub(crate) async fn execute_rows_cbor(
1189		&self,
1190		sql: &str,
1191		params: Option<&[u8]>,
1192	) -> Result<Vec<u8>> {
1193		let bind_params = bind_params_from_cbor(sql, params)?;
1194		let result = self.execute(sql.to_owned(), bind_params).await?;
1195		encode_json_as_cbor(&query_result_to_json_rows(&QueryResult {
1196			columns: result.columns,
1197			rows: result.rows,
1198		}))
1199	}
1200
1201	fn handle(&self) -> Result<EnvoyHandle> {
1202		self.handle
1203			.clone()
1204			.ok_or_else(|| sqlite_not_configured("handle"))
1205	}
1206
1207	fn remote_sqlite_error_response(&self, error: protocol::SqliteErrorResponse) -> anyhow::Error {
1208		if is_head_fence_mismatch_response(&error) {
1209			if let Ok(config) = self.runtime_config() {
1210				report_sqlite_worker_fatal(
1211					&self.worker_fatal_reported,
1212					config,
1213					format!("remote sqlite fatal storage error: {}", error.message),
1214				);
1215			}
1216			return SqliteRuntimeError::Closed.build();
1217		}
1218
1219		remote_sqlite_error_response(error.message)
1220	}
1221}
1222
1223fn report_sqlite_worker_fatal(reported: &AtomicBool, config: SqliteRuntimeConfig, message: String) {
1224	if reported.swap(true, Ordering::AcqRel) {
1225		return;
1226	}
1227	// A dead worker means SQLite's sole native connection is no longer a valid
1228	// actor subsystem. Core reports that through envoy lifecycle instead of
1229	// letting the actor continue to serve requests with a broken database.
1230	// This is a crash, not a deliberate destroy, so it goes out as a sleep
1231	// intent: the next generation opens a fresh worker over the same durable
1232	// state.
1233	config.handle.sleep_actor(
1234		config.actor_id,
1235		config
1236			.generation
1237			.and_then(|generation| generation.try_into().ok()),
1238		Some(message),
1239	);
1240}
1241
1242struct RemoteSqliteConfig {
1243	handle: EnvoyHandle,
1244	namespace_id: String,
1245	actor_id: String,
1246	generation: u64,
1247}
1248
1249fn select_sqlite_backend(remote_sqlite: bool) -> Result<SqliteBackend> {
1250	if remote_sqlite {
1251		return Ok(SqliteBackend::RemoteEnvoy);
1252	}
1253
1254	#[cfg(feature = "sqlite-local")]
1255	{
1256		Ok(SqliteBackend::LocalNative)
1257	}
1258
1259	#[cfg(not(feature = "sqlite-local"))]
1260	{
1261		Err(SqliteRuntimeError::Unavailable.build())
1262	}
1263}
1264
1265fn bind_param_count(params: &Option<Vec<BindParam>>) -> usize {
1266	params.as_ref().map_or(0, Vec::len)
1267}
1268
1269#[cfg(feature = "sqlite-local")]
1270fn duration_ns(duration: std::time::Duration) -> u64 {
1271	duration.as_nanos().try_into().unwrap_or(u64::MAX)
1272}
1273
1274#[cfg(feature = "sqlite-local")]
1275fn is_fatal_worker_error(error: &anyhow::Error) -> bool {
1276	error.downcast_ref::<SqliteWorkerFatalError>().is_some()
1277		|| error.downcast_ref::<SqliteWorkerDeadError>().is_some()
1278		|| error
1279			.downcast_ref::<SqliteWorkerCloseTimeoutError>()
1280			.is_some()
1281}
1282
1283#[cfg(feature = "sqlite-local")]
1284fn sqlite_worker_fatal_message(error: &anyhow::Error) -> String {
1285	if let Some(error) = error.downcast_ref::<SqliteWorkerFatalError>() {
1286		return format!("sqlite fatal storage error: {}", error.message());
1287	}
1288
1289	format!("sqlite worker failed: {error}")
1290}
1291
1292#[cfg(feature = "sqlite-local")]
1293fn map_local_worker_error(error: anyhow::Error) -> anyhow::Error {
1294	if error
1295		.downcast_ref::<SqliteWorkerOverloadedError>()
1296		.is_some()
1297	{
1298		return ActorLifecycle::Overloaded {
1299			channel: "sqlite_worker".to_string(),
1300			capacity: SQLITE_WORKER_QUEUE_CAPACITY,
1301			operation: "execute sqlite command".to_string(),
1302		}
1303		.build();
1304	}
1305
1306	if error.downcast_ref::<SqliteWorkerClosingError>().is_some()
1307		|| error.downcast_ref::<SqliteWorkerDeadError>().is_some()
1308		|| error.downcast_ref::<SqliteWorkerFatalError>().is_some()
1309	{
1310		return SqliteRuntimeError::Closed.build();
1311	}
1312
1313	error
1314}
1315
1316fn protocol_bind_params(params: Vec<BindParam>) -> Vec<protocol::SqliteBindParam> {
1317	params.into_iter().map(protocol_bind_param).collect()
1318}
1319
1320fn protocol_bind_param(param: BindParam) -> protocol::SqliteBindParam {
1321	match param {
1322		BindParam::Null => protocol::SqliteBindParam::SqliteValueNull,
1323		BindParam::Integer(value) => {
1324			protocol::SqliteBindParam::SqliteValueInteger(protocol::SqliteValueInteger { value })
1325		}
1326		BindParam::Float(value) => {
1327			protocol::SqliteBindParam::SqliteValueFloat(protocol::SqliteValueFloat {
1328				value: value.to_bits().to_be_bytes(),
1329			})
1330		}
1331		BindParam::Text(value) => {
1332			protocol::SqliteBindParam::SqliteValueText(protocol::SqliteValueText { value })
1333		}
1334		BindParam::Blob(value) => {
1335			protocol::SqliteBindParam::SqliteValueBlob(protocol::SqliteValueBlob { value })
1336		}
1337	}
1338}
1339
1340fn query_result_from_protocol(result: protocol::SqliteQueryResult) -> QueryResult {
1341	QueryResult {
1342		columns: result.columns,
1343		rows: result
1344			.rows
1345			.into_iter()
1346			.map(|row| row.into_iter().map(column_value_from_protocol).collect())
1347			.collect(),
1348	}
1349}
1350
1351fn execute_result_from_protocol(result: protocol::SqliteExecuteResult) -> ExecuteResult {
1352	ExecuteResult {
1353		columns: result.columns,
1354		rows: result
1355			.rows
1356			.into_iter()
1357			.map(|row| row.into_iter().map(column_value_from_protocol).collect())
1358			.collect(),
1359		changes: result.changes,
1360		last_insert_row_id: result.last_insert_row_id,
1361	}
1362}
1363
1364fn column_value_from_protocol(value: protocol::SqliteColumnValue) -> ColumnValue {
1365	match value {
1366		protocol::SqliteColumnValue::SqliteValueNull => ColumnValue::Null,
1367		protocol::SqliteColumnValue::SqliteValueInteger(value) => ColumnValue::Integer(value.value),
1368		protocol::SqliteColumnValue::SqliteValueFloat(value) => {
1369			ColumnValue::Float(f64::from_bits(u64::from_be_bytes(value.value)))
1370		}
1371		protocol::SqliteColumnValue::SqliteValueText(value) => ColumnValue::Text(value.value),
1372		protocol::SqliteColumnValue::SqliteValueBlob(value) => ColumnValue::Blob(value.value),
1373	}
1374}
1375
1376fn remote_request_error(error: anyhow::Error) -> anyhow::Error {
1377	if let Some(indeterminate) = error.downcast_ref::<RemoteSqliteIndeterminateResultError>() {
1378		return SqliteRuntimeError::RemoteIndeterminateResult {
1379			operation: indeterminate.operation.to_owned(),
1380		}
1381		.build();
1382	}
1383
1384	if let Some(compatibility) =
1385		error.downcast_ref::<protocol::versioned::ProtocolCompatibilityError>()
1386	{
1387		if compatibility.feature
1388			== protocol::versioned::ProtocolCompatibilityFeature::RemoteSqliteExecution
1389		{
1390			return SqliteRuntimeError::RemoteUnavailable {
1391				reason: compatibility.to_string(),
1392			}
1393			.build();
1394		}
1395	}
1396
1397	error
1398}
1399
1400fn remote_sqlite_error_response(message: String) -> anyhow::Error {
1401	if message.contains("unavailable") || message.contains("unsupported") {
1402		return SqliteRuntimeError::RemoteUnavailable { reason: message }.build();
1403	}
1404
1405	SqliteRuntimeError::RemoteExecutionFailed { message }.build()
1406}
1407
1408fn is_head_fence_mismatch_response(error: &protocol::SqliteErrorResponse) -> bool {
1409	is_head_fence_mismatch(&error.group, &error.code)
1410}
1411impl std::fmt::Debug for SqliteDb {
1412	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1413		f.debug_struct("SqliteDb")
1414			.field("configured", &self.handle.is_some())
1415			.field("actor_id", &self.actor_id)
1416			.finish()
1417	}
1418}
1419
1420fn bind_params_from_cbor(sql: &str, params: Option<&[u8]>) -> Result<Option<Vec<BindParam>>> {
1421	let Some(params) = params else {
1422		return Ok(None);
1423	};
1424	if params.is_empty() {
1425		return Ok(None);
1426	}
1427
1428	let value = ciborium::from_reader::<JsonValue, _>(Cursor::new(params))
1429		.context("decode sqlite bind params as cbor json")?;
1430	match value {
1431		JsonValue::Array(values) => values
1432			.iter()
1433			.map(json_to_bind_param)
1434			.collect::<Result<Vec<_>>>()
1435			.map(Some),
1436		JsonValue::Object(properties) => {
1437			let ordered_names = extract_named_sqlite_parameters(sql);
1438			if ordered_names.is_empty() {
1439				return properties
1440					.values()
1441					.map(json_to_bind_param)
1442					.collect::<Result<Vec<_>>>()
1443					.map(Some);
1444			}
1445
1446			ordered_names
1447				.iter()
1448				.map(|name| {
1449					get_named_sqlite_binding(&properties, name)
1450						.ok_or_else(|| {
1451							SqliteRuntimeError::InvalidBindParameter {
1452								name: name.clone(),
1453								reason: "missing parameter".to_owned(),
1454							}
1455							.build()
1456						})
1457						.and_then(json_to_bind_param)
1458				})
1459				.collect::<Result<Vec<_>>>()
1460				.map(Some)
1461		}
1462		JsonValue::Null => Ok(None),
1463		other => Err(SqliteRuntimeError::InvalidBindParameter {
1464			name: "params".to_owned(),
1465			reason: format!("expected array or object, got {}", json_type_name(&other)),
1466		}
1467		.build()),
1468	}
1469}
1470
1471fn json_to_bind_param(value: &JsonValue) -> Result<BindParam> {
1472	match value {
1473		JsonValue::Null => Ok(BindParam::Null),
1474		JsonValue::Bool(value) => Ok(BindParam::Integer(i64::from(*value))),
1475		JsonValue::Number(value) => {
1476			if let Some(value) = value.as_i64() {
1477				return Ok(BindParam::Integer(value));
1478			}
1479			if let Some(value) = value.as_u64() {
1480				let value = i64::try_from(value)
1481					.context("sqlite integer bind parameter exceeds i64 range")?;
1482				return Ok(BindParam::Integer(value));
1483			}
1484			value.as_f64().map(BindParam::Float).ok_or_else(|| {
1485				SqliteRuntimeError::InvalidBindParameter {
1486					name: "number".to_owned(),
1487					reason: "unsupported numeric value".to_owned(),
1488				}
1489				.build()
1490			})
1491		}
1492		JsonValue::String(value) => Ok(BindParam::Text(value.clone())),
1493		other => Err(SqliteRuntimeError::InvalidBindParameter {
1494			name: "value".to_owned(),
1495			reason: format!("unsupported type {}", json_type_name(other)),
1496		}
1497		.build()),
1498	}
1499}
1500
1501fn sqlite_not_configured(component: &str) -> anyhow::Error {
1502	SqliteRuntimeError::NotConfigured {
1503		component: component.to_owned(),
1504	}
1505	.build()
1506}
1507
1508fn extract_named_sqlite_parameters(sql: &str) -> Vec<String> {
1509	let mut ordered_names = Vec::new();
1510	let mut seen = HashSet::new();
1511	let bytes = sql.as_bytes();
1512	let mut idx = 0;
1513
1514	while idx < bytes.len() {
1515		let byte = bytes[idx];
1516		if !matches!(byte, b':' | b'@' | b'$') {
1517			idx += 1;
1518			continue;
1519		}
1520
1521		let start = idx;
1522		idx += 1;
1523		if idx >= bytes.len() || !is_sqlite_param_start(bytes[idx]) {
1524			continue;
1525		}
1526		idx += 1;
1527		while idx < bytes.len() && is_sqlite_param_continue(bytes[idx]) {
1528			idx += 1;
1529		}
1530
1531		let name = &sql[start..idx];
1532		if seen.insert(name.to_owned()) {
1533			ordered_names.push(name.to_owned());
1534		}
1535	}
1536
1537	ordered_names
1538}
1539
1540fn is_sqlite_param_start(byte: u8) -> bool {
1541	byte == b'_' || byte.is_ascii_alphabetic()
1542}
1543
1544fn is_sqlite_param_continue(byte: u8) -> bool {
1545	byte == b'_' || byte.is_ascii_alphanumeric()
1546}
1547
1548fn get_named_sqlite_binding<'a>(
1549	bindings: &'a JsonMap<String, JsonValue>,
1550	name: &str,
1551) -> Option<&'a JsonValue> {
1552	if let Some(value) = bindings.get(name) {
1553		return Some(value);
1554	}
1555
1556	let bare_name = name.get(1..)?;
1557	if let Some(value) = bindings.get(bare_name) {
1558		return Some(value);
1559	}
1560
1561	for prefix in [":", "@", "$"] {
1562		let candidate = format!("{prefix}{bare_name}");
1563		if let Some(value) = bindings.get(&candidate) {
1564			return Some(value);
1565		}
1566	}
1567
1568	None
1569}
1570
1571fn query_result_to_json_rows(result: &QueryResult) -> JsonValue {
1572	JsonValue::Array(
1573		result
1574			.rows
1575			.iter()
1576			.map(|row| {
1577				let mut object = JsonMap::new();
1578				for (index, column) in result.columns.iter().enumerate() {
1579					let value = row
1580						.get(index)
1581						.map(column_value_to_json)
1582						.unwrap_or(JsonValue::Null);
1583					object.insert(column.clone(), value);
1584				}
1585				JsonValue::Object(object)
1586			})
1587			.collect(),
1588	)
1589}
1590
1591fn column_value_to_json(value: &ColumnValue) -> JsonValue {
1592	match value {
1593		ColumnValue::Null => JsonValue::Null,
1594		ColumnValue::Integer(value) => JsonValue::from(*value),
1595		ColumnValue::Float(value) => JsonValue::from(*value),
1596		ColumnValue::Text(value) => JsonValue::String(value.clone()),
1597		ColumnValue::Blob(value) => {
1598			JsonValue::Array(value.iter().map(|byte| JsonValue::from(*byte)).collect())
1599		}
1600	}
1601}
1602
1603fn encode_json_as_cbor(value: &impl Serialize) -> Result<Vec<u8>> {
1604	let mut encoded = Vec::new();
1605	ciborium::into_writer(value, &mut encoded).context("encode sqlite rows as cbor")?;
1606	Ok(encoded)
1607}
1608
1609fn json_type_name(value: &JsonValue) -> &'static str {
1610	match value {
1611		JsonValue::Null => "null",
1612		JsonValue::Bool(_) => "boolean",
1613		JsonValue::Number(_) => "number",
1614		JsonValue::String(_) => "string",
1615		JsonValue::Array(_) => "array",
1616		JsonValue::Object(_) => "object",
1617	}
1618}
1619
1620#[cfg(test)]
1621#[path = "../../../tests/sqlite.rs"]
1622mod tests;