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 enabled: bool,
245 invocation_telemetry: Option<crate::ActorInvocationTelemetry>,
246 #[cfg(feature = "sqlite-local")]
247 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 #[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 #[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 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 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(¶ms);
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(¶ms);
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(¶ms);
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 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 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 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 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 pub async fn close_and_wait(&self) -> Result<()> {
990 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 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 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 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 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 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