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