1use std::collections::HashSet;
11
12use async_trait::async_trait;
13use chrono::Utc;
14use helios_fhir::FhirVersion;
15use rusqlite::params;
16
17use crate::core::{
18 ChainedSearchProvider, IncludeProvider, MultiTypeSearchProvider, ResourceStorage,
19 RevincludeProvider, SearchProvider, SearchResult,
20};
21use crate::error::{BackendError, StorageError, StorageResult};
22use crate::tenant::TenantContext;
23use crate::types::{
24 CursorDirection, CursorValue, IncludeDirective, Page, PageCursor, PageInfo,
25 ReverseChainedParameter, SearchQuery, SearchValue, StoredResource,
26};
27
28use super::SqliteBackend;
29use super::search::{QueryBuilder, SortValueKind, SqlParam};
30
31fn internal_error(message: String) -> StorageError {
32 StorageError::Backend(BackendError::Internal {
33 backend_name: "sqlite".to_string(),
34 message,
35 source: None,
36 })
37}
38
39fn bind_cursor_value(
42 params: &mut Vec<Box<dyn rusqlite::ToSql>>,
43 kind: SortValueKind,
44 cursor: &PageCursor,
45) -> StorageResult<()> {
46 let value = cursor.sort_values().first();
47 match kind {
48 SortValueKind::Number => {
49 let n = match value {
50 Some(CursorValue::Decimal(f)) => *f,
51 Some(CursorValue::Number(i)) => *i as f64,
52 Some(CursorValue::String(s)) => s.parse().unwrap_or(0.0),
53 _ => {
54 return Err(internal_error(
55 "Invalid cursor: expected number".to_string(),
56 ));
57 }
58 };
59 params.push(Box::new(n));
60 }
61 SortValueKind::Timestamp | SortValueKind::Text => match value {
62 Some(CursorValue::String(s)) => params.push(Box::new(s.clone())),
63 Some(CursorValue::Null) | None => params.push(Box::new(Option::<String>::None)),
64 _ => {
65 return Err(internal_error("Invalid cursor: expected text".to_string()));
66 }
67 },
68 }
69 Ok(())
70}
71
72#[async_trait]
73impl SearchProvider for SqliteBackend {
74 async fn search(
75 &self,
76 tenant: &TenantContext,
77 query: &SearchQuery,
78 ) -> StorageResult<SearchResult> {
79 if query.contained != crate::types::ContainedMode::Off {
82 return self.search_contained(tenant, query).await;
83 }
84
85 let total = if query.wants_total() {
89 Some(self.search_count(tenant, query).await?)
90 } else {
91 None
92 };
93
94 let conn = self.get_connection()?;
95 let tenant_id = tenant.tenant_id().as_str();
96 let resource_type = &query.resource_type;
97
98 let count = query.count.unwrap_or(100) as usize;
100
101 let keyset = QueryBuilder::new(tenant_id, resource_type).primary_keyset_key(query);
105
106 let cursor = if keyset.is_some() {
108 query
109 .cursor
110 .as_ref()
111 .and_then(|c| PageCursor::decode(c).ok())
112 } else {
113 None
114 };
115
116 let param_offset = if cursor.is_some() { 4 } else { 2 };
119
120 let search_filter = if !query.parameters.is_empty() || query.compartment.is_some() {
121 let builder =
122 QueryBuilder::new(tenant_id, resource_type).with_param_offset(param_offset);
123 let fragment = builder.build(query);
124 if !fragment.sql.is_empty() {
125 Some(fragment)
126 } else {
127 None
128 }
129 } else {
130 None
131 };
132 let filter_clause = search_filter
133 .as_ref()
134 .map(|f| format!(" AND id IN ({})", f.sql))
135 .unwrap_or_default();
136 let search_params = search_filter.map(|f| f.params).unwrap_or_default();
137
138 let select_cols = match &keyset {
140 Some(k) => format!(
141 "id, version_id, data, last_updated, fhir_version, {} AS sort_key",
142 k.expr
143 ),
144 None => "id, version_id, data, last_updated, fhir_version".to_string(),
145 };
146
147 let order_by = if query.sort.is_empty() {
149 "ORDER BY last_updated DESC, id ASC".to_string()
150 } else {
151 QueryBuilder::new(tenant_id, resource_type).build_order_by(query)
152 };
153
154 let (sql, has_previous) = if let (Some(cursor), Some(k)) = (&cursor, &keyset) {
156 let e = &k.expr;
157 let asc = k.direction == crate::types::SortDirection::Ascending;
158 match cursor.direction() {
159 CursorDirection::Next => {
160 let e_op = if asc { ">" } else { "<" };
161 let sql = format!(
162 "SELECT {cols} FROM resources \
163 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0{filter} \
164 AND ({e} {e_op} ?3 OR ({e} = ?3 AND id > ?4)) \
165 ORDER BY {e} {dir}, id ASC LIMIT {lim}",
166 cols = select_cols,
167 filter = filter_clause,
168 e = e,
169 e_op = e_op,
170 dir = if asc { "ASC" } else { "DESC" },
171 lim = count + 1,
172 );
173 (sql, true)
174 }
175 CursorDirection::Previous => {
176 let e_op = if asc { "<" } else { ">" };
177 let sql = format!(
178 "SELECT {cols} FROM resources \
179 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0{filter} \
180 AND ({e} {e_op} ?3 OR ({e} = ?3 AND id < ?4)) \
181 ORDER BY {e} {dir}, id DESC LIMIT {lim}",
182 cols = select_cols,
183 filter = filter_clause,
184 e = e,
185 e_op = e_op,
186 dir = if asc { "DESC" } else { "ASC" },
187 lim = count + 1,
188 );
189 (sql, false)
190 }
191 }
192 } else if let Some(offset) = query.offset {
193 let sql = format!(
194 "SELECT {cols} FROM resources \
195 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0{filter} \
196 {order} LIMIT {lim} OFFSET {off}",
197 cols = select_cols,
198 filter = filter_clause,
199 order = order_by,
200 lim = count + 1,
201 off = offset,
202 );
203 (sql, offset > 0)
204 } else {
205 let sql = format!(
206 "SELECT {cols} FROM resources \
207 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0{filter} \
208 {order} LIMIT {lim}",
209 cols = select_cols,
210 filter = filter_clause,
211 order = order_by,
212 lim = count + 1,
213 );
214 (sql, false)
215 };
216
217 let mut stmt = conn
218 .prepare(&sql)
219 .map_err(|e| internal_error(format!("Failed to prepare search query: {}", e)))?;
220
221 let mut all_params: Vec<Box<dyn rusqlite::ToSql>> = vec![
223 Box::new(tenant_id.to_string()),
224 Box::new(resource_type.to_string()),
225 ];
226 if let (Some(cursor), Some(k)) = (&cursor, &keyset) {
227 bind_cursor_value(&mut all_params, k.kind, cursor)?;
228 all_params.push(Box::new(cursor.resource_id().to_string()));
229 }
230 for param in &search_params {
231 match param {
232 SqlParam::String(s) => all_params.push(Box::new(s.clone())),
233 SqlParam::Integer(i) => all_params.push(Box::new(*i)),
234 SqlParam::Float(f) => all_params.push(Box::new(*f)),
235 SqlParam::Null => all_params.push(Box::new(Option::<String>::None)),
236 }
237 }
238 let param_refs: Vec<&dyn rusqlite::ToSql> = all_params.iter().map(|p| p.as_ref()).collect();
239
240 let sort_kind = keyset.as_ref().map(|k| k.kind);
242 let raw_rows: Vec<(String, String, Vec<u8>, String, String, Option<CursorValue>)> = stmt
243 .query_map(param_refs.as_slice(), |row| {
244 let id: String = row.get(0)?;
245 let version_id: String = row.get(1)?;
246 let data: Vec<u8> = row.get(2)?;
247 let last_updated: String = row.get(3)?;
248 let fhir_version: String = row.get(4)?;
249 let sort_key = match sort_kind {
250 Some(SortValueKind::Number) => {
251 row.get::<_, Option<f64>>(5)?.map(CursorValue::Decimal)
252 }
253 Some(_) => row.get::<_, Option<String>>(5)?.map(CursorValue::String),
255 None => None,
256 };
257 Ok((id, version_id, data, last_updated, fhir_version, sort_key))
258 })
259 .map_err(|e| internal_error(format!("Failed to execute search: {}", e)))?
260 .collect::<Result<Vec<_>, _>>()
261 .map_err(|e| internal_error(format!("Failed to read row: {}", e)))?;
262
263 let mut parsed: Vec<(StoredResource, Option<CursorValue>)> = Vec::new();
265 for (id, version_id, data, last_updated_str, fhir_version_str, sort_key) in raw_rows {
266 let json_data: serde_json::Value = serde_json::from_slice(&data)
267 .map_err(|e| internal_error(format!("Failed to deserialize resource: {}", e)))?;
268
269 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
270 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
271 .with_timezone(&Utc);
272
273 let fhir_version = FhirVersion::from_storage(&fhir_version_str)
274 .unwrap_or_else(helios_fhir::FhirVersion::default_enabled);
275
276 let resource = StoredResource::from_storage(
277 resource_type.clone(),
278 id,
279 version_id,
280 tenant.tenant_id().clone(),
281 json_data,
282 last_updated,
283 last_updated,
284 None,
285 fhir_version,
286 );
287
288 parsed.push((resource, sort_key));
289 }
290
291 if cursor
293 .as_ref()
294 .map(|c| c.direction() == CursorDirection::Previous)
295 .unwrap_or(false)
296 {
297 parsed.reverse();
298 }
299
300 let has_next = parsed.len() > count;
302 if has_next {
303 parsed.pop();
304 }
305
306 let next_cursor = if has_next {
307 parsed.last().map(|(r, sk)| {
308 PageCursor::new(vec![sk.clone().unwrap_or(CursorValue::Null)], r.id()).encode()
309 })
310 } else {
311 None
312 };
313 let previous_cursor = if has_previous {
314 parsed.first().map(|(r, sk)| {
315 PageCursor::previous(vec![sk.clone().unwrap_or(CursorValue::Null)], r.id()).encode()
316 })
317 } else {
318 None
319 };
320
321 let resources: Vec<StoredResource> = parsed.into_iter().map(|(r, _)| r).collect();
322 let page_info = PageInfo {
323 next_cursor,
324 previous_cursor,
325 total,
326 has_next,
327 has_previous,
328 };
329
330 let page = Page::new(resources, page_info);
331
332 Ok(SearchResult {
333 resources: page,
334 included: Vec::new(),
335 total,
336 scores: Default::default(),
337 })
338 }
339
340 async fn search_count(
341 &self,
342 tenant: &TenantContext,
343 query: &SearchQuery,
344 ) -> StorageResult<u64> {
345 let conn = self.get_connection()?;
346 let tenant_id = tenant.tenant_id().as_str();
347 let resource_type = &query.resource_type;
348
349 let (sql, all_params): (String, Vec<Box<dyn rusqlite::ToSql>>) = if !query
351 .parameters
352 .is_empty()
353 || query.compartment.is_some()
354 {
355 let builder = QueryBuilder::new(tenant_id, resource_type).with_param_offset(2);
356 let fragment = builder.build(query);
357
358 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![
359 Box::new(tenant_id.to_string()),
360 Box::new(resource_type.to_string()),
361 ];
362
363 for param in &fragment.params {
365 match param {
366 SqlParam::String(s) => params.push(Box::new(s.clone())),
367 SqlParam::Integer(i) => params.push(Box::new(*i)),
368 SqlParam::Float(f) => params.push(Box::new(*f)),
369 SqlParam::Null => params.push(Box::new(Option::<String>::None)),
370 }
371 }
372
373 let sql = format!(
374 "SELECT COUNT(*) FROM resources WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0 AND id IN ({})",
375 fragment.sql
376 );
377
378 (sql, params)
379 } else {
380 let sql = "SELECT COUNT(*) FROM resources WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0".to_string();
381 let params: Vec<Box<dyn rusqlite::ToSql>> = vec![
382 Box::new(tenant_id.to_string()),
383 Box::new(resource_type.to_string()),
384 ];
385 (sql, params)
386 };
387
388 let param_refs: Vec<&dyn rusqlite::ToSql> = all_params.iter().map(|p| p.as_ref()).collect();
389
390 let count: i64 = conn
391 .query_row(&sql, param_refs.as_slice(), |row| row.get(0))
392 .map_err(|e| internal_error(format!("Failed to count resources: {}", e)))?;
393
394 Ok(count as u64)
395 }
396
397 fn search_param_registry(
398 &self,
399 ) -> &std::sync::Arc<parking_lot::RwLock<crate::search::SearchParameterRegistry>> {
400 self.search_registry()
401 }
402
403 fn supports_contained_search(&self) -> bool {
404 true
405 }
406
407 fn modifiers_for_param_type(
408 &self,
409 param_type: crate::types::SearchParamType,
410 ) -> Vec<&'static str> {
411 Self::modifiers_for_type(param_type)
412 }
413}
414
415#[async_trait]
416impl MultiTypeSearchProvider for SqliteBackend {
417 async fn search_multi(
418 &self,
419 tenant: &TenantContext,
420 resource_types: &[&str],
421 query: &SearchQuery,
422 ) -> StorageResult<SearchResult> {
423 let conn = self.get_connection()?;
424 let tenant_id = tenant.tenant_id().as_str();
425
426 let count = query.count.unwrap_or(100) as usize;
428 let offset = query.offset.unwrap_or(0) as usize;
429
430 let type_filter = if resource_types.is_empty() {
432 String::new()
434 } else {
435 let types: Vec<String> = resource_types
437 .iter()
438 .map(|t| format!("'{}'", t.replace('\'', "''")))
439 .collect();
440 format!(" AND resource_type IN ({})", types.join(", "))
441 };
442
443 let sql = format!(
444 "SELECT resource_type, id, version_id, data, last_updated, fhir_version FROM resources
445 WHERE tenant_id = ?1 AND is_deleted = 0{}
446 ORDER BY last_updated DESC
447 LIMIT {} OFFSET {}",
448 type_filter,
449 count + 1,
450 offset
451 );
452
453 let mut stmt = conn
454 .prepare(&sql)
455 .map_err(|e| internal_error(format!("Failed to prepare multi-type search: {}", e)))?;
456
457 let rows = stmt
458 .query_map(params![tenant_id], |row| {
459 let resource_type: String = row.get(0)?;
460 let id: String = row.get(1)?;
461 let version_id: String = row.get(2)?;
462 let data: Vec<u8> = row.get(3)?;
463 let last_updated: String = row.get(4)?;
464 let fhir_version: String = row.get(5)?;
465 Ok((
466 resource_type,
467 id,
468 version_id,
469 data,
470 last_updated,
471 fhir_version,
472 ))
473 })
474 .map_err(|e| internal_error(format!("Failed to execute multi-type search: {}", e)))?;
475
476 let mut resources = Vec::new();
477 for row in rows {
478 let (resource_type, id, version_id, data, last_updated_str, fhir_version_str) =
479 row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?;
480
481 let json_data: serde_json::Value = serde_json::from_slice(&data)
482 .map_err(|e| internal_error(format!("Failed to deserialize resource: {}", e)))?;
483
484 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
485 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
486 .with_timezone(&Utc);
487
488 let fhir_version = FhirVersion::from_storage(&fhir_version_str)
489 .unwrap_or_else(helios_fhir::FhirVersion::default_enabled);
490
491 let resource = StoredResource::from_storage(
492 resource_type,
493 id,
494 version_id,
495 tenant.tenant_id().clone(),
496 json_data,
497 last_updated,
498 last_updated,
499 None,
500 fhir_version,
501 );
502
503 resources.push(resource);
504 }
505
506 let has_next = resources.len() > count;
508 if has_next {
509 resources.pop();
510 }
511
512 let page_info = PageInfo {
513 next_cursor: None,
514 previous_cursor: None,
515 total: None,
516 has_next,
517 has_previous: offset > 0,
518 };
519
520 Ok(SearchResult {
521 resources: Page::new(resources, page_info),
522 included: Vec::new(),
523 total: None,
524 scores: Default::default(),
525 })
526 }
527}
528
529#[async_trait]
530impl IncludeProvider for SqliteBackend {
531 async fn resolve_includes(
532 &self,
533 tenant: &TenantContext,
534 resources: &[StoredResource],
535 includes: &[IncludeDirective],
536 ) -> StorageResult<Vec<StoredResource>> {
537 if resources.is_empty() || includes.is_empty() {
538 return Ok(Vec::new());
539 }
540
541 let conn = self.get_connection()?;
542 let tenant_id = tenant.tenant_id().as_str();
543
544 let mut included = Vec::new();
545 let mut seen_refs: HashSet<String> = HashSet::new();
546
547 for include in includes {
548 for resource in resources {
550 if resource.resource_type() != include.source_type {
552 continue;
553 }
554
555 let refs = self.extract_references(resource.content(), &include.search_param);
557
558 for reference in refs {
559 if let Some((ref_type, ref_id)) = self.parse_reference(&reference) {
561 if let Some(ref target) = include.target_type {
563 if ref_type != *target {
564 continue;
565 }
566 }
567
568 let ref_key = format!("{}/{}", ref_type, ref_id);
570 if seen_refs.contains(&ref_key) {
571 continue;
572 }
573 seen_refs.insert(ref_key);
574
575 if let Some(included_resource) =
577 self.fetch_resource(&conn, tenant_id, &ref_type, &ref_id)?
578 {
579 included.push(included_resource);
580 }
581 }
582 }
583 }
584 }
585
586 Ok(included)
587 }
588}
589
590#[async_trait]
591impl RevincludeProvider for SqliteBackend {
592 async fn resolve_revincludes(
593 &self,
594 tenant: &TenantContext,
595 resources: &[StoredResource],
596 revincludes: &[IncludeDirective],
597 ) -> StorageResult<Vec<StoredResource>> {
598 if resources.is_empty() || revincludes.is_empty() {
599 return Ok(Vec::new());
600 }
601
602 let conn = self.get_connection()?;
603 let tenant_id = tenant.tenant_id().as_str();
604
605 let mut included = Vec::new();
606 let mut seen_ids: HashSet<String> = HashSet::new();
607
608 for revinclude in revincludes {
609 let mut reference_values: Vec<String> = Vec::new();
611 for resource in resources {
612 reference_values.push(format!("{}/{}", resource.resource_type(), resource.id()));
615 reference_values.push(resource.id().to_string());
617 }
618
619 if reference_values.is_empty() {
620 continue;
621 }
622
623 let reference_pattern = reference_values
625 .iter()
626 .map(|r| format!("%{}%", r.replace('%', "\\%").replace('_', "\\_")))
627 .collect::<Vec<_>>();
628
629 let sql = format!(
632 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
633 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
634 AND ({})",
635 reference_pattern
636 .iter()
637 .map(|_| "data LIKE ?".to_string())
638 .collect::<Vec<_>>()
639 .join(" OR ")
640 );
641
642 let mut stmt = conn.prepare(&sql).map_err(|e| {
643 internal_error(format!("Failed to prepare revinclude query: {}", e))
644 })?;
645
646 let mut param_values: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
648 param_values.push(Box::new(tenant_id.to_string()));
649 param_values.push(Box::new(revinclude.source_type.clone()));
650 for pattern in &reference_pattern {
651 param_values.push(Box::new(pattern.clone()));
652 }
653
654 let param_refs: Vec<&dyn rusqlite::ToSql> =
655 param_values.iter().map(|p| p.as_ref()).collect();
656
657 let rows = stmt
658 .query_map(param_refs.as_slice(), |row| {
659 let id: String = row.get(0)?;
660 let version_id: String = row.get(1)?;
661 let data: Vec<u8> = row.get(2)?;
662 let last_updated: String = row.get(3)?;
663 let fhir_version: String = row.get(4)?;
664 Ok((id, version_id, data, last_updated, fhir_version))
665 })
666 .map_err(|e| {
667 internal_error(format!("Failed to execute revinclude query: {}", e))
668 })?;
669
670 for row in rows {
671 let (id, version_id, data, last_updated_str, fhir_version_str) =
672 row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?;
673
674 let resource_key = format!("{}/{}", revinclude.source_type, id);
676 if seen_ids.contains(&resource_key) {
677 continue;
678 }
679
680 let json_data: serde_json::Value = serde_json::from_slice(&data)
681 .map_err(|e| internal_error(format!("Failed to deserialize: {}", e)))?;
682
683 if !self.verify_reference(&json_data, &revinclude.search_param, &reference_values) {
685 continue;
686 }
687
688 seen_ids.insert(resource_key);
689
690 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
691 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
692 .with_timezone(&Utc);
693
694 let fhir_version = FhirVersion::from_storage(&fhir_version_str)
695 .unwrap_or_else(helios_fhir::FhirVersion::default_enabled);
696
697 let resource = StoredResource::from_storage(
698 &revinclude.source_type,
699 id,
700 version_id,
701 tenant.tenant_id().clone(),
702 json_data,
703 last_updated,
704 last_updated,
705 None,
706 fhir_version,
707 );
708
709 included.push(resource);
710 }
711 }
712
713 Ok(included)
714 }
715}
716
717#[async_trait]
718impl ChainedSearchProvider for SqliteBackend {
719 async fn resolve_chain(
720 &self,
721 tenant: &TenantContext,
722 base_type: &str,
723 chain: &str,
724 value: &str,
725 ) -> StorageResult<Vec<String>> {
726 use super::search::ChainQueryBuilder;
727
728 let conn = self.get_connection()?;
729 let tenant_id = tenant.tenant_id().as_str();
730
731 if chain.is_empty() {
732 return Ok(Vec::new());
733 }
734
735 let builder = ChainQueryBuilder::new(tenant_id, base_type, self.get_search_registry())
737 .with_param_offset(2); let parsed = match builder.parse_chain(chain) {
741 Ok(p) => p,
742 Err(e) => {
743 return Err(internal_error(format!("Failed to parse chain: {}", e)));
744 }
745 };
746
747 let search_value = SearchValue::eq(value);
749 let fragment = match builder.build_forward_chain_sql(&parsed, &search_value) {
750 Ok(f) => f,
751 Err(e) => {
752 return Err(internal_error(format!("Failed to build chain SQL: {}", e)));
753 }
754 };
755
756 let sql = format!(
760 "SELECT DISTINCT r.id FROM resources r \
761 WHERE r.tenant_id = ?1 AND r.resource_type = ?2 AND r.is_deleted = 0 AND {}",
762 fragment.sql
763 );
764
765 let mut stmt = conn
767 .prepare(&sql)
768 .map_err(|e| internal_error(format!("Failed to prepare chain query: {}", e)))?;
769
770 let mut bound_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
772 bound_params.push(Box::new(tenant_id.to_string()));
773 bound_params.push(Box::new(base_type.to_string()));
774 for param in &fragment.params {
775 match param {
776 SqlParam::String(s) => bound_params.push(Box::new(s.clone())),
777 SqlParam::Integer(i) => bound_params.push(Box::new(*i)),
778 SqlParam::Float(f) => bound_params.push(Box::new(*f)),
779 SqlParam::Null => bound_params.push(Box::new(rusqlite::types::Null)),
780 }
781 }
782
783 let params_ref: Vec<&dyn rusqlite::ToSql> =
784 bound_params.iter().map(|p| p.as_ref()).collect();
785
786 let rows = stmt
787 .query_map(params_ref.as_slice(), |row| row.get::<_, String>(0))
788 .map_err(|e| internal_error(format!("Failed to execute chain query: {}", e)))?;
789
790 let mut ids = Vec::new();
791 for row in rows {
792 ids.push(row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?);
793 }
794
795 Ok(ids)
796 }
797
798 async fn resolve_reverse_chain(
799 &self,
800 tenant: &TenantContext,
801 base_type: &str,
802 reverse_chain: &ReverseChainedParameter,
803 ) -> StorageResult<Vec<String>> {
804 use super::search::ChainQueryBuilder;
805
806 let conn = self.get_connection()?;
807 let tenant_id = tenant.tenant_id().as_str();
808
809 let builder = ChainQueryBuilder::new(tenant_id, base_type, self.get_search_registry())
811 .with_param_offset(2); let fragment = match builder.build_reverse_chain_sql(reverse_chain) {
815 Ok(f) => f,
816 Err(e) => {
817 return Err(internal_error(format!(
818 "Failed to build reverse chain SQL: {}",
819 e
820 )));
821 }
822 };
823
824 let sql = format!(
827 "SELECT DISTINCT r.id FROM resources r \
828 WHERE r.tenant_id = ?1 AND r.resource_type = ?2 AND r.is_deleted = 0 AND {}",
829 fragment.sql
830 );
831
832 let mut stmt = conn
833 .prepare(&sql)
834 .map_err(|e| internal_error(format!("Failed to prepare reverse chain query: {}", e)))?;
835
836 let mut bound_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
838 bound_params.push(Box::new(tenant_id.to_string()));
839 bound_params.push(Box::new(base_type.to_string()));
840 for param in &fragment.params {
841 match param {
842 SqlParam::String(s) => bound_params.push(Box::new(s.clone())),
843 SqlParam::Integer(i) => bound_params.push(Box::new(*i)),
844 SqlParam::Float(f) => bound_params.push(Box::new(*f)),
845 SqlParam::Null => bound_params.push(Box::new(rusqlite::types::Null)),
846 }
847 }
848
849 let params_ref: Vec<&dyn rusqlite::ToSql> =
850 bound_params.iter().map(|p| p.as_ref()).collect();
851
852 let rows = stmt
853 .query_map(params_ref.as_slice(), |row| row.get::<_, String>(0))
854 .map_err(|e| internal_error(format!("Failed to execute reverse chain query: {}", e)))?;
855
856 let mut ids = Vec::new();
857 for row in rows {
858 ids.push(row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?);
859 }
860
861 Ok(ids)
862 }
863}
864
865fn extract_contained_resource(
868 content: &serde_json::Value,
869 local_id: &str,
870) -> Option<serde_json::Value> {
871 content
872 .get("contained")?
873 .as_array()?
874 .iter()
875 .find(|e| e.get("id").and_then(|v| v.as_str()) == Some(local_id))
876 .cloned()
877}
878
879fn build_contained_stored(
882 container: &StoredResource,
883 contained_type: &str,
884 local_id: &str,
885 content: serde_json::Value,
886) -> StoredResource {
887 StoredResource::from_storage(
888 contained_type.to_string(),
889 local_id.to_string(),
890 container.version_id().to_string(),
891 container.tenant_id().clone(),
892 content,
893 container.created_at(),
894 container.last_modified(),
895 None,
896 container.fhir_version(),
897 )
898}
899
900impl SqliteBackend {
902 async fn search_contained(
913 &self,
914 tenant: &TenantContext,
915 query: &SearchQuery,
916 ) -> StorageResult<SearchResult> {
917 use crate::types::{ContainedMode, ContainedReturn};
918
919 let tenant_id = tenant.tenant_id().as_str();
920 let contained_type = query.resource_type.as_str();
921
922 let builder = QueryBuilder::new(tenant_id, contained_type);
924 let matches: Vec<(String, String, Option<String>)> = match builder.build_contained(query) {
925 Some(fragment) => {
926 let conn = self.get_connection()?;
927 let mut stmt = conn.prepare(&fragment.sql).map_err(|e| {
928 internal_error(format!("Failed to prepare contained query: {e}"))
929 })?;
930 let mut all_params: Vec<Box<dyn rusqlite::ToSql>> = vec![
931 Box::new(tenant_id.to_string()),
932 Box::new(contained_type.to_string()),
933 ];
934 for param in &fragment.params {
935 match param {
936 SqlParam::String(s) => all_params.push(Box::new(s.clone())),
937 SqlParam::Integer(i) => all_params.push(Box::new(*i)),
938 SqlParam::Float(f) => all_params.push(Box::new(*f)),
939 SqlParam::Null => all_params.push(Box::new(Option::<String>::None)),
940 }
941 }
942 let refs: Vec<&dyn rusqlite::ToSql> =
943 all_params.iter().map(|p| p.as_ref()).collect();
944 stmt.query_map(refs.as_slice(), |row| {
945 Ok((
946 row.get::<_, String>(0)?,
947 row.get::<_, String>(1)?,
948 row.get::<_, Option<String>>(2)?,
949 ))
950 })
951 .map_err(|e| internal_error(format!("Failed to execute contained query: {e}")))?
952 .collect::<Result<Vec<_>, _>>()
953 .map_err(|e| internal_error(format!("Failed to read contained row: {e}")))?
954 }
955 None => Vec::new(),
956 };
957
958 let mut items: Vec<StoredResource> = Vec::new();
960 let mut seen: HashSet<String> = HashSet::new();
961 match query.contained_return {
962 ContainedReturn::Container => {
963 for (ctype, cid, _) in &matches {
964 if !seen.insert(format!("{ctype}/{cid}")) {
965 continue;
966 }
967 if let Some(container) = self.read(tenant, ctype, cid).await? {
968 items.push(container);
969 }
970 }
971 }
972 ContainedReturn::Contained => {
973 for (ctype, cid, local) in &matches {
974 let Some(local_id) = local else { continue };
975 if !seen.insert(format!("{ctype}/{cid}#{local_id}")) {
976 continue;
977 }
978 if let Some(container) = self.read(tenant, ctype, cid).await? {
979 if let Some(c) = extract_contained_resource(container.content(), local_id) {
980 items.push(build_contained_stored(
981 &container,
982 contained_type,
983 local_id,
984 c,
985 ));
986 }
987 }
988 }
989 }
990 }
991
992 if query.contained == ContainedMode::Both {
994 let mut top_query = query.clone();
995 top_query.contained = ContainedMode::Off;
996 top_query.contained_return = ContainedReturn::Container;
997 let top = self.search(tenant, &top_query).await?;
998 let mut merged = top.resources.items;
999 let top_urls: HashSet<String> = merged.iter().map(|r| r.url()).collect();
1000 for item in items {
1001 if !top_urls.contains(&item.url()) {
1002 merged.push(item);
1003 }
1004 }
1005 items = merged;
1006 }
1007
1008 let count = query.count.unwrap_or(100) as usize;
1010 let offset = query.offset.unwrap_or(0) as usize;
1011 let total_matches = items.len() as u64;
1012 let windowed: Vec<StoredResource> = items.into_iter().skip(offset).take(count).collect();
1013
1014 let total = if query.wants_total() {
1015 Some(total_matches)
1016 } else {
1017 None
1018 };
1019 let page = Page::new(windowed, PageInfo::end());
1020 let mut result = SearchResult::new(page);
1021 if let Some(t) = total {
1022 result = result.with_total(t);
1023 }
1024 Ok(result)
1025 }
1026}
1027
1028impl SqliteBackend {
1030 fn extract_references(&self, content: &serde_json::Value, search_param: &str) -> Vec<String> {
1032 let mut refs = Vec::new();
1033
1034 if let Some(value) = content.get(search_param) {
1036 self.collect_references_from_value(value, &mut refs);
1037 }
1038
1039 refs
1043 }
1044
1045 #[allow(clippy::only_used_in_recursion)]
1047 fn collect_references_from_value(&self, value: &serde_json::Value, refs: &mut Vec<String>) {
1048 match value {
1049 serde_json::Value::Object(obj) => {
1050 if let Some(serde_json::Value::String(ref_str)) = obj.get("reference") {
1052 refs.push(ref_str.clone());
1053 }
1054 for v in obj.values() {
1056 self.collect_references_from_value(v, refs);
1057 }
1058 }
1059 serde_json::Value::Array(arr) => {
1060 for item in arr {
1061 self.collect_references_from_value(item, refs);
1062 }
1063 }
1064 _ => {}
1065 }
1066 }
1067
1068 fn parse_reference(&self, reference: &str) -> Option<(String, String)> {
1070 let path = reference
1074 .strip_prefix("http://")
1075 .or_else(|| reference.strip_prefix("https://"))
1076 .map(|s| s.rsplit('/').take(2).collect::<Vec<_>>())
1077 .unwrap_or_else(|| reference.split('/').collect());
1078
1079 if path.len() >= 2 {
1080 if reference.starts_with("http") {
1082 Some((path[1].to_string(), path[0].to_string()))
1083 } else {
1084 Some((path[0].to_string(), path[1].to_string()))
1085 }
1086 } else {
1087 None
1088 }
1089 }
1090
1091 fn fetch_resource(
1093 &self,
1094 conn: &rusqlite::Connection,
1095 tenant_id: &str,
1096 resource_type: &str,
1097 id: &str,
1098 ) -> StorageResult<Option<StoredResource>> {
1099 let result = conn.query_row(
1100 "SELECT version_id, data, last_updated, fhir_version FROM resources
1101 WHERE tenant_id = ?1 AND resource_type = ?2 AND id = ?3 AND is_deleted = 0",
1102 params![tenant_id, resource_type, id],
1103 |row| {
1104 let version_id: String = row.get(0)?;
1105 let data: Vec<u8> = row.get(1)?;
1106 let last_updated: String = row.get(2)?;
1107 let fhir_version: String = row.get(3)?;
1108 Ok((version_id, data, last_updated, fhir_version))
1109 },
1110 );
1111
1112 match result {
1113 Ok((version_id, data, last_updated_str, fhir_version_str)) => {
1114 let json_data: serde_json::Value = serde_json::from_slice(&data)
1115 .map_err(|e| internal_error(format!("Failed to deserialize: {}", e)))?;
1116
1117 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
1118 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
1119 .with_timezone(&Utc);
1120
1121 let fhir_version = FhirVersion::from_storage(&fhir_version_str)
1122 .unwrap_or_else(helios_fhir::FhirVersion::default_enabled);
1123
1124 Ok(Some(StoredResource::from_storage(
1125 resource_type,
1126 id,
1127 version_id,
1128 crate::tenant::TenantId::new(tenant_id),
1129 json_data,
1130 last_updated,
1131 last_updated,
1132 None,
1133 fhir_version,
1134 )))
1135 }
1136 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
1137 Err(e) => Err(internal_error(format!("Failed to fetch resource: {}", e))),
1138 }
1139 }
1140
1141 fn verify_reference(
1143 &self,
1144 content: &serde_json::Value,
1145 search_param: &str,
1146 reference_values: &[String],
1147 ) -> bool {
1148 let refs = self.extract_references(content, search_param);
1149 for ref_str in refs {
1150 if reference_values.iter().any(|v| ref_str.contains(v)) {
1152 return true;
1153 }
1154 if let Some((_, ref_id)) = self.parse_reference(&ref_str) {
1156 if reference_values.contains(&ref_id) {
1157 return true;
1158 }
1159 }
1160 }
1161 false
1162 }
1163
1164 #[allow(dead_code)]
1166 fn find_resources_by_value(
1167 &self,
1168 conn: &rusqlite::Connection,
1169 tenant_id: &str,
1170 resource_type: &str,
1171 param_name: &str,
1172 value: &str,
1173 ) -> StorageResult<Vec<String>> {
1174 let (system_clause, search_value) = if value.contains('|') {
1179 let parts: Vec<&str> = value.splitn(2, '|').collect();
1180 if parts.len() == 2 && !parts[0].is_empty() {
1181 (
1183 format!(
1184 "AND value_token_system = '{}'",
1185 parts[0].replace('\'', "''")
1186 ),
1187 parts[1].to_string(),
1188 )
1189 } else if parts.len() == 2 {
1190 (
1192 "AND (value_token_system IS NULL OR value_token_system = '')".to_string(),
1193 parts[1].to_string(),
1194 )
1195 } else {
1196 (String::new(), value.to_string())
1197 }
1198 } else {
1199 (String::new(), value.to_string())
1200 };
1201
1202 let escaped_value = search_value.replace('\'', "''");
1203
1204 let sql = format!(
1207 "SELECT DISTINCT resource_id FROM search_index
1208 WHERE tenant_id = ?1 AND resource_type = ?2 AND param_name = ?3
1209 AND (
1210 value_string LIKE '%{}%' COLLATE NOCASE
1211 OR value_token_code = '{}'
1212 OR value_token_code LIKE '%{}%'
1213 OR value_reference LIKE '%{}%'
1214 )
1215 {}",
1216 escaped_value, escaped_value, escaped_value, escaped_value, system_clause
1217 );
1218
1219 let mut stmt = conn
1220 .prepare(&sql)
1221 .map_err(|e| internal_error(format!("Failed to prepare find query: {}", e)))?;
1222
1223 let rows = stmt
1224 .query_map(params![tenant_id, resource_type, param_name], |row| {
1225 row.get::<_, String>(0)
1226 })
1227 .map_err(|e| internal_error(format!("Failed to execute find query: {}", e)))?;
1228
1229 let mut ids = Vec::new();
1230 for row in rows {
1231 ids.push(row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?);
1232 }
1233
1234 Ok(ids)
1235 }
1236
1237 #[allow(dead_code)]
1239 fn get_all_resources(
1240 &self,
1241 conn: &rusqlite::Connection,
1242 tenant_id: &str,
1243 resource_type: &str,
1244 ) -> StorageResult<Vec<StoredResource>> {
1245 let mut stmt = conn
1246 .prepare(
1247 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
1248 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0",
1249 )
1250 .map_err(|e| internal_error(format!("Failed to prepare query: {}", e)))?;
1251
1252 let rows = stmt
1253 .query_map(params![tenant_id, resource_type], |row| {
1254 let id: String = row.get(0)?;
1255 let version_id: String = row.get(1)?;
1256 let data: Vec<u8> = row.get(2)?;
1257 let last_updated: String = row.get(3)?;
1258 let fhir_version: String = row.get(4)?;
1259 Ok((id, version_id, data, last_updated, fhir_version))
1260 })
1261 .map_err(|e| internal_error(format!("Failed to query resources: {}", e)))?;
1262
1263 let mut resources = Vec::new();
1264 for row in rows {
1265 let (id, version_id, data, last_updated_str, fhir_version_str) =
1266 row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?;
1267
1268 let json_data: serde_json::Value = serde_json::from_slice(&data)
1269 .map_err(|e| internal_error(format!("Failed to deserialize: {}", e)))?;
1270
1271 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
1272 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
1273 .with_timezone(&Utc);
1274
1275 let fhir_version = FhirVersion::from_storage(&fhir_version_str)
1276 .unwrap_or_else(helios_fhir::FhirVersion::default_enabled);
1277
1278 resources.push(StoredResource::from_storage(
1279 resource_type,
1280 id,
1281 version_id,
1282 crate::tenant::TenantId::new(tenant_id),
1283 json_data,
1284 last_updated,
1285 last_updated,
1286 None,
1287 fhir_version,
1288 ));
1289 }
1290
1291 Ok(resources)
1292 }
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::*;
1298 use crate::core::ResourceStorage;
1299 use crate::tenant::{TenantId, TenantPermissions};
1300 use crate::types::SearchParameter;
1301 use serde_json::json;
1302
1303 fn create_test_backend() -> SqliteBackend {
1304 let data_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1309 .join("..")
1310 .join("..")
1311 .join("data");
1312 let mut config = crate::backends::sqlite::backend::SqliteBackendConfig::default();
1313 config.data_dir = Some(data_dir);
1314 let backend = SqliteBackend::with_config(":memory:", config).unwrap();
1315 backend.init_schema().unwrap();
1316 backend
1317 }
1318
1319 fn create_test_tenant() -> TenantContext {
1320 TenantContext::new(
1321 TenantId::new("test-tenant"),
1322 TenantPermissions::full_access(),
1323 )
1324 }
1325
1326 #[tokio::test]
1327 async fn test_search_empty() {
1328 let backend = create_test_backend();
1329 let tenant = create_test_tenant();
1330
1331 let query = SearchQuery::new("Patient");
1332 let result = backend.search(&tenant, &query).await.unwrap();
1333
1334 assert!(result.resources.items.is_empty());
1335 }
1336
1337 #[tokio::test]
1338 async fn test_search_returns_resources() {
1339 let backend = create_test_backend();
1340 let tenant = create_test_tenant();
1341
1342 backend
1344 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1345 .await
1346 .unwrap();
1347 backend
1348 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1349 .await
1350 .unwrap();
1351
1352 let query = SearchQuery::new("Patient");
1353 let result = backend.search(&tenant, &query).await.unwrap();
1354
1355 assert_eq!(result.resources.items.len(), 2);
1356 }
1357
1358 #[tokio::test]
1359 async fn test_search_count() {
1360 let backend = create_test_backend();
1361 let tenant = create_test_tenant();
1362
1363 backend
1364 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1365 .await
1366 .unwrap();
1367 backend
1368 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1369 .await
1370 .unwrap();
1371 backend
1372 .create(&tenant, "Observation", json!({}), FhirVersion::default())
1373 .await
1374 .unwrap();
1375
1376 let query = SearchQuery::new("Patient");
1377 let count = backend.search_count(&tenant, &query).await.unwrap();
1378
1379 assert_eq!(count, 2);
1380 }
1381
1382 #[tokio::test]
1383 async fn test_search_tenant_isolation() {
1384 let backend = create_test_backend();
1385
1386 let tenant1 =
1387 TenantContext::new(TenantId::new("tenant-1"), TenantPermissions::full_access());
1388 let tenant2 =
1389 TenantContext::new(TenantId::new("tenant-2"), TenantPermissions::full_access());
1390
1391 backend
1392 .create(&tenant1, "Patient", json!({}), FhirVersion::default())
1393 .await
1394 .unwrap();
1395 backend
1396 .create(&tenant2, "Patient", json!({}), FhirVersion::default())
1397 .await
1398 .unwrap();
1399 backend
1400 .create(&tenant2, "Patient", json!({}), FhirVersion::default())
1401 .await
1402 .unwrap();
1403
1404 let query = SearchQuery::new("Patient");
1405
1406 let result1 = backend.search(&tenant1, &query).await.unwrap();
1407 assert_eq!(result1.resources.items.len(), 1);
1408
1409 let result2 = backend.search(&tenant2, &query).await.unwrap();
1410 assert_eq!(result2.resources.items.len(), 2);
1411 }
1412
1413 #[tokio::test]
1418 async fn test_cursor_pagination_basic() {
1419 let backend = create_test_backend();
1420 let tenant = create_test_tenant();
1421
1422 for i in 0..5 {
1424 backend
1425 .create(
1426 &tenant,
1427 "Patient",
1428 json!({"name": format!("Patient{}", i)}),
1429 FhirVersion::default(),
1430 )
1431 .await
1432 .unwrap();
1433 }
1434
1435 let query = SearchQuery::new("Patient").with_count(2);
1437 let page1 = backend.search(&tenant, &query).await.unwrap();
1438
1439 assert_eq!(page1.resources.items.len(), 2);
1440 assert!(page1.resources.page_info.has_next);
1441 assert!(page1.resources.page_info.next_cursor.is_some());
1442
1443 let cursor = page1.resources.page_info.next_cursor.unwrap();
1445 let query2 = SearchQuery::new("Patient")
1446 .with_count(2)
1447 .with_cursor(cursor);
1448 let page2 = backend.search(&tenant, &query2).await.unwrap();
1449
1450 assert_eq!(page2.resources.items.len(), 2);
1451 assert!(page2.resources.page_info.has_next);
1452 assert!(page2.resources.page_info.has_previous);
1453
1454 let cursor = page2.resources.page_info.next_cursor.unwrap();
1456 let query3 = SearchQuery::new("Patient")
1457 .with_count(2)
1458 .with_cursor(cursor);
1459 let page3 = backend.search(&tenant, &query3).await.unwrap();
1460
1461 assert_eq!(page3.resources.items.len(), 1);
1462 assert!(!page3.resources.page_info.has_next);
1463 assert!(page3.resources.page_info.next_cursor.is_none());
1464
1465 let page1_ids: Vec<_> = page1.resources.items.iter().map(|r| r.id()).collect();
1467 let page2_ids: Vec<_> = page2.resources.items.iter().map(|r| r.id()).collect();
1468 let page3_ids: Vec<_> = page3.resources.items.iter().map(|r| r.id()).collect();
1469
1470 for id in &page1_ids {
1471 assert!(!page2_ids.contains(id), "Page 1 and 2 should not overlap");
1472 assert!(!page3_ids.contains(id), "Page 1 and 3 should not overlap");
1473 }
1474 for id in &page2_ids {
1475 assert!(!page3_ids.contains(id), "Page 2 and 3 should not overlap");
1476 }
1477 }
1478
1479 #[tokio::test]
1480 async fn test_cursor_pagination_no_more_results() {
1481 let backend = create_test_backend();
1482 let tenant = create_test_tenant();
1483
1484 for _ in 0..3 {
1486 backend
1487 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1488 .await
1489 .unwrap();
1490 }
1491
1492 let query = SearchQuery::new("Patient").with_count(10);
1494 let result = backend.search(&tenant, &query).await.unwrap();
1495
1496 assert_eq!(result.resources.items.len(), 3);
1497 assert!(!result.resources.page_info.has_next);
1498 assert!(result.resources.page_info.next_cursor.is_none());
1499 }
1500
1501 #[tokio::test]
1502 async fn test_cursor_pagination_empty() {
1503 let backend = create_test_backend();
1504 let tenant = create_test_tenant();
1505
1506 let query = SearchQuery::new("Patient").with_count(10);
1507 let result = backend.search(&tenant, &query).await.unwrap();
1508
1509 assert!(result.resources.items.is_empty());
1510 assert!(!result.resources.page_info.has_next);
1511 assert!(!result.resources.page_info.has_previous);
1512 }
1513
1514 #[tokio::test]
1519 async fn test_search_multi_all_types() {
1520 let backend = create_test_backend();
1521 let tenant = create_test_tenant();
1522
1523 backend
1525 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1526 .await
1527 .unwrap();
1528 backend
1529 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1530 .await
1531 .unwrap();
1532 backend
1533 .create(&tenant, "Observation", json!({}), FhirVersion::default())
1534 .await
1535 .unwrap();
1536 backend
1537 .create(&tenant, "Encounter", json!({}), FhirVersion::default())
1538 .await
1539 .unwrap();
1540
1541 let query = SearchQuery::new("Patient"); let result = backend.search_multi(&tenant, &[], &query).await.unwrap();
1544
1545 assert_eq!(result.resources.items.len(), 4);
1547 }
1548
1549 #[tokio::test]
1550 async fn test_search_multi_specific_types() {
1551 let backend = create_test_backend();
1552 let tenant = create_test_tenant();
1553
1554 backend
1556 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1557 .await
1558 .unwrap();
1559 backend
1560 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1561 .await
1562 .unwrap();
1563 backend
1564 .create(&tenant, "Observation", json!({}), FhirVersion::default())
1565 .await
1566 .unwrap();
1567 backend
1568 .create(&tenant, "Encounter", json!({}), FhirVersion::default())
1569 .await
1570 .unwrap();
1571
1572 let query = SearchQuery::new("Patient");
1574 let result = backend
1575 .search_multi(&tenant, &["Patient", "Observation"], &query)
1576 .await
1577 .unwrap();
1578
1579 assert_eq!(result.resources.items.len(), 3);
1581
1582 let types: Vec<&str> = result
1584 .resources
1585 .items
1586 .iter()
1587 .map(|r| r.resource_type())
1588 .collect();
1589 assert!(types.contains(&"Patient"));
1590 assert!(types.contains(&"Observation"));
1591 assert!(!types.contains(&"Encounter"));
1592 }
1593
1594 #[tokio::test]
1595 async fn test_search_multi_tenant_isolation() {
1596 let backend = create_test_backend();
1597 let tenant1 =
1598 TenantContext::new(TenantId::new("tenant-1"), TenantPermissions::full_access());
1599 let tenant2 =
1600 TenantContext::new(TenantId::new("tenant-2"), TenantPermissions::full_access());
1601
1602 backend
1603 .create(&tenant1, "Patient", json!({}), FhirVersion::default())
1604 .await
1605 .unwrap();
1606 backend
1607 .create(&tenant2, "Patient", json!({}), FhirVersion::default())
1608 .await
1609 .unwrap();
1610 backend
1611 .create(&tenant2, "Observation", json!({}), FhirVersion::default())
1612 .await
1613 .unwrap();
1614
1615 let query = SearchQuery::new("Patient");
1616
1617 let result1 = backend.search_multi(&tenant1, &[], &query).await.unwrap();
1618 assert_eq!(result1.resources.items.len(), 1);
1619
1620 let result2 = backend.search_multi(&tenant2, &[], &query).await.unwrap();
1621 assert_eq!(result2.resources.items.len(), 2);
1622 }
1623
1624 #[tokio::test]
1629 async fn test_resolve_includes_basic() {
1630 let backend = create_test_backend();
1631 let tenant = create_test_tenant();
1632
1633 let _patient = backend
1635 .create(
1636 &tenant,
1637 "Patient",
1638 json!({"id": "p1", "name": [{"family": "Smith"}]}),
1639 FhirVersion::default(),
1640 )
1641 .await
1642 .unwrap();
1643
1644 let observation = backend
1646 .create(
1647 &tenant,
1648 "Observation",
1649 json!({
1650 "id": "o1",
1651 "subject": {"reference": "Patient/p1"},
1652 "code": {"text": "Blood pressure"}
1653 }),
1654 FhirVersion::default(),
1655 )
1656 .await
1657 .unwrap();
1658
1659 let include = IncludeDirective {
1661 include_type: crate::types::IncludeType::Include,
1662 source_type: "Observation".to_string(),
1663 search_param: "subject".to_string(),
1664 target_type: None,
1665 iterate: false,
1666 };
1667
1668 let included = backend
1669 .resolve_includes(&tenant, &[observation], &[include])
1670 .await
1671 .unwrap();
1672
1673 assert_eq!(included.len(), 1);
1675 assert_eq!(included[0].resource_type(), "Patient");
1676 assert_eq!(included[0].id(), "p1");
1677 }
1678
1679 #[tokio::test]
1680 async fn test_resolve_includes_with_target_type_filter() {
1681 let backend = create_test_backend();
1682 let tenant = create_test_tenant();
1683
1684 backend
1686 .create(
1687 &tenant,
1688 "Patient",
1689 json!({"id": "p1"}),
1690 FhirVersion::default(),
1691 )
1692 .await
1693 .unwrap();
1694 backend
1695 .create(
1696 &tenant,
1697 "Practitioner",
1698 json!({"id": "pr1"}),
1699 FhirVersion::default(),
1700 )
1701 .await
1702 .unwrap();
1703
1704 let observation = backend
1705 .create(
1706 &tenant,
1707 "Observation",
1708 json!({
1709 "id": "o1",
1710 "subject": {"reference": "Patient/p1"},
1711 "performer": [{"reference": "Practitioner/pr1"}]
1712 }),
1713 FhirVersion::default(),
1714 )
1715 .await
1716 .unwrap();
1717
1718 let include = IncludeDirective {
1720 include_type: crate::types::IncludeType::Include,
1721 source_type: "Observation".to_string(),
1722 search_param: "subject".to_string(),
1723 target_type: Some("Patient".to_string()),
1724 iterate: false,
1725 };
1726
1727 let included = backend
1728 .resolve_includes(&tenant, &[observation], &[include])
1729 .await
1730 .unwrap();
1731
1732 assert_eq!(included.len(), 1);
1733 assert_eq!(included[0].resource_type(), "Patient");
1734 }
1735
1736 #[tokio::test]
1737 async fn test_resolve_includes_empty_resources() {
1738 let backend = create_test_backend();
1739 let tenant = create_test_tenant();
1740
1741 let include = IncludeDirective {
1742 include_type: crate::types::IncludeType::Include,
1743 source_type: "Observation".to_string(),
1744 search_param: "subject".to_string(),
1745 target_type: None,
1746 iterate: false,
1747 };
1748
1749 let included = backend
1750 .resolve_includes(&tenant, &[], &[include])
1751 .await
1752 .unwrap();
1753
1754 assert!(included.is_empty());
1755 }
1756
1757 #[tokio::test]
1758 async fn test_resolve_includes_tenant_isolation() {
1759 let backend = create_test_backend();
1760 let tenant1 =
1761 TenantContext::new(TenantId::new("tenant-1"), TenantPermissions::full_access());
1762 let tenant2 =
1763 TenantContext::new(TenantId::new("tenant-2"), TenantPermissions::full_access());
1764
1765 backend
1767 .create(
1768 &tenant1,
1769 "Patient",
1770 json!({"id": "p1"}),
1771 FhirVersion::default(),
1772 )
1773 .await
1774 .unwrap();
1775
1776 let observation = backend
1778 .create(
1779 &tenant2,
1780 "Observation",
1781 json!({
1782 "id": "o1",
1783 "subject": {"reference": "Patient/p1"}
1784 }),
1785 FhirVersion::default(),
1786 )
1787 .await
1788 .unwrap();
1789
1790 let include = IncludeDirective {
1791 include_type: crate::types::IncludeType::Include,
1792 source_type: "Observation".to_string(),
1793 search_param: "subject".to_string(),
1794 target_type: None,
1795 iterate: false,
1796 };
1797
1798 let included = backend
1800 .resolve_includes(&tenant2, &[observation], &[include])
1801 .await
1802 .unwrap();
1803
1804 assert!(included.is_empty());
1805 }
1806
1807 #[tokio::test]
1812 async fn test_resolve_revincludes_basic() {
1813 let backend = create_test_backend();
1814 let tenant = create_test_tenant();
1815
1816 let patient = backend
1818 .create(
1819 &tenant,
1820 "Patient",
1821 json!({"id": "p1"}),
1822 FhirVersion::default(),
1823 )
1824 .await
1825 .unwrap();
1826
1827 backend
1829 .create(
1830 &tenant,
1831 "Observation",
1832 json!({
1833 "id": "o1",
1834 "subject": {"reference": "Patient/p1"}
1835 }),
1836 FhirVersion::default(),
1837 )
1838 .await
1839 .unwrap();
1840 backend
1841 .create(
1842 &tenant,
1843 "Observation",
1844 json!({
1845 "id": "o2",
1846 "subject": {"reference": "Patient/p1"}
1847 }),
1848 FhirVersion::default(),
1849 )
1850 .await
1851 .unwrap();
1852
1853 backend
1855 .create(
1856 &tenant,
1857 "Observation",
1858 json!({
1859 "id": "o3",
1860 "subject": {"reference": "Patient/p2"}
1861 }),
1862 FhirVersion::default(),
1863 )
1864 .await
1865 .unwrap();
1866
1867 let revinclude = IncludeDirective {
1868 include_type: crate::types::IncludeType::Revinclude,
1869 source_type: "Observation".to_string(),
1870 search_param: "subject".to_string(),
1871 target_type: None,
1872 iterate: false,
1873 };
1874
1875 let included = backend
1876 .resolve_revincludes(&tenant, &[patient], &[revinclude])
1877 .await
1878 .unwrap();
1879
1880 assert_eq!(included.len(), 2);
1882 assert!(included.iter().all(|r| r.resource_type() == "Observation"));
1883 let ids: Vec<&str> = included.iter().map(|r| r.id()).collect();
1884 assert!(ids.contains(&"o1"));
1885 assert!(ids.contains(&"o2"));
1886 }
1887
1888 #[tokio::test]
1889 async fn test_resolve_revincludes_empty() {
1890 let backend = create_test_backend();
1891 let tenant = create_test_tenant();
1892
1893 let patient = backend
1894 .create(
1895 &tenant,
1896 "Patient",
1897 json!({"id": "p1"}),
1898 FhirVersion::default(),
1899 )
1900 .await
1901 .unwrap();
1902
1903 let revinclude = IncludeDirective {
1904 include_type: crate::types::IncludeType::Revinclude,
1905 source_type: "Observation".to_string(),
1906 search_param: "subject".to_string(),
1907 target_type: None,
1908 iterate: false,
1909 };
1910
1911 let included = backend
1913 .resolve_revincludes(&tenant, &[patient], &[revinclude])
1914 .await
1915 .unwrap();
1916
1917 assert!(included.is_empty());
1918 }
1919
1920 #[tokio::test]
1925 async fn test_resolve_chain_simple() {
1926 let backend = create_test_backend();
1927 let tenant = create_test_tenant();
1928 let tenant_id = tenant.tenant_id().as_str();
1929
1930 backend
1932 .create(
1933 &tenant,
1934 "Patient",
1935 json!({"id": "p1", "name": [{"family": "Smith"}]}),
1936 FhirVersion::default(),
1937 )
1938 .await
1939 .unwrap();
1940 backend
1941 .create(
1942 &tenant,
1943 "Patient",
1944 json!({"id": "p2", "name": [{"family": "Jones"}]}),
1945 FhirVersion::default(),
1946 )
1947 .await
1948 .unwrap();
1949
1950 backend
1952 .create(
1953 &tenant,
1954 "Observation",
1955 json!({
1956 "id": "o1",
1957 "subject": {"reference": "Patient/p1"}
1958 }),
1959 FhirVersion::default(),
1960 )
1961 .await
1962 .unwrap();
1963 backend
1964 .create(
1965 &tenant,
1966 "Observation",
1967 json!({
1968 "id": "o2",
1969 "subject": {"reference": "Patient/p2"}
1970 }),
1971 FhirVersion::default(),
1972 )
1973 .await
1974 .unwrap();
1975
1976 {
1979 let conn = backend.get_connection().unwrap();
1980 conn.execute(
1982 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
1983 VALUES (?1, 'Patient', 'p1', 'name', 'Smith')",
1984 params![tenant_id],
1985 ).unwrap();
1986 conn.execute(
1987 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
1988 VALUES (?1, 'Patient', 'p2', 'name', 'Jones')",
1989 params![tenant_id],
1990 ).unwrap();
1991 conn.execute(
1993 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
1994 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
1995 params![tenant_id],
1996 ).unwrap();
1997 conn.execute(
1998 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
1999 VALUES (?1, 'Observation', 'o2', 'subject', 'Patient/p2')",
2000 params![tenant_id],
2001 ).unwrap();
2002 }
2003
2004 let matching_ids = backend
2006 .resolve_chain(&tenant, "Observation", "subject.name", "Smith")
2007 .await
2008 .unwrap();
2009
2010 assert_eq!(matching_ids.len(), 1);
2011 assert!(matching_ids.contains(&"o1".to_string()));
2012 }
2013
2014 #[tokio::test]
2015 async fn test_resolve_chain_no_match() {
2016 let backend = create_test_backend();
2017 let tenant = create_test_tenant();
2018 let tenant_id = tenant.tenant_id().as_str();
2019
2020 backend
2022 .create(
2023 &tenant,
2024 "Patient",
2025 json!({"id": "p1", "name": [{"family": "Smith"}]}),
2026 FhirVersion::default(),
2027 )
2028 .await
2029 .unwrap();
2030
2031 backend
2033 .create(
2034 &tenant,
2035 "Observation",
2036 json!({
2037 "id": "o1",
2038 "subject": {"reference": "Patient/p1"}
2039 }),
2040 FhirVersion::default(),
2041 )
2042 .await
2043 .unwrap();
2044
2045 {
2047 let conn = backend.get_connection().unwrap();
2048 conn.execute(
2049 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
2050 VALUES (?1, 'Patient', 'p1', 'name', 'Smith')",
2051 params![tenant_id],
2052 ).unwrap();
2053 conn.execute(
2054 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2055 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
2056 params![tenant_id],
2057 ).unwrap();
2058 }
2059
2060 let matching_ids = backend
2062 .resolve_chain(&tenant, "Observation", "subject.name", "Nonexistent")
2063 .await
2064 .unwrap();
2065
2066 assert!(matching_ids.is_empty());
2067 }
2068
2069 #[tokio::test]
2070 async fn test_resolve_reverse_chain() {
2071 let backend = create_test_backend();
2072 let tenant = create_test_tenant();
2073 let tenant_id = tenant.tenant_id().as_str();
2074
2075 backend
2077 .create(
2078 &tenant,
2079 "Patient",
2080 json!({"id": "p1"}),
2081 FhirVersion::default(),
2082 )
2083 .await
2084 .unwrap();
2085 backend
2086 .create(
2087 &tenant,
2088 "Patient",
2089 json!({"id": "p2"}),
2090 FhirVersion::default(),
2091 )
2092 .await
2093 .unwrap();
2094
2095 backend
2097 .create(
2098 &tenant,
2099 "Observation",
2100 json!({
2101 "id": "o1",
2102 "subject": {"reference": "Patient/p1"},
2103 "code": {"coding": [{"code": "8867-4"}]}
2104 }),
2105 FhirVersion::default(),
2106 )
2107 .await
2108 .unwrap();
2109 backend
2110 .create(
2111 &tenant,
2112 "Observation",
2113 json!({
2114 "id": "o2",
2115 "subject": {"reference": "Patient/p2"},
2116 "code": {"coding": [{"code": "other"}]}
2117 }),
2118 FhirVersion::default(),
2119 )
2120 .await
2121 .unwrap();
2122
2123 {
2126 let conn = backend.get_connection().unwrap();
2127 conn.execute(
2129 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2130 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
2131 params![tenant_id],
2132 ).unwrap();
2133 conn.execute(
2134 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2135 VALUES (?1, 'Observation', 'o2', 'subject', 'Patient/p2')",
2136 params![tenant_id],
2137 ).unwrap();
2138 conn.execute(
2140 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_code)
2141 VALUES (?1, 'Observation', 'o1', 'code', '8867-4')",
2142 params![tenant_id],
2143 ).unwrap();
2144 conn.execute(
2145 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_code)
2146 VALUES (?1, 'Observation', 'o2', 'code', 'other')",
2147 params![tenant_id],
2148 ).unwrap();
2149 }
2150
2151 let reverse_chain = ReverseChainedParameter::terminal(
2153 "Observation",
2154 "subject",
2155 "code",
2156 crate::types::SearchValue::eq("8867-4"),
2157 );
2158
2159 let matching_ids = backend
2160 .resolve_reverse_chain(&tenant, "Patient", &reverse_chain)
2161 .await
2162 .unwrap();
2163
2164 assert_eq!(matching_ids.len(), 1);
2166 assert!(matching_ids.contains(&"p1".to_string()));
2167 }
2168
2169 #[tokio::test]
2170 async fn test_resolve_chain_multi_level() {
2171 let backend = create_test_backend();
2173 let tenant = create_test_tenant();
2174 let tenant_id = tenant.tenant_id().as_str();
2175
2176 backend
2178 .create(
2179 &tenant,
2180 "Organization",
2181 json!({"id": "org1", "name": "General Hospital"}),
2182 FhirVersion::default(),
2183 )
2184 .await
2185 .unwrap();
2186
2187 backend
2189 .create(
2190 &tenant,
2191 "Patient",
2192 json!({"id": "p1", "managingOrganization": {"reference": "Organization/org1"}}),
2193 FhirVersion::default(),
2194 )
2195 .await
2196 .unwrap();
2197
2198 backend
2200 .create(
2201 &tenant,
2202 "Observation",
2203 json!({"id": "o1", "subject": {"reference": "Patient/p1"}}),
2204 FhirVersion::default(),
2205 )
2206 .await
2207 .unwrap();
2208
2209 {
2211 let conn = backend.get_connection().unwrap();
2212 conn.execute(
2214 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
2215 VALUES (?1, 'Organization', 'org1', 'name', 'General Hospital')",
2216 params![tenant_id],
2217 ).unwrap();
2218 conn.execute(
2220 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2221 VALUES (?1, 'Patient', 'p1', 'organization', 'Organization/org1')",
2222 params![tenant_id],
2223 ).unwrap();
2224 conn.execute(
2226 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2227 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
2228 params![tenant_id],
2229 ).unwrap();
2230 }
2231
2232 let matching_ids = backend
2234 .resolve_chain(
2235 &tenant,
2236 "Observation",
2237 "subject.organization.name",
2238 "Hospital",
2239 )
2240 .await
2241 .unwrap();
2242
2243 assert_eq!(matching_ids.len(), 1);
2244 assert!(matching_ids.contains(&"o1".to_string()));
2245 }
2246
2247 #[tokio::test]
2248 async fn test_resolve_chain_with_type_modifier() {
2249 let backend = create_test_backend();
2251 let tenant = create_test_tenant();
2252 let tenant_id = tenant.tenant_id().as_str();
2253
2254 backend
2256 .create(
2257 &tenant,
2258 "Patient",
2259 json!({"id": "p1", "name": [{"family": "Smith"}]}),
2260 FhirVersion::default(),
2261 )
2262 .await
2263 .unwrap();
2264
2265 backend
2267 .create(
2268 &tenant,
2269 "Observation",
2270 json!({"id": "o1", "subject": {"reference": "Patient/p1"}}),
2271 FhirVersion::default(),
2272 )
2273 .await
2274 .unwrap();
2275
2276 {
2278 let conn = backend.get_connection().unwrap();
2279 conn.execute(
2280 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
2281 VALUES (?1, 'Patient', 'p1', 'name', 'Smith')",
2282 params![tenant_id],
2283 ).unwrap();
2284 conn.execute(
2285 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2286 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
2287 params![tenant_id],
2288 ).unwrap();
2289 }
2290
2291 let matching_ids = backend
2293 .resolve_chain(&tenant, "Observation", "subject:Patient.name", "Smith")
2294 .await
2295 .unwrap();
2296
2297 assert_eq!(matching_ids.len(), 1);
2298 assert!(matching_ids.contains(&"o1".to_string()));
2299 }
2300
2301 #[tokio::test]
2302 async fn test_chain_invalid_param_error() {
2303 let backend = create_test_backend();
2305 let tenant = create_test_tenant();
2306
2307 let result = backend
2309 .resolve_chain(&tenant, "Observation", "invalid.param", "value")
2310 .await;
2311
2312 assert!(result.is_err());
2314 }
2315
2316 #[test]
2321 fn test_parse_reference_simple() {
2322 let backend = SqliteBackend::in_memory().unwrap();
2323
2324 let result = backend.parse_reference("Patient/123");
2325 assert_eq!(result, Some(("Patient".to_string(), "123".to_string())));
2326 }
2327
2328 #[test]
2329 fn test_parse_reference_url() {
2330 let backend = SqliteBackend::in_memory().unwrap();
2331
2332 let result = backend.parse_reference("http://example.com/fhir/Patient/456");
2333 assert_eq!(result, Some(("Patient".to_string(), "456".to_string())));
2334 }
2335
2336 #[tokio::test]
2341 async fn test_token_search_system_and_code() {
2342 let backend = create_test_backend();
2343 let tenant = create_test_tenant();
2344 let tenant_id = tenant.tenant_id().as_str();
2345
2346 backend
2348 .create(
2349 &tenant,
2350 "DocumentReference",
2351 json!({
2352 "resourceType": "DocumentReference",
2353 "id": "doc1",
2354 "status": "current",
2355 "type": {
2356 "coding": [{
2357 "system": "http://loinc.org",
2358 "code": "86533-7",
2359 "display": "Patient Living will"
2360 }]
2361 },
2362 "subject": {"reference": "Patient/p1"}
2363 }),
2364 FhirVersion::default(),
2365 )
2366 .await
2367 .unwrap();
2368
2369 backend
2370 .create(
2371 &tenant,
2372 "DocumentReference",
2373 json!({
2374 "resourceType": "DocumentReference",
2375 "id": "doc2",
2376 "status": "current",
2377 "type": {
2378 "coding": [{
2379 "system": "http://loinc.org",
2380 "code": "34117-2",
2381 "display": "History and physical note"
2382 }]
2383 },
2384 "subject": {"reference": "Patient/p1"}
2385 }),
2386 FhirVersion::default(),
2387 )
2388 .await
2389 .unwrap();
2390
2391 {
2393 let conn = backend.get_connection().unwrap();
2394 conn.execute(
2395 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2396 VALUES (?1, 'DocumentReference', 'doc1', 'type', 'http://loinc.org', '86533-7')",
2397 params![tenant_id],
2398 ).unwrap();
2399 conn.execute(
2400 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2401 VALUES (?1, 'DocumentReference', 'doc2', 'type', 'http://loinc.org', '34117-2')",
2402 params![tenant_id],
2403 ).unwrap();
2404 }
2405
2406 let mut query = SearchQuery::new("DocumentReference");
2408 query.parameters.push(SearchParameter {
2409 name: "type".to_string(),
2410 param_type: crate::types::SearchParamType::Token,
2411 modifier: None,
2412 values: vec![SearchValue::eq("http://loinc.org|86533-7")],
2413 chain: vec![],
2414 components: vec![],
2415 });
2416
2417 let result = backend.search(&tenant, &query).await.unwrap();
2418
2419 assert_eq!(
2420 result.resources.items.len(),
2421 1,
2422 "Should find exactly 1 DocumentReference with type http://loinc.org|86533-7"
2423 );
2424 assert_eq!(result.resources.items[0].id(), "doc1");
2425 }
2426
2427 #[tokio::test]
2428 async fn test_token_search_code_only() {
2429 let backend = create_test_backend();
2430 let tenant = create_test_tenant();
2431 let tenant_id = tenant.tenant_id().as_str();
2432
2433 backend
2435 .create(
2436 &tenant,
2437 "DocumentReference",
2438 json!({
2439 "resourceType": "DocumentReference",
2440 "id": "doc1",
2441 "status": "current",
2442 "type": {
2443 "coding": [{
2444 "system": "http://loinc.org",
2445 "code": "86533-7"
2446 }]
2447 }
2448 }),
2449 FhirVersion::default(),
2450 )
2451 .await
2452 .unwrap();
2453
2454 {
2456 let conn = backend.get_connection().unwrap();
2457 conn.execute(
2458 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2459 VALUES (?1, 'DocumentReference', 'doc1', 'type', 'http://loinc.org', '86533-7')",
2460 params![tenant_id],
2461 ).unwrap();
2462 }
2463
2464 let mut query = SearchQuery::new("DocumentReference");
2466 query.parameters.push(SearchParameter {
2467 name: "type".to_string(),
2468 param_type: crate::types::SearchParamType::Token,
2469 modifier: None,
2470 values: vec![SearchValue::eq("86533-7")],
2471 chain: vec![],
2472 components: vec![],
2473 });
2474
2475 let result = backend.search(&tenant, &query).await.unwrap();
2476
2477 assert_eq!(
2478 result.resources.items.len(),
2479 1,
2480 "Code-only search should find the document regardless of system"
2481 );
2482 assert_eq!(result.resources.items[0].id(), "doc1");
2483 }
2484
2485 #[tokio::test]
2486 async fn test_token_search_wrong_system() {
2487 let backend = create_test_backend();
2488 let tenant = create_test_tenant();
2489 let tenant_id = tenant.tenant_id().as_str();
2490
2491 backend
2493 .create(
2494 &tenant,
2495 "DocumentReference",
2496 json!({
2497 "resourceType": "DocumentReference",
2498 "id": "doc1",
2499 "status": "current"
2500 }),
2501 FhirVersion::default(),
2502 )
2503 .await
2504 .unwrap();
2505
2506 {
2508 let conn = backend.get_connection().unwrap();
2509 conn.execute(
2510 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2511 VALUES (?1, 'DocumentReference', 'doc1', 'type', 'http://loinc.org', '86533-7')",
2512 params![tenant_id],
2513 ).unwrap();
2514 }
2515
2516 let mut query = SearchQuery::new("DocumentReference");
2518 query.parameters.push(SearchParameter {
2519 name: "type".to_string(),
2520 param_type: crate::types::SearchParamType::Token,
2521 modifier: None,
2522 values: vec![SearchValue::eq("http://snomed.info/sct|86533-7")],
2523 chain: vec![],
2524 components: vec![],
2525 });
2526
2527 let result = backend.search(&tenant, &query).await.unwrap();
2528
2529 assert_eq!(
2530 result.resources.items.len(),
2531 0,
2532 "Search with wrong system should return no results"
2533 );
2534 }
2535
2536 #[tokio::test]
2537 async fn test_token_search_combined_with_reference() {
2538 let backend = create_test_backend();
2539 let tenant = create_test_tenant();
2540 let tenant_id = tenant.tenant_id().as_str();
2541
2542 backend
2544 .create(
2545 &tenant,
2546 "DocumentReference",
2547 json!({
2548 "resourceType": "DocumentReference",
2549 "id": "doc1",
2550 "status": "current",
2551 "type": {
2552 "coding": [{"system": "http://loinc.org", "code": "86533-7"}]
2553 },
2554 "subject": {"reference": "Patient/p1"}
2555 }),
2556 FhirVersion::default(),
2557 )
2558 .await
2559 .unwrap();
2560
2561 backend
2562 .create(
2563 &tenant,
2564 "DocumentReference",
2565 json!({
2566 "resourceType": "DocumentReference",
2567 "id": "doc2",
2568 "status": "current",
2569 "type": {
2570 "coding": [{"system": "http://loinc.org", "code": "86533-7"}]
2571 },
2572 "subject": {"reference": "Patient/p2"}
2573 }),
2574 FhirVersion::default(),
2575 )
2576 .await
2577 .unwrap();
2578
2579 {
2581 let conn = backend.get_connection().unwrap();
2582 conn.execute(
2584 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2585 VALUES (?1, 'DocumentReference', 'doc1', 'type', 'http://loinc.org', '86533-7')",
2586 params![tenant_id],
2587 ).unwrap();
2588 conn.execute(
2589 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2590 VALUES (?1, 'DocumentReference', 'doc2', 'type', 'http://loinc.org', '86533-7')",
2591 params![tenant_id],
2592 ).unwrap();
2593 conn.execute(
2595 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2596 VALUES (?1, 'DocumentReference', 'doc1', 'patient', 'Patient/p1')",
2597 params![tenant_id],
2598 ).unwrap();
2599 conn.execute(
2600 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2601 VALUES (?1, 'DocumentReference', 'doc2', 'patient', 'Patient/p2')",
2602 params![tenant_id],
2603 ).unwrap();
2604 }
2605
2606 let mut query = SearchQuery::new("DocumentReference");
2608 query.parameters.push(SearchParameter {
2609 name: "patient".to_string(),
2610 param_type: crate::types::SearchParamType::Reference,
2611 modifier: None,
2612 values: vec![SearchValue::eq("p1")],
2613 chain: vec![],
2614 components: vec![],
2615 });
2616 query.parameters.push(SearchParameter {
2617 name: "type".to_string(),
2618 param_type: crate::types::SearchParamType::Token,
2619 modifier: None,
2620 values: vec![SearchValue::eq("http://loinc.org|86533-7")],
2621 chain: vec![],
2622 components: vec![],
2623 });
2624
2625 let result = backend.search(&tenant, &query).await.unwrap();
2626
2627 assert_eq!(
2628 result.resources.items.len(),
2629 1,
2630 "Combined patient + type search should find exactly doc1"
2631 );
2632 assert_eq!(result.resources.items[0].id(), "doc1");
2633 }
2634
2635 use crate::types::{ContainedMode, ContainedReturn, SearchParamType, SearchValue};
2638
2639 async fn seed_contained(backend: &SqliteBackend, tenant: &TenantContext) {
2642 backend
2643 .create(
2644 tenant,
2645 "Observation",
2646 json!({
2647 "resourceType": "Observation",
2648 "id": "obs1",
2649 "status": "final",
2650 "code": { "coding": [{ "system": "http://loinc.org", "code": "1234-5" }] },
2651 "subject": { "reference": "#p1" },
2652 "contained": [{
2653 "resourceType": "Patient",
2654 "id": "p1",
2655 "name": [{ "family": "Smith", "given": ["Contained"] }],
2656 "gender": "male"
2657 }]
2658 }),
2659 helios_fhir::FhirVersion::default(),
2660 )
2661 .await
2662 .unwrap();
2663 backend
2664 .create(
2665 tenant,
2666 "Patient",
2667 json!({ "resourceType": "Patient", "id": "top1", "name": [{ "family": "Smith" }] }),
2668 helios_fhir::FhirVersion::default(),
2669 )
2670 .await
2671 .unwrap();
2672 }
2673
2674 fn name_query(mode: ContainedMode, ret: ContainedReturn) -> SearchQuery {
2675 let mut q = SearchQuery::new("Patient");
2676 q.contained = mode;
2677 q.contained_return = ret;
2678 q.parameters.push(SearchParameter {
2679 name: "name".to_string(),
2680 param_type: SearchParamType::String,
2681 modifier: None,
2682 values: vec![SearchValue::eq("Smith")],
2683 chain: vec![],
2684 components: vec![],
2685 });
2686 q
2687 }
2688
2689 #[tokio::test]
2690 async fn contained_off_excludes_contained_resources() {
2691 let backend = create_test_backend();
2692 let tenant = create_test_tenant();
2693 seed_contained(&backend, &tenant).await;
2694
2695 let result = backend
2697 .search(
2698 &tenant,
2699 &name_query(ContainedMode::Off, ContainedReturn::Container),
2700 )
2701 .await
2702 .unwrap();
2703 let urls: Vec<String> = result.resources.items.iter().map(|r| r.url()).collect();
2704 assert_eq!(urls, vec!["Patient/top1"]);
2705 }
2706
2707 #[tokio::test]
2708 async fn contained_on_returns_container_by_default() {
2709 let backend = create_test_backend();
2710 let tenant = create_test_tenant();
2711 seed_contained(&backend, &tenant).await;
2712
2713 let result = backend
2714 .search(
2715 &tenant,
2716 &name_query(ContainedMode::On, ContainedReturn::Container),
2717 )
2718 .await
2719 .unwrap();
2720 let urls: Vec<String> = result.resources.items.iter().map(|r| r.url()).collect();
2721 assert_eq!(urls, vec!["Observation/obs1"], "container is returned");
2722 }
2723
2724 #[tokio::test]
2725 async fn contained_on_contained_type_returns_contained_resource() {
2726 let backend = create_test_backend();
2727 let tenant = create_test_tenant();
2728 seed_contained(&backend, &tenant).await;
2729
2730 let result = backend
2731 .search(
2732 &tenant,
2733 &name_query(ContainedMode::On, ContainedReturn::Contained),
2734 )
2735 .await
2736 .unwrap();
2737 assert_eq!(result.resources.items.len(), 1);
2738 let r = &result.resources.items[0];
2739 assert_eq!(r.resource_type(), "Patient");
2740 assert_eq!(r.id(), "p1");
2741 assert_eq!(r.content()["name"][0]["given"][0], "Contained");
2742 }
2743
2744 #[tokio::test]
2745 async fn contained_both_merges_top_level_and_container() {
2746 let backend = create_test_backend();
2747 let tenant = create_test_tenant();
2748 seed_contained(&backend, &tenant).await;
2749
2750 let result = backend
2751 .search(
2752 &tenant,
2753 &name_query(ContainedMode::Both, ContainedReturn::Container),
2754 )
2755 .await
2756 .unwrap();
2757 let mut urls: Vec<String> = result.resources.items.iter().map(|r| r.url()).collect();
2758 urls.sort();
2759 assert_eq!(urls, vec!["Observation/obs1", "Patient/top1"]);
2760 }
2761
2762 #[tokio::test]
2763 async fn contained_respects_param_so_no_false_match() {
2764 let backend = create_test_backend();
2765 let tenant = create_test_tenant();
2766 seed_contained(&backend, &tenant).await;
2767
2768 let mut q = name_query(ContainedMode::On, ContainedReturn::Container);
2770 q.parameters[0].values = vec![SearchValue::eq("Jones")];
2771 let result = backend.search(&tenant, &q).await.unwrap();
2772 assert!(result.resources.items.is_empty());
2773 }
2774}