1use std::fmt::Debug;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8use async_trait::async_trait;
9use parking_lot::RwLock;
10use r2d2::{Pool, PooledConnection};
11use r2d2_sqlite::SqliteConnectionManager;
12use serde::{Deserialize, Serialize};
13
14use helios_fhir::FhirVersion;
15
16use crate::core::{Backend, BackendCapability, BackendKind};
17use crate::error::{BackendError, StorageResult};
18use crate::search::{SearchParameterExtractor, SearchParameterLoader, SearchParameterRegistry};
19
20use super::schema;
21
22static MEMORY_DB_COUNTER: AtomicU64 = AtomicU64::new(0);
24
25pub struct SqliteBackend {
27 pool: Pool<SqliteConnectionManager>,
28 config: SqliteBackendConfig,
29 is_memory: bool,
30 search_registry: Arc<RwLock<SearchParameterRegistry>>,
32 search_extractor: Arc<SearchParameterExtractor>,
34}
35
36impl Debug for SqliteBackend {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 f.debug_struct("SqliteBackend")
39 .field("config", &self.config)
40 .field("is_memory", &self.is_memory)
41 .field("search_registry_len", &self.search_registry.read().len())
42 .finish_non_exhaustive()
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct SqliteBackendConfig {
49 #[serde(default = "default_max_connections")]
51 pub max_connections: u32,
52
53 #[serde(default = "default_min_connections")]
55 pub min_connections: u32,
56
57 #[serde(default = "default_connection_timeout_ms")]
59 pub connection_timeout_ms: u64,
60
61 #[serde(default = "default_busy_timeout_ms")]
63 pub busy_timeout_ms: u32,
64
65 #[serde(default = "default_true")]
67 pub enable_wal: bool,
68
69 #[serde(default = "default_true")]
71 pub enable_foreign_keys: bool,
72
73 #[serde(default)]
76 pub fhir_version: FhirVersion,
77
78 #[serde(default)]
81 pub data_dir: Option<PathBuf>,
82
83 #[serde(default)]
86 pub search_offloaded: bool,
87}
88
89fn default_max_connections() -> u32 {
90 10
91}
92
93fn default_min_connections() -> u32 {
94 1
95}
96
97fn default_connection_timeout_ms() -> u64 {
98 30000
99}
100
101fn default_busy_timeout_ms() -> u32 {
102 5000
103}
104
105fn default_true() -> bool {
106 true
107}
108
109impl Default for SqliteBackendConfig {
110 fn default() -> Self {
111 Self {
112 max_connections: default_max_connections(),
113 min_connections: default_min_connections(),
114 connection_timeout_ms: default_connection_timeout_ms(),
115 busy_timeout_ms: default_busy_timeout_ms(),
116 enable_wal: true,
117 enable_foreign_keys: true,
118 fhir_version: FhirVersion::default(),
119 data_dir: None,
120 search_offloaded: false,
121 }
122 }
123}
124
125impl SqliteBackend {
126 pub fn in_memory() -> StorageResult<Self> {
128 Self::with_config(":memory:", SqliteBackendConfig::default())
129 }
130
131 pub fn open<P: AsRef<Path>>(path: P) -> StorageResult<Self> {
133 Self::with_config(path, SqliteBackendConfig::default())
134 }
135
136 pub fn with_config<P: AsRef<Path>>(
138 path: P,
139 config: SqliteBackendConfig,
140 ) -> StorageResult<Self> {
141 let path_str = path.as_ref().to_string_lossy();
142 let is_memory = path_str == ":memory:";
143
144 let manager = if is_memory {
149 let db_id = MEMORY_DB_COUNTER.fetch_add(1, Ordering::Relaxed);
150 let uri = format!("file:hfs_mem_{}?mode=memory&cache=shared", db_id);
151 SqliteConnectionManager::file(uri)
152 } else {
153 SqliteConnectionManager::file(path.as_ref())
154 };
155
156 let pool = Pool::builder()
157 .max_size(config.max_connections)
158 .min_idle(Some(config.min_connections))
159 .connection_timeout(std::time::Duration::from_millis(
160 config.connection_timeout_ms,
161 ))
162 .build(manager)
163 .map_err(|e| {
164 crate::error::StorageError::Backend(BackendError::ConnectionFailed {
165 backend_name: "sqlite".to_string(),
166 message: e.to_string(),
167 })
168 })?;
169
170 let search_registry = Arc::new(RwLock::new(SearchParameterRegistry::new()));
172 {
173 let loader = SearchParameterLoader::new(config.fhir_version);
174 let mut registry = search_registry.write();
175
176 let mut fallback_count = 0;
178 let mut spec_count = 0;
179 let mut spec_file: Option<PathBuf> = None;
180 let mut custom_count = 0;
181 let mut custom_files: Vec<String> = Vec::new();
182
183 match loader.load_embedded() {
185 Ok(params) => {
186 for param in params {
187 if registry.register(param).is_ok() {
188 fallback_count += 1;
189 }
190 }
191 }
192 Err(e) => {
193 tracing::error!("Failed to load embedded SearchParameters: {}", e);
194 }
195 }
196
197 let data_dir = config
199 .data_dir
200 .clone()
201 .unwrap_or_else(|| PathBuf::from("./data"));
202 let spec_filename = loader.spec_filename();
203 let spec_path = data_dir.join(spec_filename);
204 match loader.load_from_spec_file(&data_dir) {
205 Ok(params) => {
206 for param in params {
207 if registry.register(param).is_ok() {
208 spec_count += 1;
209 }
210 }
211 if spec_count > 0 {
212 spec_file = Some(spec_path);
213 }
214 }
215 Err(e) => {
216 tracing::warn!(
217 "Could not load spec SearchParameters from {}: {}. Using minimal fallback.",
218 spec_path.display(),
219 e
220 );
221 }
222 }
223
224 match loader.load_custom_from_directory_with_files(&data_dir) {
226 Ok((params, files)) => {
227 for param in params {
228 if registry.register(param).is_ok() {
229 custom_count += 1;
230 }
231 }
232 custom_files = files;
233 }
234 Err(e) => {
235 tracing::warn!(
236 "Error loading custom SearchParameters from {}: {}",
237 data_dir.display(),
238 e
239 );
240 }
241 }
242
243 let resource_type_count = registry.resource_types().len();
245 let spec_info = spec_file
246 .map(|p| format!(" from {}", p.display()))
247 .unwrap_or_default();
248 let custom_info = if custom_files.is_empty() {
249 String::new()
250 } else {
251 format!(" [{}]", custom_files.join(", "))
252 };
253 tracing::info!(
254 "SearchParameter registry initialized: {} total ({} spec{}, {} fallback, {} custom{}) covering {} resource types",
255 registry.len(),
256 spec_count,
257 spec_info,
258 fallback_count,
259 custom_count,
260 custom_info,
261 resource_type_count
262 );
263 }
264 let search_extractor = Arc::new(SearchParameterExtractor::new(search_registry.clone()));
265
266 let backend = Self {
267 pool,
268 config,
269 is_memory,
270 search_registry,
271 search_extractor,
272 };
273
274 backend.configure_connection()?;
276
277 Ok(backend)
278 }
279
280 pub fn init_schema(&self) -> StorageResult<()> {
285 let conn = self.get_connection()?;
286 schema::initialize_schema(&conn)?;
287
288 let stored_count = self.load_stored_search_parameters()?;
290 if stored_count > 0 {
291 let registry = self.search_registry.read();
292 tracing::info!(
293 "Loaded {} stored SearchParameters from database (total now: {})",
294 stored_count,
295 registry.len()
296 );
297 }
298
299 Ok(())
300 }
301
302 fn load_stored_search_parameters(&self) -> StorageResult<usize> {
307 use crate::search::registry::{SearchParameterSource, SearchParameterStatus};
308
309 let conn = self.get_connection()?;
310 let mut stmt = conn
311 .prepare(
312 "SELECT data FROM resources WHERE resource_type = 'SearchParameter' AND is_deleted = 0",
313 )
314 .map_err(|e| {
315 crate::error::StorageError::Backend(BackendError::Internal {
316 backend_name: "sqlite".to_string(),
317 message: format!("Failed to prepare SearchParameter query: {}", e),
318 source: None,
319 })
320 })?;
321
322 let loader = SearchParameterLoader::new(self.config.fhir_version);
323 let mut registry = self.search_registry.write();
324 let mut count = 0;
325
326 let rows = stmt
327 .query_map([], |row| row.get::<_, Vec<u8>>(0))
328 .map_err(|e| {
329 crate::error::StorageError::Backend(BackendError::Internal {
330 backend_name: "sqlite".to_string(),
331 message: format!("Failed to query SearchParameters: {}", e),
332 source: None,
333 })
334 })?;
335
336 for row in rows {
337 let data = match row {
338 Ok(data) => data,
339 Err(e) => {
340 tracing::warn!("Failed to read SearchParameter row: {}", e);
341 continue;
342 }
343 };
344
345 let json: serde_json::Value = match serde_json::from_slice(&data) {
346 Ok(json) => json,
347 Err(e) => {
348 tracing::warn!("Failed to parse SearchParameter JSON: {}", e);
349 continue;
350 }
351 };
352
353 match loader.parse_resource(&json) {
354 Ok(mut def) => {
355 if def.status == SearchParameterStatus::Active {
357 def.source = SearchParameterSource::Stored;
358 if registry.register(def).is_ok() {
359 count += 1;
360 }
361 }
362 }
363 Err(e) => {
364 tracing::warn!("Failed to parse stored SearchParameter: {}", e);
365 }
366 }
367 }
368
369 Ok(count)
370 }
371
372 pub(crate) fn get_connection(
374 &self,
375 ) -> StorageResult<PooledConnection<SqliteConnectionManager>> {
376 self.pool.get().map_err(|e| {
377 crate::error::StorageError::Backend(BackendError::ConnectionFailed {
378 backend_name: "sqlite".to_string(),
379 message: e.to_string(),
380 })
381 })
382 }
383
384 pub(crate) fn get_search_registry(&self) -> Arc<RwLock<SearchParameterRegistry>> {
386 Arc::clone(&self.search_registry)
387 }
388
389 fn configure_connection(&self) -> StorageResult<()> {
391 let conn = self.get_connection()?;
392
393 conn.busy_timeout(std::time::Duration::from_millis(
394 self.config.busy_timeout_ms as u64,
395 ))
396 .map_err(|e| {
397 crate::error::StorageError::Backend(BackendError::Internal {
398 backend_name: "sqlite".to_string(),
399 message: format!("Failed to set busy timeout: {}", e),
400 source: None,
401 })
402 })?;
403
404 if self.config.enable_foreign_keys {
405 conn.execute("PRAGMA foreign_keys = ON", []).map_err(|e| {
406 crate::error::StorageError::Backend(BackendError::Internal {
407 backend_name: "sqlite".to_string(),
408 message: format!("Failed to enable foreign keys: {}", e),
409 source: None,
410 })
411 })?;
412 }
413
414 if self.config.enable_wal && !self.is_memory {
415 conn.query_row("PRAGMA journal_mode = WAL", [], |_row| Ok(()))
417 .map_err(|e| {
418 crate::error::StorageError::Backend(BackendError::Internal {
419 backend_name: "sqlite".to_string(),
420 message: format!("Failed to enable WAL mode: {}", e),
421 source: None,
422 })
423 })?;
424 }
425
426 Ok(())
427 }
428
429 pub fn is_memory(&self) -> bool {
431 self.is_memory
432 }
433
434 pub fn config(&self) -> &SqliteBackendConfig {
436 &self.config
437 }
438
439 pub fn search_registry(&self) -> &Arc<RwLock<SearchParameterRegistry>> {
441 &self.search_registry
442 }
443
444 pub fn search_extractor(&self) -> &Arc<SearchParameterExtractor> {
446 &self.search_extractor
447 }
448
449 pub fn is_search_offloaded(&self) -> bool {
451 self.config.search_offloaded
452 }
453
454 pub fn set_search_offloaded(&mut self, offloaded: bool) {
460 self.config.search_offloaded = offloaded;
461 }
462}
463
464#[allow(dead_code)]
466pub struct SqliteConnection(pub(crate) PooledConnection<SqliteConnectionManager>);
467
468impl Debug for SqliteConnection {
469 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470 f.debug_struct("SqliteConnection").finish()
471 }
472}
473
474#[async_trait]
475impl Backend for SqliteBackend {
476 type Connection = SqliteConnection;
477
478 fn kind(&self) -> BackendKind {
479 BackendKind::Sqlite
480 }
481
482 fn name(&self) -> &'static str {
483 "sqlite"
484 }
485
486 fn supports(&self, capability: BackendCapability) -> bool {
487 matches!(
488 capability,
489 BackendCapability::Crud
490 | BackendCapability::Versioning
491 | BackendCapability::InstanceHistory
492 | BackendCapability::TypeHistory
493 | BackendCapability::SystemHistory
494 | BackendCapability::BasicSearch
495 | BackendCapability::DateSearch
496 | BackendCapability::ReferenceSearch
497 | BackendCapability::Sorting
498 | BackendCapability::OffsetPagination
499 | BackendCapability::Transactions
500 | BackendCapability::OptimisticLocking
501 | BackendCapability::Include
502 | BackendCapability::Revinclude
503 | BackendCapability::SharedSchema
504 )
505 }
506
507 fn capabilities(&self) -> Vec<BackendCapability> {
508 vec![
509 BackendCapability::Crud,
510 BackendCapability::Versioning,
511 BackendCapability::InstanceHistory,
512 BackendCapability::TypeHistory,
513 BackendCapability::SystemHistory,
514 BackendCapability::BasicSearch,
515 BackendCapability::DateSearch,
516 BackendCapability::ReferenceSearch,
517 BackendCapability::Sorting,
518 BackendCapability::OffsetPagination,
519 BackendCapability::Transactions,
520 BackendCapability::OptimisticLocking,
521 BackendCapability::Include,
522 BackendCapability::Revinclude,
523 BackendCapability::SharedSchema,
524 ]
525 }
526
527 async fn acquire(&self) -> Result<Self::Connection, BackendError> {
528 let conn = self
529 .pool
530 .get()
531 .map_err(|e| BackendError::ConnectionFailed {
532 backend_name: "sqlite".to_string(),
533 message: e.to_string(),
534 })?;
535 Ok(SqliteConnection(conn))
536 }
537
538 async fn release(&self, _conn: Self::Connection) {
539 }
541
542 async fn health_check(&self) -> Result<(), BackendError> {
543 let conn = self
544 .get_connection()
545 .map_err(|_| BackendError::Unavailable {
546 backend_name: "sqlite".to_string(),
547 message: "Failed to get connection".to_string(),
548 })?;
549 conn.query_row("SELECT 1", [], |_| Ok(()))
550 .map_err(|e| BackendError::Internal {
551 backend_name: "sqlite".to_string(),
552 message: format!("Health check failed: {}", e),
553 source: None,
554 })?;
555 Ok(())
556 }
557
558 async fn initialize(&self) -> Result<(), BackendError> {
559 self.init_schema().map_err(|e| BackendError::Internal {
560 backend_name: "sqlite".to_string(),
561 message: format!("Failed to initialize schema: {}", e),
562 source: None,
563 })
564 }
565
566 async fn migrate(&self) -> Result<(), BackendError> {
567 self.init_schema().map_err(|e| BackendError::Internal {
569 backend_name: "sqlite".to_string(),
570 message: format!("Failed to run migrations: {}", e),
571 source: None,
572 })
573 }
574}
575
576use crate::core::capabilities::{
581 GlobalSearchCapabilities, ResourceSearchCapabilities, SearchCapabilityProvider,
582};
583use crate::types::{
584 IncludeCapability, PaginationCapability, ResultModeCapability, SearchParamFullCapability,
585 SearchParamType, SpecialSearchParam,
586};
587
588impl SearchCapabilityProvider for SqliteBackend {
589 fn resource_search_capabilities(
590 &self,
591 resource_type: &str,
592 ) -> Option<ResourceSearchCapabilities> {
593 let params = {
595 let registry = self.search_registry.read();
596 registry.get_active_params(resource_type)
597 };
598
599 if params.is_empty() {
600 let common_params = {
602 let registry = self.search_registry.read();
603 registry.get_active_params("Resource")
604 };
605 if common_params.is_empty() {
606 return None;
607 }
608 }
609
610 let mut search_params = Vec::new();
612 for param in ¶ms {
613 let mut cap = SearchParamFullCapability::new(¶m.code, param.param_type)
614 .with_definition(¶m.url);
615
616 let modifiers = Self::modifiers_for_type(param.param_type);
618 cap = cap.with_modifiers(modifiers);
619
620 if let Some(ref targets) = param.target {
622 cap = cap.with_targets(targets.iter().map(|s| s.as_str()));
623 }
624
625 search_params.push(cap);
626 }
627
628 let common_params = {
630 let registry = self.search_registry.read();
631 registry.get_active_params("Resource")
632 };
633 for param in &common_params {
634 if !search_params.iter().any(|p| p.name == param.code) {
635 let mut cap = SearchParamFullCapability::new(¶m.code, param.param_type)
636 .with_definition(¶m.url);
637 cap = cap.with_modifiers(Self::modifiers_for_type(param.param_type));
638 search_params.push(cap);
639 }
640 }
641
642 Some(
643 ResourceSearchCapabilities::new(resource_type)
644 .with_special_params(vec![
645 SpecialSearchParam::Id,
646 SpecialSearchParam::LastUpdated,
647 SpecialSearchParam::Tag,
648 SpecialSearchParam::Profile,
649 SpecialSearchParam::Security,
650 ])
651 .with_include_capabilities(vec![
652 IncludeCapability::Include,
653 IncludeCapability::Revinclude,
654 ])
655 .with_pagination_capabilities(vec![
656 PaginationCapability::Count,
657 PaginationCapability::Offset,
658 PaginationCapability::Cursor,
659 PaginationCapability::MaxPageSize(1000),
660 PaginationCapability::DefaultPageSize(20),
661 ])
662 .with_result_mode_capabilities(vec![
663 ResultModeCapability::Total,
664 ResultModeCapability::TotalNone,
665 ResultModeCapability::TotalAccurate,
666 ResultModeCapability::SummaryCount,
667 ])
668 .with_param_list(search_params),
669 )
670 }
671
672 fn global_search_capabilities(&self) -> GlobalSearchCapabilities {
673 GlobalSearchCapabilities::new()
674 .with_special_params(vec![
675 SpecialSearchParam::Id,
676 SpecialSearchParam::LastUpdated,
677 SpecialSearchParam::Tag,
678 SpecialSearchParam::Profile,
679 SpecialSearchParam::Security,
680 ])
681 .with_pagination(vec![
682 PaginationCapability::Count,
683 PaginationCapability::Offset,
684 PaginationCapability::Cursor,
685 PaginationCapability::MaxPageSize(1000),
686 PaginationCapability::DefaultPageSize(20),
687 ])
688 .with_system_search()
689 }
690}
691
692impl SqliteBackend {
693 fn modifiers_for_type(param_type: SearchParamType) -> Vec<&'static str> {
695 match param_type {
696 SearchParamType::String => vec!["exact", "contains", "missing"],
697 SearchParamType::Token => vec!["not", "text", "in", "not-in", "of-type", "missing"],
698 SearchParamType::Reference => vec!["identifier", "missing"],
699 SearchParamType::Date => vec!["missing"],
700 SearchParamType::Number => vec!["missing"],
701 SearchParamType::Quantity => vec!["missing"],
702 SearchParamType::Uri => vec!["below", "above", "missing"],
703 SearchParamType::Composite => vec!["missing"],
704 SearchParamType::Special => vec![],
705 }
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712
713 #[test]
714 fn test_in_memory_backend() {
715 let backend = SqliteBackend::in_memory().unwrap();
716 assert!(backend.is_memory());
717 assert_eq!(backend.name(), "sqlite");
718 assert_eq!(backend.kind(), BackendKind::Sqlite);
719 }
720
721 #[test]
722 fn test_backend_initialization() {
723 let backend = SqliteBackend::in_memory().unwrap();
724 backend.init_schema().unwrap();
725 backend.init_schema().unwrap(); }
727
728 #[test]
729 fn test_backend_capabilities() {
730 let backend = SqliteBackend::in_memory().unwrap();
731
732 assert!(backend.supports(BackendCapability::Crud));
733 assert!(backend.supports(BackendCapability::BasicSearch));
734 assert!(backend.supports(BackendCapability::Transactions));
735 assert!(!backend.supports(BackendCapability::FullTextSearch));
736 }
737
738 #[tokio::test]
739 async fn test_health_check() {
740 let backend = SqliteBackend::in_memory().unwrap();
741 backend.init_schema().unwrap();
742 assert!(backend.health_check().await.is_ok());
743 }
744
745 #[tokio::test]
746 async fn test_acquire_release() {
747 let backend = SqliteBackend::in_memory().unwrap();
748 let conn = backend.acquire().await.unwrap();
749 backend.release(conn).await;
750 }
751
752 #[test]
753 fn test_search_capability_provider_patient() {
754 let backend = SqliteBackend::in_memory().unwrap();
755
756 let caps = backend.resource_search_capabilities("Patient");
758 assert!(caps.is_some(), "Should have capabilities for Patient");
759
760 let caps = caps.unwrap();
761 assert_eq!(caps.resource_type, "Patient");
762
763 assert!(caps.supports_special(SpecialSearchParam::Id));
765 assert!(caps.supports_special(SpecialSearchParam::LastUpdated));
766
767 assert!(caps.supports_include(IncludeCapability::Include));
769 assert!(caps.supports_include(IncludeCapability::Revinclude));
770
771 assert!(
774 !caps.search_params.is_empty(),
775 "Should have search parameters"
776 );
777 }
778
779 #[test]
780 fn test_global_search_capabilities() {
781 let backend = SqliteBackend::in_memory().unwrap();
782
783 let global = backend.global_search_capabilities();
784
785 assert!(
787 global
788 .common_special_params
789 .contains(&SpecialSearchParam::Id)
790 );
791 assert!(
792 global
793 .common_special_params
794 .contains(&SpecialSearchParam::LastUpdated)
795 );
796
797 assert!(global.supports_system_search);
799
800 assert!(!global.common_pagination_capabilities.is_empty());
802 }
803
804 #[test]
805 fn test_modifiers_for_type() {
806 let string_mods = SqliteBackend::modifiers_for_type(SearchParamType::String);
808 assert!(string_mods.contains(&"exact"));
809 assert!(string_mods.contains(&"contains"));
810 assert!(string_mods.contains(&"missing"));
811
812 let token_mods = SqliteBackend::modifiers_for_type(SearchParamType::Token);
814 assert!(token_mods.contains(&"not"));
815 assert!(token_mods.contains(&"text"));
816
817 let ref_mods = SqliteBackend::modifiers_for_type(SearchParamType::Reference);
819 assert!(ref_mods.contains(&"identifier"));
820
821 let uri_mods = SqliteBackend::modifiers_for_type(SearchParamType::Uri);
823 assert!(uri_mods.contains(&"below"));
824 assert!(uri_mods.contains(&"above"));
825 }
826}