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 = "crate::default_fhir_version")]
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_enabled(),
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 let manager = manager.with_init(|conn| {
159 crate::sof::sqlite_udfs::register(conn).map_err(|e| {
160 rusqlite::Error::SqliteFailure(
161 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_ERROR),
162 Some(format!("failed to register SOF SQLite UDFs: {e}")),
163 )
164 })
165 });
166
167 let pool = Pool::builder()
168 .max_size(config.max_connections)
169 .min_idle(Some(config.min_connections))
170 .connection_timeout(std::time::Duration::from_millis(
171 config.connection_timeout_ms,
172 ))
173 .build(manager)
174 .map_err(|e| {
175 crate::error::StorageError::Backend(BackendError::ConnectionFailed {
176 backend_name: "sqlite".to_string(),
177 message: e.to_string(),
178 })
179 })?;
180
181 let search_registry = Arc::new(RwLock::new(SearchParameterRegistry::new()));
183 {
184 let loader = SearchParameterLoader::new(config.fhir_version);
185 let mut registry = search_registry.write();
186
187 let mut fallback_count = 0;
189 let mut spec_count = 0;
190 let mut spec_file: Option<PathBuf> = None;
191 let mut custom_count = 0;
192 let mut custom_files: Vec<String> = Vec::new();
193
194 match loader.load_embedded() {
196 Ok(params) => {
197 for param in params {
198 if registry.register(param).is_ok() {
199 fallback_count += 1;
200 }
201 }
202 }
203 Err(e) => {
204 tracing::error!("Failed to load embedded SearchParameters: {}", e);
205 }
206 }
207
208 let data_dir = config
210 .data_dir
211 .clone()
212 .unwrap_or_else(|| PathBuf::from("./data"));
213 let spec_filename = loader.spec_filename();
214 let spec_path = data_dir.join(spec_filename);
215 match loader.load_from_spec_file(&data_dir) {
216 Ok(params) => {
217 for param in params {
218 if registry.register(param).is_ok() {
219 spec_count += 1;
220 }
221 }
222 if spec_count > 0 {
223 spec_file = Some(spec_path);
224 }
225 }
226 Err(e) => {
227 tracing::warn!(
228 "Could not load spec SearchParameters from {}: {}. Using minimal fallback.",
229 spec_path.display(),
230 e
231 );
232 }
233 }
234
235 match loader.load_custom_from_directory_with_files(&data_dir) {
237 Ok((params, files)) => {
238 for param in params {
239 if registry.register(param).is_ok() {
240 custom_count += 1;
241 }
242 }
243 custom_files = files;
244 }
245 Err(e) => {
246 tracing::warn!(
247 "Error loading custom SearchParameters from {}: {}",
248 data_dir.display(),
249 e
250 );
251 }
252 }
253
254 let resource_type_count = registry.resource_types().len();
256 let spec_info = spec_file
257 .map(|p| format!(" from {}", p.display()))
258 .unwrap_or_default();
259 let custom_info = if custom_files.is_empty() {
260 String::new()
261 } else {
262 format!(" [{}]", custom_files.join(", "))
263 };
264 tracing::info!(
265 "SearchParameter registry initialized: {} total ({} spec{}, {} fallback, {} custom{}) covering {} resource types",
266 registry.len(),
267 spec_count,
268 spec_info,
269 fallback_count,
270 custom_count,
271 custom_info,
272 resource_type_count
273 );
274 }
275 let search_extractor = Arc::new(SearchParameterExtractor::new(search_registry.clone()));
276
277 let backend = Self {
278 pool,
279 config,
280 is_memory,
281 search_registry,
282 search_extractor,
283 };
284
285 backend.configure_connection()?;
287
288 Ok(backend)
289 }
290
291 pub fn init_schema(&self) -> StorageResult<()> {
296 let conn = self.get_connection()?;
297 schema::initialize_schema(&conn)?;
298
299 let stored_count = self.load_stored_search_parameters()?;
301 if stored_count > 0 {
302 let registry = self.search_registry.read();
303 tracing::info!(
304 "Loaded {} stored SearchParameters from database (total now: {})",
305 stored_count,
306 registry.len()
307 );
308 }
309
310 Ok(())
311 }
312
313 fn load_stored_search_parameters(&self) -> StorageResult<usize> {
318 use crate::search::registry::{SearchParameterSource, SearchParameterStatus};
319
320 let conn = self.get_connection()?;
321 let mut stmt = conn
322 .prepare(
323 "SELECT data FROM resources WHERE resource_type = 'SearchParameter' AND is_deleted = 0",
324 )
325 .map_err(|e| {
326 crate::error::StorageError::Backend(BackendError::Internal {
327 backend_name: "sqlite".to_string(),
328 message: format!("Failed to prepare SearchParameter query: {}", e),
329 source: None,
330 })
331 })?;
332
333 let loader = SearchParameterLoader::new(self.config.fhir_version);
334 let mut registry = self.search_registry.write();
335 let mut count = 0;
336
337 let rows = stmt
338 .query_map([], |row| row.get::<_, Vec<u8>>(0))
339 .map_err(|e| {
340 crate::error::StorageError::Backend(BackendError::Internal {
341 backend_name: "sqlite".to_string(),
342 message: format!("Failed to query SearchParameters: {}", e),
343 source: None,
344 })
345 })?;
346
347 for row in rows {
348 let data = match row {
349 Ok(data) => data,
350 Err(e) => {
351 tracing::warn!("Failed to read SearchParameter row: {}", e);
352 continue;
353 }
354 };
355
356 let json: serde_json::Value = match serde_json::from_slice(&data) {
357 Ok(json) => json,
358 Err(e) => {
359 tracing::warn!("Failed to parse SearchParameter JSON: {}", e);
360 continue;
361 }
362 };
363
364 match loader.parse_resource(&json) {
365 Ok(mut def) => {
366 if def.status == SearchParameterStatus::Active {
368 def.source = SearchParameterSource::Stored;
369 if registry.register(def).is_ok() {
370 count += 1;
371 }
372 }
373 }
374 Err(e) => {
375 tracing::warn!("Failed to parse stored SearchParameter: {}", e);
376 }
377 }
378 }
379
380 Ok(count)
381 }
382
383 pub(crate) fn pool(&self) -> Pool<SqliteConnectionManager> {
385 self.pool.clone()
386 }
387
388 pub(crate) fn get_connection(
390 &self,
391 ) -> StorageResult<PooledConnection<SqliteConnectionManager>> {
392 self.pool.get().map_err(|e| {
393 crate::error::StorageError::Backend(BackendError::ConnectionFailed {
394 backend_name: "sqlite".to_string(),
395 message: e.to_string(),
396 })
397 })
398 }
399
400 pub(crate) fn get_search_registry(&self) -> Arc<RwLock<SearchParameterRegistry>> {
402 Arc::clone(&self.search_registry)
403 }
404
405 fn configure_connection(&self) -> StorageResult<()> {
407 let conn = self.get_connection()?;
408
409 conn.busy_timeout(std::time::Duration::from_millis(
410 self.config.busy_timeout_ms as u64,
411 ))
412 .map_err(|e| {
413 crate::error::StorageError::Backend(BackendError::Internal {
414 backend_name: "sqlite".to_string(),
415 message: format!("Failed to set busy timeout: {}", e),
416 source: None,
417 })
418 })?;
419
420 if self.config.enable_foreign_keys {
421 conn.execute("PRAGMA foreign_keys = ON", []).map_err(|e| {
422 crate::error::StorageError::Backend(BackendError::Internal {
423 backend_name: "sqlite".to_string(),
424 message: format!("Failed to enable foreign keys: {}", e),
425 source: None,
426 })
427 })?;
428 }
429
430 if self.config.enable_wal && !self.is_memory {
431 conn.query_row("PRAGMA journal_mode = WAL", [], |_row| Ok(()))
433 .map_err(|e| {
434 crate::error::StorageError::Backend(BackendError::Internal {
435 backend_name: "sqlite".to_string(),
436 message: format!("Failed to enable WAL mode: {}", e),
437 source: None,
438 })
439 })?;
440 }
441
442 Ok(())
443 }
444
445 pub fn is_memory(&self) -> bool {
447 self.is_memory
448 }
449
450 pub fn config(&self) -> &SqliteBackendConfig {
452 &self.config
453 }
454
455 pub fn search_registry(&self) -> &Arc<RwLock<SearchParameterRegistry>> {
457 &self.search_registry
458 }
459
460 pub fn search_extractor(&self) -> &Arc<SearchParameterExtractor> {
462 &self.search_extractor
463 }
464
465 pub fn is_search_offloaded(&self) -> bool {
467 self.config.search_offloaded
468 }
469
470 pub fn set_search_offloaded(&mut self, offloaded: bool) {
476 self.config.search_offloaded = offloaded;
477 }
478}
479
480#[allow(dead_code)]
482pub struct SqliteConnection(pub(crate) PooledConnection<SqliteConnectionManager>);
483
484impl Debug for SqliteConnection {
485 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
486 f.debug_struct("SqliteConnection").finish()
487 }
488}
489
490#[async_trait]
491impl Backend for SqliteBackend {
492 type Connection = SqliteConnection;
493
494 fn kind(&self) -> BackendKind {
495 BackendKind::Sqlite
496 }
497
498 fn name(&self) -> &'static str {
499 "sqlite"
500 }
501
502 fn supports(&self, capability: BackendCapability) -> bool {
503 matches!(
504 capability,
505 BackendCapability::Crud
506 | BackendCapability::Versioning
507 | BackendCapability::InstanceHistory
508 | BackendCapability::TypeHistory
509 | BackendCapability::SystemHistory
510 | BackendCapability::BasicSearch
511 | BackendCapability::DateSearch
512 | BackendCapability::ReferenceSearch
513 | BackendCapability::Sorting
514 | BackendCapability::OffsetPagination
515 | BackendCapability::Transactions
516 | BackendCapability::OptimisticLocking
517 | BackendCapability::BulkExport
518 | BackendCapability::BulkSubmitIngest
519 | BackendCapability::BulkSubmitRestWorker
520 | BackendCapability::Include
521 | BackendCapability::Revinclude
522 | BackendCapability::SharedSchema
523 )
524 }
525
526 fn capabilities(&self) -> Vec<BackendCapability> {
527 vec![
528 BackendCapability::Crud,
529 BackendCapability::Versioning,
530 BackendCapability::InstanceHistory,
531 BackendCapability::TypeHistory,
532 BackendCapability::SystemHistory,
533 BackendCapability::BasicSearch,
534 BackendCapability::DateSearch,
535 BackendCapability::ReferenceSearch,
536 BackendCapability::Sorting,
537 BackendCapability::OffsetPagination,
538 BackendCapability::Transactions,
539 BackendCapability::OptimisticLocking,
540 BackendCapability::BulkExport,
541 BackendCapability::BulkSubmitIngest,
542 BackendCapability::BulkSubmitRestWorker,
543 BackendCapability::Include,
544 BackendCapability::Revinclude,
545 BackendCapability::SharedSchema,
546 ]
547 }
548
549 async fn acquire(&self) -> Result<Self::Connection, BackendError> {
550 let conn = self
551 .pool
552 .get()
553 .map_err(|e| BackendError::ConnectionFailed {
554 backend_name: "sqlite".to_string(),
555 message: e.to_string(),
556 })?;
557 Ok(SqliteConnection(conn))
558 }
559
560 async fn release(&self, _conn: Self::Connection) {
561 }
563
564 async fn health_check(&self) -> Result<(), BackendError> {
565 let conn = self
566 .get_connection()
567 .map_err(|_| BackendError::Unavailable {
568 backend_name: "sqlite".to_string(),
569 message: "Failed to get connection".to_string(),
570 })?;
571 conn.query_row("SELECT 1", [], |_| Ok(()))
572 .map_err(|e| BackendError::Internal {
573 backend_name: "sqlite".to_string(),
574 message: format!("Health check failed: {}", e),
575 source: None,
576 })?;
577 Ok(())
578 }
579
580 async fn initialize(&self) -> Result<(), BackendError> {
581 self.init_schema().map_err(|e| BackendError::Internal {
582 backend_name: "sqlite".to_string(),
583 message: format!("Failed to initialize schema: {}", e),
584 source: None,
585 })
586 }
587
588 async fn migrate(&self) -> Result<(), BackendError> {
589 self.init_schema().map_err(|e| BackendError::Internal {
591 backend_name: "sqlite".to_string(),
592 message: format!("Failed to run migrations: {}", e),
593 source: None,
594 })
595 }
596}
597
598use crate::core::capabilities::{
603 GlobalSearchCapabilities, ResourceSearchCapabilities, SearchCapabilityProvider,
604};
605use crate::types::{
606 IncludeCapability, PaginationCapability, ResultModeCapability, SearchParamFullCapability,
607 SearchParamType, SpecialSearchParam,
608};
609
610impl SearchCapabilityProvider for SqliteBackend {
611 fn resource_search_capabilities(
612 &self,
613 resource_type: &str,
614 ) -> Option<ResourceSearchCapabilities> {
615 let params = {
617 let registry = self.search_registry.read();
618 registry.get_active_params(resource_type)
619 };
620
621 if params.is_empty() {
622 let common_params = {
624 let registry = self.search_registry.read();
625 registry.get_active_params("Resource")
626 };
627 if common_params.is_empty() {
628 return None;
629 }
630 }
631
632 let mut search_params = Vec::new();
634 for param in ¶ms {
635 let mut cap = SearchParamFullCapability::new(¶m.code, param.param_type)
636 .with_definition(¶m.url);
637
638 let modifiers = Self::modifiers_for_type(param.param_type);
640 cap = cap.with_modifiers(modifiers);
641
642 if let Some(ref targets) = param.target {
644 cap = cap.with_targets(targets.iter().map(|s| s.as_str()));
645 }
646
647 search_params.push(cap);
648 }
649
650 let common_params = {
652 let registry = self.search_registry.read();
653 registry.get_active_params("Resource")
654 };
655 for param in &common_params {
656 if !search_params.iter().any(|p| p.name == param.code) {
657 let mut cap = SearchParamFullCapability::new(¶m.code, param.param_type)
658 .with_definition(¶m.url);
659 cap = cap.with_modifiers(Self::modifiers_for_type(param.param_type));
660 search_params.push(cap);
661 }
662 }
663
664 Some(
665 ResourceSearchCapabilities::new(resource_type)
666 .with_special_params(vec![
667 SpecialSearchParam::Id,
668 SpecialSearchParam::LastUpdated,
669 SpecialSearchParam::Tag,
670 SpecialSearchParam::Profile,
671 SpecialSearchParam::Security,
672 ])
673 .with_include_capabilities(vec![
674 IncludeCapability::Include,
675 IncludeCapability::Revinclude,
676 ])
677 .with_pagination_capabilities(vec![
678 PaginationCapability::Count,
679 PaginationCapability::Offset,
680 PaginationCapability::Cursor,
681 PaginationCapability::MaxPageSize(1000),
682 PaginationCapability::DefaultPageSize(20),
683 ])
684 .with_result_mode_capabilities(vec![
685 ResultModeCapability::Total,
686 ResultModeCapability::TotalNone,
687 ResultModeCapability::TotalAccurate,
688 ResultModeCapability::SummaryCount,
689 ])
690 .with_param_list(search_params),
691 )
692 }
693
694 fn global_search_capabilities(&self) -> GlobalSearchCapabilities {
695 GlobalSearchCapabilities::new()
696 .with_special_params(vec![
697 SpecialSearchParam::Id,
698 SpecialSearchParam::LastUpdated,
699 SpecialSearchParam::Tag,
700 SpecialSearchParam::Profile,
701 SpecialSearchParam::Security,
702 ])
703 .with_pagination(vec![
704 PaginationCapability::Count,
705 PaginationCapability::Offset,
706 PaginationCapability::Cursor,
707 PaginationCapability::MaxPageSize(1000),
708 PaginationCapability::DefaultPageSize(20),
709 ])
710 .with_system_search()
711 }
712}
713
714impl SqliteBackend {
715 pub(super) fn modifiers_for_type(param_type: SearchParamType) -> Vec<&'static str> {
717 match param_type {
718 SearchParamType::String => vec!["exact", "contains", "text", "missing"],
719 SearchParamType::Token => vec![
724 "not",
725 "text",
726 "in",
727 "of-type",
728 "code-text",
729 "text-advanced",
730 "missing",
731 ],
732 SearchParamType::Reference => vec![
733 "identifier",
734 "contains",
735 "text",
736 "code-text",
737 "below",
738 "above",
739 "missing",
740 ],
741 SearchParamType::Date => vec!["missing"],
742 SearchParamType::Number => vec!["missing"],
743 SearchParamType::Quantity => vec!["missing"],
744 SearchParamType::Uri => vec!["contains", "below", "above", "missing"],
745 SearchParamType::Composite => vec!["missing"],
746 SearchParamType::Special => vec![],
747 }
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754
755 #[test]
756 fn test_in_memory_backend() {
757 let backend = SqliteBackend::in_memory().unwrap();
758 assert!(backend.is_memory());
759 assert_eq!(backend.name(), "sqlite");
760 assert_eq!(backend.kind(), BackendKind::Sqlite);
761 }
762
763 #[test]
764 fn test_backend_initialization() {
765 let backend = SqliteBackend::in_memory().unwrap();
766 backend.init_schema().unwrap();
767 backend.init_schema().unwrap(); }
769
770 #[test]
771 fn test_backend_capabilities() {
772 let backend = SqliteBackend::in_memory().unwrap();
773
774 assert!(backend.supports(BackendCapability::Crud));
775 assert!(backend.supports(BackendCapability::BasicSearch));
776 assert!(backend.supports(BackendCapability::Transactions));
777 assert!(backend.supports(BackendCapability::BulkExport));
778 assert!(backend.supports(BackendCapability::BulkSubmitIngest));
779 assert!(backend.supports(BackendCapability::BulkSubmitRestWorker));
780 assert!(!backend.supports(BackendCapability::FullTextSearch));
781 }
782
783 #[tokio::test]
784 async fn test_health_check() {
785 let backend = SqliteBackend::in_memory().unwrap();
786 backend.init_schema().unwrap();
787 assert!(backend.health_check().await.is_ok());
788 }
789
790 #[tokio::test]
791 async fn test_acquire_release() {
792 let backend = SqliteBackend::in_memory().unwrap();
793 let conn = backend.acquire().await.unwrap();
794 backend.release(conn).await;
795 }
796
797 #[test]
798 fn test_search_capability_provider_patient() {
799 let backend = SqliteBackend::in_memory().unwrap();
800
801 let caps = backend.resource_search_capabilities("Patient");
803 assert!(caps.is_some(), "Should have capabilities for Patient");
804
805 let caps = caps.unwrap();
806 assert_eq!(caps.resource_type, "Patient");
807
808 assert!(caps.supports_special(SpecialSearchParam::Id));
810 assert!(caps.supports_special(SpecialSearchParam::LastUpdated));
811
812 assert!(caps.supports_include(IncludeCapability::Include));
814 assert!(caps.supports_include(IncludeCapability::Revinclude));
815
816 assert!(
819 !caps.search_params.is_empty(),
820 "Should have search parameters"
821 );
822 }
823
824 #[test]
825 fn test_global_search_capabilities() {
826 let backend = SqliteBackend::in_memory().unwrap();
827
828 let global = backend.global_search_capabilities();
829
830 assert!(
832 global
833 .common_special_params
834 .contains(&SpecialSearchParam::Id)
835 );
836 assert!(
837 global
838 .common_special_params
839 .contains(&SpecialSearchParam::LastUpdated)
840 );
841
842 assert!(global.supports_system_search);
844
845 assert!(!global.common_pagination_capabilities.is_empty());
847 }
848
849 #[test]
850 fn test_modifiers_for_type() {
851 let string_mods = SqliteBackend::modifiers_for_type(SearchParamType::String);
853 assert!(string_mods.contains(&"exact"));
854 assert!(string_mods.contains(&"contains"));
855 assert!(string_mods.contains(&"text"));
856 assert!(string_mods.contains(&"missing"));
857
858 let token_mods = SqliteBackend::modifiers_for_type(SearchParamType::Token);
860 assert!(token_mods.contains(&"not"));
861 assert!(token_mods.contains(&"text"));
862 assert!(token_mods.contains(&"of-type"));
863 assert!(!token_mods.contains(&"code"));
865 assert!(token_mods.contains(&"text-advanced"));
866 assert!(!token_mods.contains(&"not-in"));
868
869 let ref_mods = SqliteBackend::modifiers_for_type(SearchParamType::Reference);
871 assert!(ref_mods.contains(&"identifier"));
872
873 let uri_mods = SqliteBackend::modifiers_for_type(SearchParamType::Uri);
875 assert!(uri_mods.contains(&"below"));
876 assert!(uri_mods.contains(&"above"));
877 }
878}