Skip to main content

helios_persistence/backends/sqlite/
backend.rs

1//! SQLite backend implementation.
2
3use 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
22/// Counter for generating unique in-memory database names.
23static MEMORY_DB_COUNTER: AtomicU64 = AtomicU64::new(0);
24
25/// SQLite backend for FHIR resource storage.
26pub struct SqliteBackend {
27    pool: Pool<SqliteConnectionManager>,
28    config: SqliteBackendConfig,
29    is_memory: bool,
30    /// Search parameter registry (in-memory cache of active parameters).
31    search_registry: Arc<RwLock<SearchParameterRegistry>>,
32    /// Extractor for deriving searchable values from resources.
33    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/// Configuration for the SQLite backend.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct SqliteBackendConfig {
49    /// Maximum number of connections in the pool.
50    #[serde(default = "default_max_connections")]
51    pub max_connections: u32,
52
53    /// Minimum number of idle connections.
54    #[serde(default = "default_min_connections")]
55    pub min_connections: u32,
56
57    /// Connection timeout in milliseconds.
58    #[serde(default = "default_connection_timeout_ms")]
59    pub connection_timeout_ms: u64,
60
61    /// SQLite busy timeout in milliseconds.
62    #[serde(default = "default_busy_timeout_ms")]
63    pub busy_timeout_ms: u32,
64
65    /// Enable WAL mode for better concurrency.
66    #[serde(default = "default_true")]
67    pub enable_wal: bool,
68
69    /// Enable foreign key constraints.
70    #[serde(default = "default_true")]
71    pub enable_foreign_keys: bool,
72
73    /// FHIR version for this backend instance.
74    /// Used to load the appropriate SearchParameter definitions.
75    #[serde(default)]
76    pub fhir_version: FhirVersion,
77
78    /// Directory containing FHIR SearchParameter spec files.
79    /// If None, defaults to "./data" or the directory containing the executable.
80    #[serde(default)]
81    pub data_dir: Option<PathBuf>,
82
83    /// When true, search indexing is offloaded to a secondary backend (e.g., Elasticsearch).
84    /// The SQLite search_index and resource_fts tables will not be populated.
85    #[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    /// Creates a new in-memory SQLite backend.
127    pub fn in_memory() -> StorageResult<Self> {
128        Self::with_config(":memory:", SqliteBackendConfig::default())
129    }
130
131    /// Opens or creates a file-based SQLite database.
132    pub fn open<P: AsRef<Path>>(path: P) -> StorageResult<Self> {
133        Self::with_config(path, SqliteBackendConfig::default())
134    }
135
136    /// Creates a backend with custom configuration.
137    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        // For in-memory databases with connection pools, we need to use a shared-cache
145        // URI so all connections access the same database. Otherwise, each connection
146        // gets its own isolated in-memory database. Each backend instance gets a unique
147        // database name to ensure test isolation.
148        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        // Initialize the search parameter registry
171        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            // Track counts and sources for summary
177            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            // 1. Load minimal embedded fallback params (always available)
184            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            // 2. Load spec file params (may fail if file missing)
198            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            // 3. Load custom SearchParameters from data directory (optional)
225            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            // Log summary
244            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        // Configure the connection
275        backend.configure_connection()?;
276
277        Ok(backend)
278    }
279
280    /// Initialize the database schema.
281    ///
282    /// This also loads any stored SearchParameter resources from the database
283    /// into the registry.
284    pub fn init_schema(&self) -> StorageResult<()> {
285        let conn = self.get_connection()?;
286        schema::initialize_schema(&conn)?;
287
288        // Load stored (POSTed) SearchParameters from database
289        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    /// Loads SearchParameter resources stored in the database into the registry.
303    ///
304    /// This is called during schema initialization to restore any custom
305    /// SearchParameters that were POSTed to the server.
306    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                    // Only register active parameters
356                    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    /// Get a connection from the pool.
373    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    /// Get the search parameter registry.
385    pub(crate) fn get_search_registry(&self) -> Arc<RwLock<SearchParameterRegistry>> {
386        Arc::clone(&self.search_registry)
387    }
388
389    /// Configure connection settings.
390    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            // PRAGMA journal_mode returns a result, so we use query_row instead of execute
416            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    /// Returns whether this is an in-memory database.
430    pub fn is_memory(&self) -> bool {
431        self.is_memory
432    }
433
434    /// Returns the backend configuration.
435    pub fn config(&self) -> &SqliteBackendConfig {
436        &self.config
437    }
438
439    /// Returns a reference to the search parameter registry.
440    pub fn search_registry(&self) -> &Arc<RwLock<SearchParameterRegistry>> {
441        &self.search_registry
442    }
443
444    /// Returns a reference to the search parameter extractor.
445    pub fn search_extractor(&self) -> &Arc<SearchParameterExtractor> {
446        &self.search_extractor
447    }
448
449    /// Returns whether search indexing is offloaded to a secondary backend.
450    pub fn is_search_offloaded(&self) -> bool {
451        self.config.search_offloaded
452    }
453
454    /// Sets the search offloaded flag.
455    ///
456    /// When set to true, the SQLite backend will skip populating the
457    /// `search_index` and `resource_fts` tables, as search is handled
458    /// by a secondary backend (e.g., Elasticsearch).
459    pub fn set_search_offloaded(&mut self, offloaded: bool) {
460        self.config.search_offloaded = offloaded;
461    }
462}
463
464/// Connection wrapper for SQLite.
465#[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        // Connection is automatically returned to pool when dropped
540    }
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        // Schema migrations are handled by initialize_schema
568        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
576// ============================================================================
577// SearchCapabilityProvider Implementation
578// ============================================================================
579
580use 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        // Get active parameters for this resource type from the registry
594        let params = {
595            let registry = self.search_registry.read();
596            registry.get_active_params(resource_type)
597        };
598
599        if params.is_empty() {
600            // Also check if there are Resource-level params
601            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        // Build search parameter capabilities from the registry
611        let mut search_params = Vec::new();
612        for param in &params {
613            let mut cap = SearchParamFullCapability::new(&param.code, param.param_type)
614                .with_definition(&param.url);
615
616            // Add modifiers based on parameter type
617            let modifiers = Self::modifiers_for_type(param.param_type);
618            cap = cap.with_modifiers(modifiers);
619
620            // Add target types for reference parameters
621            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        // Add common Resource-level parameters
629        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(&param.code, param.param_type)
636                    .with_definition(&param.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    /// Returns supported modifiers for a parameter type.
694    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(); // Should be idempotent
726    }
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        // Get capabilities for Patient resource type
757        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        // Should have common special parameters
764        assert!(caps.supports_special(SpecialSearchParam::Id));
765        assert!(caps.supports_special(SpecialSearchParam::LastUpdated));
766
767        // Should support includes
768        assert!(caps.supports_include(IncludeCapability::Include));
769        assert!(caps.supports_include(IncludeCapability::Revinclude));
770
771        // Should have search parameters from the registry
772        // The exact set depends on what's loaded from R4 parameters
773        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        // Should have common special parameters
786        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        // Should support system search
798        assert!(global.supports_system_search);
799
800        // Should have pagination capabilities
801        assert!(!global.common_pagination_capabilities.is_empty());
802    }
803
804    #[test]
805    fn test_modifiers_for_type() {
806        // String modifiers
807        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        // Token modifiers
813        let token_mods = SqliteBackend::modifiers_for_type(SearchParamType::Token);
814        assert!(token_mods.contains(&"not"));
815        assert!(token_mods.contains(&"text"));
816
817        // Reference modifiers
818        let ref_mods = SqliteBackend::modifiers_for_type(SearchParamType::Reference);
819        assert!(ref_mods.contains(&"identifier"));
820
821        // URI modifiers
822        let uri_mods = SqliteBackend::modifiers_for_type(SearchParamType::Uri);
823        assert!(uri_mods.contains(&"below"));
824        assert!(uri_mods.contains(&"above"));
825    }
826}