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