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, RevincludeProvider,
19 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, 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
39#[async_trait]
40impl SearchProvider for SqliteBackend {
41 async fn search(
42 &self,
43 tenant: &TenantContext,
44 query: &SearchQuery,
45 ) -> StorageResult<SearchResult> {
46 let conn = self.get_connection()?;
47 let tenant_id = tenant.tenant_id().as_str();
48 let resource_type = &query.resource_type;
49
50 let count = query.count.unwrap_or(100) as usize;
52
53 let cursor = query
55 .cursor
56 .as_ref()
57 .and_then(|c| PageCursor::decode(c).ok());
58
59 let param_offset = if cursor.is_some() { 4 } else { 2 };
63
64 let search_filter = if !query.parameters.is_empty() {
66 let builder =
67 QueryBuilder::new(tenant_id, resource_type).with_param_offset(param_offset);
68 let fragment = builder.build(query);
69 if !fragment.sql.is_empty() {
70 Some(fragment)
73 } else {
74 None
75 }
76 } else {
77 None
78 };
79
80 let (sql, has_previous, search_params) = if let Some(ref cursor) = cursor {
82 match cursor.direction() {
84 CursorDirection::Next => {
85 let sql = if let Some(ref filter) = search_filter {
86 format!(
87 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
88 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
89 AND id IN ({})
90 AND (last_updated < ?3 OR (last_updated = ?3 AND id < ?4))
91 ORDER BY last_updated DESC, id DESC
92 LIMIT {}",
93 filter.sql,
94 count + 1
95 )
96 } else {
97 format!(
98 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
99 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
100 AND (last_updated < ?3 OR (last_updated = ?3 AND id < ?4))
101 ORDER BY last_updated DESC, id DESC
102 LIMIT {}",
103 count + 1
104 )
105 };
106 (
107 sql,
108 true,
109 search_filter.map(|f| f.params).unwrap_or_default(),
110 )
111 }
112 CursorDirection::Previous => {
113 let sql = if let Some(ref filter) = search_filter {
114 format!(
115 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
116 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
117 AND id IN ({})
118 AND (last_updated > ?3 OR (last_updated = ?3 AND id > ?4))
119 ORDER BY last_updated ASC, id ASC
120 LIMIT {}",
121 filter.sql,
122 count + 1
123 )
124 } else {
125 format!(
126 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
127 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
128 AND (last_updated > ?3 OR (last_updated = ?3 AND id > ?4))
129 ORDER BY last_updated ASC, id ASC
130 LIMIT {}",
131 count + 1
132 )
133 };
134 (
135 sql,
136 false,
137 search_filter.map(|f| f.params).unwrap_or_default(),
138 )
139 }
140 }
141 } else if let Some(offset) = query.offset {
142 let sql = if let Some(ref filter) = search_filter {
144 format!(
145 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
146 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
147 AND id IN ({})
148 ORDER BY last_updated DESC, id DESC
149 LIMIT {} OFFSET {}",
150 filter.sql,
151 count + 1,
152 offset
153 )
154 } else {
155 format!(
156 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
157 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
158 ORDER BY last_updated DESC, id DESC
159 LIMIT {} OFFSET {}",
160 count + 1,
161 offset
162 )
163 };
164 (
165 sql,
166 offset > 0,
167 search_filter.map(|f| f.params).unwrap_or_default(),
168 )
169 } else {
170 let sql = if let Some(ref filter) = search_filter {
172 format!(
173 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
174 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
175 AND id IN ({})
176 ORDER BY last_updated DESC, id DESC
177 LIMIT {}",
178 filter.sql,
179 count + 1
180 )
181 } else {
182 format!(
183 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
184 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
185 ORDER BY last_updated DESC, id DESC
186 LIMIT {}",
187 count + 1
188 )
189 };
190 (
191 sql,
192 false,
193 search_filter.map(|f| f.params).unwrap_or_default(),
194 )
195 };
196
197 let mut stmt = conn
198 .prepare(&sql)
199 .map_err(|e| internal_error(format!("Failed to prepare search query: {}", e)))?;
200
201 let raw_rows: Vec<(String, String, Vec<u8>, String, String)> =
206 if let Some(ref cursor) = cursor {
207 let (cursor_timestamp, cursor_id) = Self::extract_cursor_values(cursor)?;
208
209 let mut all_params: Vec<Box<dyn rusqlite::ToSql>> = vec![
211 Box::new(tenant_id.to_string()),
212 Box::new(resource_type.to_string()),
213 Box::new(cursor_timestamp),
214 Box::new(cursor_id),
215 ];
216
217 for param in &search_params {
219 match param {
220 SqlParam::String(s) => all_params.push(Box::new(s.clone())),
221 SqlParam::Integer(i) => all_params.push(Box::new(*i)),
222 SqlParam::Float(f) => all_params.push(Box::new(*f)),
223 SqlParam::Null => all_params.push(Box::new(Option::<String>::None)),
224 }
225 }
226
227 let param_refs: Vec<&dyn rusqlite::ToSql> =
228 all_params.iter().map(|p| p.as_ref()).collect();
229
230 let rows = stmt
231 .query_map(param_refs.as_slice(), |row| {
232 let id: String = row.get(0)?;
233 let version_id: String = row.get(1)?;
234 let data: Vec<u8> = row.get(2)?;
235 let last_updated: String = row.get(3)?;
236 let fhir_version: String = row.get(4)?;
237 Ok((id, version_id, data, last_updated, fhir_version))
238 })
239 .map_err(|e| internal_error(format!("Failed to execute search: {}", e)))?;
240
241 rows.collect::<Result<Vec<_>, _>>()
242 .map_err(|e| internal_error(format!("Failed to read row: {}", e)))?
243 } else {
244 let mut all_params: Vec<Box<dyn rusqlite::ToSql>> = vec![
246 Box::new(tenant_id.to_string()),
247 Box::new(resource_type.to_string()),
248 ];
249
250 for param in &search_params {
252 match param {
253 SqlParam::String(s) => all_params.push(Box::new(s.clone())),
254 SqlParam::Integer(i) => all_params.push(Box::new(*i)),
255 SqlParam::Float(f) => all_params.push(Box::new(*f)),
256 SqlParam::Null => all_params.push(Box::new(Option::<String>::None)),
257 }
258 }
259
260 let param_refs: Vec<&dyn rusqlite::ToSql> =
261 all_params.iter().map(|p| p.as_ref()).collect();
262
263 let rows = stmt
264 .query_map(param_refs.as_slice(), |row| {
265 let id: String = row.get(0)?;
266 let version_id: String = row.get(1)?;
267 let data: Vec<u8> = row.get(2)?;
268 let last_updated: String = row.get(3)?;
269 let fhir_version: String = row.get(4)?;
270 Ok((id, version_id, data, last_updated, fhir_version))
271 })
272 .map_err(|e| internal_error(format!("Failed to execute search: {}", e)))?;
273
274 rows.collect::<Result<Vec<_>, _>>()
275 .map_err(|e| internal_error(format!("Failed to read row: {}", e)))?
276 };
277
278 let mut resources = Vec::new();
279 for (id, version_id, data, last_updated_str, fhir_version_str) in raw_rows {
280 let json_data: serde_json::Value = serde_json::from_slice(&data)
281 .map_err(|e| internal_error(format!("Failed to deserialize resource: {}", e)))?;
282
283 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
284 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
285 .with_timezone(&Utc);
286
287 let fhir_version = FhirVersion::from_storage(&fhir_version_str).unwrap_or_default();
288
289 let resource = StoredResource::from_storage(
290 resource_type.clone(),
291 id,
292 version_id,
293 tenant.tenant_id().clone(),
294 json_data,
295 last_updated,
296 last_updated,
297 None,
298 fhir_version,
299 );
300
301 resources.push(resource);
302 }
303
304 if cursor
306 .as_ref()
307 .map(|c| c.direction() == CursorDirection::Previous)
308 .unwrap_or(false)
309 {
310 resources.reverse();
311 }
312
313 let has_next = resources.len() > count;
315 if has_next {
316 resources.pop(); }
318
319 let next_cursor = if has_next {
321 resources.last().map(|r| {
322 let cursor = PageCursor::new(
323 vec![CursorValue::String(r.last_modified().to_rfc3339())],
324 r.id(),
325 );
326 cursor.encode()
327 })
328 } else {
329 None
330 };
331
332 let previous_cursor = if has_previous {
333 resources.first().map(|r| {
334 let cursor = PageCursor::previous(
335 vec![CursorValue::String(r.last_modified().to_rfc3339())],
336 r.id(),
337 );
338 cursor.encode()
339 })
340 } else {
341 None
342 };
343
344 let page_info = PageInfo {
345 next_cursor,
346 previous_cursor,
347 total: None,
348 has_next,
349 has_previous,
350 };
351
352 let page = Page::new(resources, page_info);
353
354 Ok(SearchResult {
355 resources: page,
356 included: Vec::new(),
357 total: None,
358 })
359 }
360
361 async fn search_count(
362 &self,
363 tenant: &TenantContext,
364 query: &SearchQuery,
365 ) -> StorageResult<u64> {
366 let conn = self.get_connection()?;
367 let tenant_id = tenant.tenant_id().as_str();
368 let resource_type = &query.resource_type;
369
370 let (sql, all_params): (String, Vec<Box<dyn rusqlite::ToSql>>) = if !query
372 .parameters
373 .is_empty()
374 {
375 let builder = QueryBuilder::new(tenant_id, resource_type).with_param_offset(2);
376 let fragment = builder.build(query);
377
378 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![
379 Box::new(tenant_id.to_string()),
380 Box::new(resource_type.to_string()),
381 ];
382
383 for param in &fragment.params {
385 match param {
386 SqlParam::String(s) => params.push(Box::new(s.clone())),
387 SqlParam::Integer(i) => params.push(Box::new(*i)),
388 SqlParam::Float(f) => params.push(Box::new(*f)),
389 SqlParam::Null => params.push(Box::new(Option::<String>::None)),
390 }
391 }
392
393 let sql = format!(
394 "SELECT COUNT(*) FROM resources WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0 AND id IN ({})",
395 fragment.sql
396 );
397
398 (sql, params)
399 } else {
400 let sql = "SELECT COUNT(*) FROM resources WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0".to_string();
401 let params: Vec<Box<dyn rusqlite::ToSql>> = vec![
402 Box::new(tenant_id.to_string()),
403 Box::new(resource_type.to_string()),
404 ];
405 (sql, params)
406 };
407
408 let param_refs: Vec<&dyn rusqlite::ToSql> = all_params.iter().map(|p| p.as_ref()).collect();
409
410 let count: i64 = conn
411 .query_row(&sql, param_refs.as_slice(), |row| row.get(0))
412 .map_err(|e| internal_error(format!("Failed to count resources: {}", e)))?;
413
414 Ok(count as u64)
415 }
416}
417
418#[async_trait]
419impl MultiTypeSearchProvider for SqliteBackend {
420 async fn search_multi(
421 &self,
422 tenant: &TenantContext,
423 resource_types: &[&str],
424 query: &SearchQuery,
425 ) -> StorageResult<SearchResult> {
426 let conn = self.get_connection()?;
427 let tenant_id = tenant.tenant_id().as_str();
428
429 let count = query.count.unwrap_or(100) as usize;
431 let offset = query.offset.unwrap_or(0) as usize;
432
433 let type_filter = if resource_types.is_empty() {
435 String::new()
437 } else {
438 let types: Vec<String> = resource_types
440 .iter()
441 .map(|t| format!("'{}'", t.replace('\'', "''")))
442 .collect();
443 format!(" AND resource_type IN ({})", types.join(", "))
444 };
445
446 let sql = format!(
447 "SELECT resource_type, id, version_id, data, last_updated, fhir_version FROM resources
448 WHERE tenant_id = ?1 AND is_deleted = 0{}
449 ORDER BY last_updated DESC
450 LIMIT {} OFFSET {}",
451 type_filter,
452 count + 1,
453 offset
454 );
455
456 let mut stmt = conn
457 .prepare(&sql)
458 .map_err(|e| internal_error(format!("Failed to prepare multi-type search: {}", e)))?;
459
460 let rows = stmt
461 .query_map(params![tenant_id], |row| {
462 let resource_type: String = row.get(0)?;
463 let id: String = row.get(1)?;
464 let version_id: String = row.get(2)?;
465 let data: Vec<u8> = row.get(3)?;
466 let last_updated: String = row.get(4)?;
467 let fhir_version: String = row.get(5)?;
468 Ok((
469 resource_type,
470 id,
471 version_id,
472 data,
473 last_updated,
474 fhir_version,
475 ))
476 })
477 .map_err(|e| internal_error(format!("Failed to execute multi-type search: {}", e)))?;
478
479 let mut resources = Vec::new();
480 for row in rows {
481 let (resource_type, id, version_id, data, last_updated_str, fhir_version_str) =
482 row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?;
483
484 let json_data: serde_json::Value = serde_json::from_slice(&data)
485 .map_err(|e| internal_error(format!("Failed to deserialize resource: {}", e)))?;
486
487 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
488 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
489 .with_timezone(&Utc);
490
491 let fhir_version = FhirVersion::from_storage(&fhir_version_str).unwrap_or_default();
492
493 let resource = StoredResource::from_storage(
494 resource_type,
495 id,
496 version_id,
497 tenant.tenant_id().clone(),
498 json_data,
499 last_updated,
500 last_updated,
501 None,
502 fhir_version,
503 );
504
505 resources.push(resource);
506 }
507
508 let has_next = resources.len() > count;
510 if has_next {
511 resources.pop();
512 }
513
514 let page_info = PageInfo {
515 next_cursor: None,
516 previous_cursor: None,
517 total: None,
518 has_next,
519 has_previous: offset > 0,
520 };
521
522 Ok(SearchResult {
523 resources: Page::new(resources, page_info),
524 included: Vec::new(),
525 total: None,
526 })
527 }
528}
529
530#[async_trait]
531impl IncludeProvider for SqliteBackend {
532 async fn resolve_includes(
533 &self,
534 tenant: &TenantContext,
535 resources: &[StoredResource],
536 includes: &[IncludeDirective],
537 ) -> StorageResult<Vec<StoredResource>> {
538 if resources.is_empty() || includes.is_empty() {
539 return Ok(Vec::new());
540 }
541
542 let conn = self.get_connection()?;
543 let tenant_id = tenant.tenant_id().as_str();
544
545 let mut included = Vec::new();
546 let mut seen_refs: HashSet<String> = HashSet::new();
547
548 for include in includes {
549 for resource in resources {
551 if resource.resource_type() != include.source_type {
553 continue;
554 }
555
556 let refs = self.extract_references(resource.content(), &include.search_param);
558
559 for reference in refs {
560 if let Some((ref_type, ref_id)) = self.parse_reference(&reference) {
562 if let Some(ref target) = include.target_type {
564 if ref_type != *target {
565 continue;
566 }
567 }
568
569 let ref_key = format!("{}/{}", ref_type, ref_id);
571 if seen_refs.contains(&ref_key) {
572 continue;
573 }
574 seen_refs.insert(ref_key);
575
576 if let Some(included_resource) =
578 self.fetch_resource(&conn, tenant_id, &ref_type, &ref_id)?
579 {
580 included.push(included_resource);
581 }
582 }
583 }
584 }
585 }
586
587 Ok(included)
588 }
589}
590
591#[async_trait]
592impl RevincludeProvider for SqliteBackend {
593 async fn resolve_revincludes(
594 &self,
595 tenant: &TenantContext,
596 resources: &[StoredResource],
597 revincludes: &[IncludeDirective],
598 ) -> StorageResult<Vec<StoredResource>> {
599 if resources.is_empty() || revincludes.is_empty() {
600 return Ok(Vec::new());
601 }
602
603 let conn = self.get_connection()?;
604 let tenant_id = tenant.tenant_id().as_str();
605
606 let mut included = Vec::new();
607 let mut seen_ids: HashSet<String> = HashSet::new();
608
609 for revinclude in revincludes {
610 let mut reference_values: Vec<String> = Vec::new();
612 for resource in resources {
613 reference_values.push(format!("{}/{}", resource.resource_type(), resource.id()));
616 reference_values.push(resource.id().to_string());
618 }
619
620 if reference_values.is_empty() {
621 continue;
622 }
623
624 let reference_pattern = reference_values
626 .iter()
627 .map(|r| format!("%{}%", r.replace('%', "\\%").replace('_', "\\_")))
628 .collect::<Vec<_>>();
629
630 let sql = format!(
633 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
634 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0
635 AND ({})",
636 reference_pattern
637 .iter()
638 .map(|_| "data LIKE ?".to_string())
639 .collect::<Vec<_>>()
640 .join(" OR ")
641 );
642
643 let mut stmt = conn.prepare(&sql).map_err(|e| {
644 internal_error(format!("Failed to prepare revinclude query: {}", e))
645 })?;
646
647 let mut param_values: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
649 param_values.push(Box::new(tenant_id.to_string()));
650 param_values.push(Box::new(revinclude.source_type.clone()));
651 for pattern in &reference_pattern {
652 param_values.push(Box::new(pattern.clone()));
653 }
654
655 let param_refs: Vec<&dyn rusqlite::ToSql> =
656 param_values.iter().map(|p| p.as_ref()).collect();
657
658 let rows = stmt
659 .query_map(param_refs.as_slice(), |row| {
660 let id: String = row.get(0)?;
661 let version_id: String = row.get(1)?;
662 let data: Vec<u8> = row.get(2)?;
663 let last_updated: String = row.get(3)?;
664 let fhir_version: String = row.get(4)?;
665 Ok((id, version_id, data, last_updated, fhir_version))
666 })
667 .map_err(|e| {
668 internal_error(format!("Failed to execute revinclude query: {}", e))
669 })?;
670
671 for row in rows {
672 let (id, version_id, data, last_updated_str, fhir_version_str) =
673 row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?;
674
675 let resource_key = format!("{}/{}", revinclude.source_type, id);
677 if seen_ids.contains(&resource_key) {
678 continue;
679 }
680
681 let json_data: serde_json::Value = serde_json::from_slice(&data)
682 .map_err(|e| internal_error(format!("Failed to deserialize: {}", e)))?;
683
684 if !self.verify_reference(&json_data, &revinclude.search_param, &reference_values) {
686 continue;
687 }
688
689 seen_ids.insert(resource_key);
690
691 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
692 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
693 .with_timezone(&Utc);
694
695 let fhir_version = FhirVersion::from_storage(&fhir_version_str).unwrap_or_default();
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
865impl SqliteBackend {
867 fn extract_cursor_values(cursor: &PageCursor) -> StorageResult<(String, String)> {
869 let sort_values = cursor.sort_values();
870 let timestamp = match sort_values.first() {
871 Some(CursorValue::String(s)) => s.clone(),
872 _ => {
873 return Err(internal_error(
874 "Invalid cursor: missing or invalid timestamp".to_string(),
875 ));
876 }
877 };
878 let id = cursor.resource_id().to_string();
879 Ok((timestamp, id))
880 }
881
882 fn extract_references(&self, content: &serde_json::Value, search_param: &str) -> Vec<String> {
884 let mut refs = Vec::new();
885
886 if let Some(value) = content.get(search_param) {
888 self.collect_references_from_value(value, &mut refs);
889 }
890
891 refs
895 }
896
897 #[allow(clippy::only_used_in_recursion)]
899 fn collect_references_from_value(&self, value: &serde_json::Value, refs: &mut Vec<String>) {
900 match value {
901 serde_json::Value::Object(obj) => {
902 if let Some(serde_json::Value::String(ref_str)) = obj.get("reference") {
904 refs.push(ref_str.clone());
905 }
906 for v in obj.values() {
908 self.collect_references_from_value(v, refs);
909 }
910 }
911 serde_json::Value::Array(arr) => {
912 for item in arr {
913 self.collect_references_from_value(item, refs);
914 }
915 }
916 _ => {}
917 }
918 }
919
920 fn parse_reference(&self, reference: &str) -> Option<(String, String)> {
922 let path = reference
926 .strip_prefix("http://")
927 .or_else(|| reference.strip_prefix("https://"))
928 .map(|s| s.rsplit('/').take(2).collect::<Vec<_>>())
929 .unwrap_or_else(|| reference.split('/').collect());
930
931 if path.len() >= 2 {
932 if reference.starts_with("http") {
934 Some((path[1].to_string(), path[0].to_string()))
935 } else {
936 Some((path[0].to_string(), path[1].to_string()))
937 }
938 } else {
939 None
940 }
941 }
942
943 fn fetch_resource(
945 &self,
946 conn: &rusqlite::Connection,
947 tenant_id: &str,
948 resource_type: &str,
949 id: &str,
950 ) -> StorageResult<Option<StoredResource>> {
951 let result = conn.query_row(
952 "SELECT version_id, data, last_updated, fhir_version FROM resources
953 WHERE tenant_id = ?1 AND resource_type = ?2 AND id = ?3 AND is_deleted = 0",
954 params![tenant_id, resource_type, id],
955 |row| {
956 let version_id: String = row.get(0)?;
957 let data: Vec<u8> = row.get(1)?;
958 let last_updated: String = row.get(2)?;
959 let fhir_version: String = row.get(3)?;
960 Ok((version_id, data, last_updated, fhir_version))
961 },
962 );
963
964 match result {
965 Ok((version_id, data, last_updated_str, fhir_version_str)) => {
966 let json_data: serde_json::Value = serde_json::from_slice(&data)
967 .map_err(|e| internal_error(format!("Failed to deserialize: {}", e)))?;
968
969 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
970 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
971 .with_timezone(&Utc);
972
973 let fhir_version = FhirVersion::from_storage(&fhir_version_str).unwrap_or_default();
974
975 Ok(Some(StoredResource::from_storage(
976 resource_type,
977 id,
978 version_id,
979 crate::tenant::TenantId::new(tenant_id),
980 json_data,
981 last_updated,
982 last_updated,
983 None,
984 fhir_version,
985 )))
986 }
987 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
988 Err(e) => Err(internal_error(format!("Failed to fetch resource: {}", e))),
989 }
990 }
991
992 fn verify_reference(
994 &self,
995 content: &serde_json::Value,
996 search_param: &str,
997 reference_values: &[String],
998 ) -> bool {
999 let refs = self.extract_references(content, search_param);
1000 for ref_str in refs {
1001 if reference_values.iter().any(|v| ref_str.contains(v)) {
1003 return true;
1004 }
1005 if let Some((_, ref_id)) = self.parse_reference(&ref_str) {
1007 if reference_values.contains(&ref_id) {
1008 return true;
1009 }
1010 }
1011 }
1012 false
1013 }
1014
1015 #[allow(dead_code)]
1017 fn infer_target_type(&self, _base_type: &str, reference_param: &str) -> String {
1018 match reference_param {
1021 "patient" | "subject" => "Patient".to_string(),
1022 "practitioner" | "performer" => "Practitioner".to_string(),
1023 "organization" => "Organization".to_string(),
1024 "encounter" => "Encounter".to_string(),
1025 "location" => "Location".to_string(),
1026 "device" => "Device".to_string(),
1027 _ => {
1028 let mut chars = reference_param.chars();
1030 match chars.next() {
1031 Some(c) => c.to_uppercase().chain(chars).collect(),
1032 None => reference_param.to_string(),
1033 }
1034 }
1035 }
1036 }
1037
1038 #[allow(dead_code)]
1040 fn find_resources_by_value(
1041 &self,
1042 conn: &rusqlite::Connection,
1043 tenant_id: &str,
1044 resource_type: &str,
1045 param_name: &str,
1046 value: &str,
1047 ) -> StorageResult<Vec<String>> {
1048 let (system_clause, search_value) = if value.contains('|') {
1053 let parts: Vec<&str> = value.splitn(2, '|').collect();
1054 if parts.len() == 2 && !parts[0].is_empty() {
1055 (
1057 format!(
1058 "AND value_token_system = '{}'",
1059 parts[0].replace('\'', "''")
1060 ),
1061 parts[1].to_string(),
1062 )
1063 } else if parts.len() == 2 {
1064 (
1066 "AND (value_token_system IS NULL OR value_token_system = '')".to_string(),
1067 parts[1].to_string(),
1068 )
1069 } else {
1070 (String::new(), value.to_string())
1071 }
1072 } else {
1073 (String::new(), value.to_string())
1074 };
1075
1076 let escaped_value = search_value.replace('\'', "''");
1077
1078 let sql = format!(
1081 "SELECT DISTINCT resource_id FROM search_index
1082 WHERE tenant_id = ?1 AND resource_type = ?2 AND param_name = ?3
1083 AND (
1084 value_string LIKE '%{}%' COLLATE NOCASE
1085 OR value_token_code = '{}'
1086 OR value_token_code LIKE '%{}%'
1087 OR value_reference LIKE '%{}%'
1088 )
1089 {}",
1090 escaped_value, escaped_value, escaped_value, escaped_value, system_clause
1091 );
1092
1093 let mut stmt = conn
1094 .prepare(&sql)
1095 .map_err(|e| internal_error(format!("Failed to prepare find query: {}", e)))?;
1096
1097 let rows = stmt
1098 .query_map(params![tenant_id, resource_type, param_name], |row| {
1099 row.get::<_, String>(0)
1100 })
1101 .map_err(|e| internal_error(format!("Failed to execute find query: {}", e)))?;
1102
1103 let mut ids = Vec::new();
1104 for row in rows {
1105 ids.push(row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?);
1106 }
1107
1108 Ok(ids)
1109 }
1110
1111 #[allow(dead_code)]
1113 fn get_all_resources(
1114 &self,
1115 conn: &rusqlite::Connection,
1116 tenant_id: &str,
1117 resource_type: &str,
1118 ) -> StorageResult<Vec<StoredResource>> {
1119 let mut stmt = conn
1120 .prepare(
1121 "SELECT id, version_id, data, last_updated, fhir_version FROM resources
1122 WHERE tenant_id = ?1 AND resource_type = ?2 AND is_deleted = 0",
1123 )
1124 .map_err(|e| internal_error(format!("Failed to prepare query: {}", e)))?;
1125
1126 let rows = stmt
1127 .query_map(params![tenant_id, resource_type], |row| {
1128 let id: String = row.get(0)?;
1129 let version_id: String = row.get(1)?;
1130 let data: Vec<u8> = row.get(2)?;
1131 let last_updated: String = row.get(3)?;
1132 let fhir_version: String = row.get(4)?;
1133 Ok((id, version_id, data, last_updated, fhir_version))
1134 })
1135 .map_err(|e| internal_error(format!("Failed to query resources: {}", e)))?;
1136
1137 let mut resources = Vec::new();
1138 for row in rows {
1139 let (id, version_id, data, last_updated_str, fhir_version_str) =
1140 row.map_err(|e| internal_error(format!("Failed to read row: {}", e)))?;
1141
1142 let json_data: serde_json::Value = serde_json::from_slice(&data)
1143 .map_err(|e| internal_error(format!("Failed to deserialize: {}", e)))?;
1144
1145 let last_updated = chrono::DateTime::parse_from_rfc3339(&last_updated_str)
1146 .map_err(|e| internal_error(format!("Failed to parse last_updated: {}", e)))?
1147 .with_timezone(&Utc);
1148
1149 let fhir_version = FhirVersion::from_storage(&fhir_version_str).unwrap_or_default();
1150
1151 resources.push(StoredResource::from_storage(
1152 resource_type,
1153 id,
1154 version_id,
1155 crate::tenant::TenantId::new(tenant_id),
1156 json_data,
1157 last_updated,
1158 last_updated,
1159 None,
1160 fhir_version,
1161 ));
1162 }
1163
1164 Ok(resources)
1165 }
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use super::*;
1171 use crate::core::ResourceStorage;
1172 use crate::tenant::{TenantId, TenantPermissions};
1173 use crate::types::SearchParameter;
1174 use serde_json::json;
1175
1176 fn create_test_backend() -> SqliteBackend {
1177 let backend = SqliteBackend::in_memory().unwrap();
1178 backend.init_schema().unwrap();
1179 backend
1180 }
1181
1182 fn create_test_tenant() -> TenantContext {
1183 TenantContext::new(
1184 TenantId::new("test-tenant"),
1185 TenantPermissions::full_access(),
1186 )
1187 }
1188
1189 #[tokio::test]
1190 async fn test_search_empty() {
1191 let backend = create_test_backend();
1192 let tenant = create_test_tenant();
1193
1194 let query = SearchQuery::new("Patient");
1195 let result = backend.search(&tenant, &query).await.unwrap();
1196
1197 assert!(result.resources.items.is_empty());
1198 }
1199
1200 #[tokio::test]
1201 async fn test_search_returns_resources() {
1202 let backend = create_test_backend();
1203 let tenant = create_test_tenant();
1204
1205 backend
1207 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1208 .await
1209 .unwrap();
1210 backend
1211 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1212 .await
1213 .unwrap();
1214
1215 let query = SearchQuery::new("Patient");
1216 let result = backend.search(&tenant, &query).await.unwrap();
1217
1218 assert_eq!(result.resources.items.len(), 2);
1219 }
1220
1221 #[tokio::test]
1222 async fn test_search_count() {
1223 let backend = create_test_backend();
1224 let tenant = create_test_tenant();
1225
1226 backend
1227 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1228 .await
1229 .unwrap();
1230 backend
1231 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1232 .await
1233 .unwrap();
1234 backend
1235 .create(&tenant, "Observation", json!({}), FhirVersion::default())
1236 .await
1237 .unwrap();
1238
1239 let query = SearchQuery::new("Patient");
1240 let count = backend.search_count(&tenant, &query).await.unwrap();
1241
1242 assert_eq!(count, 2);
1243 }
1244
1245 #[tokio::test]
1246 async fn test_search_tenant_isolation() {
1247 let backend = create_test_backend();
1248
1249 let tenant1 =
1250 TenantContext::new(TenantId::new("tenant-1"), TenantPermissions::full_access());
1251 let tenant2 =
1252 TenantContext::new(TenantId::new("tenant-2"), TenantPermissions::full_access());
1253
1254 backend
1255 .create(&tenant1, "Patient", json!({}), FhirVersion::default())
1256 .await
1257 .unwrap();
1258 backend
1259 .create(&tenant2, "Patient", json!({}), FhirVersion::default())
1260 .await
1261 .unwrap();
1262 backend
1263 .create(&tenant2, "Patient", json!({}), FhirVersion::default())
1264 .await
1265 .unwrap();
1266
1267 let query = SearchQuery::new("Patient");
1268
1269 let result1 = backend.search(&tenant1, &query).await.unwrap();
1270 assert_eq!(result1.resources.items.len(), 1);
1271
1272 let result2 = backend.search(&tenant2, &query).await.unwrap();
1273 assert_eq!(result2.resources.items.len(), 2);
1274 }
1275
1276 #[tokio::test]
1281 async fn test_cursor_pagination_basic() {
1282 let backend = create_test_backend();
1283 let tenant = create_test_tenant();
1284
1285 for i in 0..5 {
1287 backend
1288 .create(
1289 &tenant,
1290 "Patient",
1291 json!({"name": format!("Patient{}", i)}),
1292 FhirVersion::default(),
1293 )
1294 .await
1295 .unwrap();
1296 }
1297
1298 let query = SearchQuery::new("Patient").with_count(2);
1300 let page1 = backend.search(&tenant, &query).await.unwrap();
1301
1302 assert_eq!(page1.resources.items.len(), 2);
1303 assert!(page1.resources.page_info.has_next);
1304 assert!(page1.resources.page_info.next_cursor.is_some());
1305
1306 let cursor = page1.resources.page_info.next_cursor.unwrap();
1308 let query2 = SearchQuery::new("Patient")
1309 .with_count(2)
1310 .with_cursor(cursor);
1311 let page2 = backend.search(&tenant, &query2).await.unwrap();
1312
1313 assert_eq!(page2.resources.items.len(), 2);
1314 assert!(page2.resources.page_info.has_next);
1315 assert!(page2.resources.page_info.has_previous);
1316
1317 let cursor = page2.resources.page_info.next_cursor.unwrap();
1319 let query3 = SearchQuery::new("Patient")
1320 .with_count(2)
1321 .with_cursor(cursor);
1322 let page3 = backend.search(&tenant, &query3).await.unwrap();
1323
1324 assert_eq!(page3.resources.items.len(), 1);
1325 assert!(!page3.resources.page_info.has_next);
1326 assert!(page3.resources.page_info.next_cursor.is_none());
1327
1328 let page1_ids: Vec<_> = page1.resources.items.iter().map(|r| r.id()).collect();
1330 let page2_ids: Vec<_> = page2.resources.items.iter().map(|r| r.id()).collect();
1331 let page3_ids: Vec<_> = page3.resources.items.iter().map(|r| r.id()).collect();
1332
1333 for id in &page1_ids {
1334 assert!(!page2_ids.contains(id), "Page 1 and 2 should not overlap");
1335 assert!(!page3_ids.contains(id), "Page 1 and 3 should not overlap");
1336 }
1337 for id in &page2_ids {
1338 assert!(!page3_ids.contains(id), "Page 2 and 3 should not overlap");
1339 }
1340 }
1341
1342 #[tokio::test]
1343 async fn test_cursor_pagination_no_more_results() {
1344 let backend = create_test_backend();
1345 let tenant = create_test_tenant();
1346
1347 for _ in 0..3 {
1349 backend
1350 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1351 .await
1352 .unwrap();
1353 }
1354
1355 let query = SearchQuery::new("Patient").with_count(10);
1357 let result = backend.search(&tenant, &query).await.unwrap();
1358
1359 assert_eq!(result.resources.items.len(), 3);
1360 assert!(!result.resources.page_info.has_next);
1361 assert!(result.resources.page_info.next_cursor.is_none());
1362 }
1363
1364 #[tokio::test]
1365 async fn test_cursor_pagination_empty() {
1366 let backend = create_test_backend();
1367 let tenant = create_test_tenant();
1368
1369 let query = SearchQuery::new("Patient").with_count(10);
1370 let result = backend.search(&tenant, &query).await.unwrap();
1371
1372 assert!(result.resources.items.is_empty());
1373 assert!(!result.resources.page_info.has_next);
1374 assert!(!result.resources.page_info.has_previous);
1375 }
1376
1377 #[tokio::test]
1382 async fn test_search_multi_all_types() {
1383 let backend = create_test_backend();
1384 let tenant = create_test_tenant();
1385
1386 backend
1388 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1389 .await
1390 .unwrap();
1391 backend
1392 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1393 .await
1394 .unwrap();
1395 backend
1396 .create(&tenant, "Observation", json!({}), FhirVersion::default())
1397 .await
1398 .unwrap();
1399 backend
1400 .create(&tenant, "Encounter", json!({}), FhirVersion::default())
1401 .await
1402 .unwrap();
1403
1404 let query = SearchQuery::new("Patient"); let result = backend.search_multi(&tenant, &[], &query).await.unwrap();
1407
1408 assert_eq!(result.resources.items.len(), 4);
1410 }
1411
1412 #[tokio::test]
1413 async fn test_search_multi_specific_types() {
1414 let backend = create_test_backend();
1415 let tenant = create_test_tenant();
1416
1417 backend
1419 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1420 .await
1421 .unwrap();
1422 backend
1423 .create(&tenant, "Patient", json!({}), FhirVersion::default())
1424 .await
1425 .unwrap();
1426 backend
1427 .create(&tenant, "Observation", json!({}), FhirVersion::default())
1428 .await
1429 .unwrap();
1430 backend
1431 .create(&tenant, "Encounter", json!({}), FhirVersion::default())
1432 .await
1433 .unwrap();
1434
1435 let query = SearchQuery::new("Patient");
1437 let result = backend
1438 .search_multi(&tenant, &["Patient", "Observation"], &query)
1439 .await
1440 .unwrap();
1441
1442 assert_eq!(result.resources.items.len(), 3);
1444
1445 let types: Vec<&str> = result
1447 .resources
1448 .items
1449 .iter()
1450 .map(|r| r.resource_type())
1451 .collect();
1452 assert!(types.contains(&"Patient"));
1453 assert!(types.contains(&"Observation"));
1454 assert!(!types.contains(&"Encounter"));
1455 }
1456
1457 #[tokio::test]
1458 async fn test_search_multi_tenant_isolation() {
1459 let backend = create_test_backend();
1460 let tenant1 =
1461 TenantContext::new(TenantId::new("tenant-1"), TenantPermissions::full_access());
1462 let tenant2 =
1463 TenantContext::new(TenantId::new("tenant-2"), TenantPermissions::full_access());
1464
1465 backend
1466 .create(&tenant1, "Patient", json!({}), FhirVersion::default())
1467 .await
1468 .unwrap();
1469 backend
1470 .create(&tenant2, "Patient", json!({}), FhirVersion::default())
1471 .await
1472 .unwrap();
1473 backend
1474 .create(&tenant2, "Observation", json!({}), FhirVersion::default())
1475 .await
1476 .unwrap();
1477
1478 let query = SearchQuery::new("Patient");
1479
1480 let result1 = backend.search_multi(&tenant1, &[], &query).await.unwrap();
1481 assert_eq!(result1.resources.items.len(), 1);
1482
1483 let result2 = backend.search_multi(&tenant2, &[], &query).await.unwrap();
1484 assert_eq!(result2.resources.items.len(), 2);
1485 }
1486
1487 #[tokio::test]
1492 async fn test_resolve_includes_basic() {
1493 let backend = create_test_backend();
1494 let tenant = create_test_tenant();
1495
1496 let _patient = backend
1498 .create(
1499 &tenant,
1500 "Patient",
1501 json!({"id": "p1", "name": [{"family": "Smith"}]}),
1502 FhirVersion::default(),
1503 )
1504 .await
1505 .unwrap();
1506
1507 let observation = backend
1509 .create(
1510 &tenant,
1511 "Observation",
1512 json!({
1513 "id": "o1",
1514 "subject": {"reference": "Patient/p1"},
1515 "code": {"text": "Blood pressure"}
1516 }),
1517 FhirVersion::default(),
1518 )
1519 .await
1520 .unwrap();
1521
1522 let include = IncludeDirective {
1524 include_type: crate::types::IncludeType::Include,
1525 source_type: "Observation".to_string(),
1526 search_param: "subject".to_string(),
1527 target_type: None,
1528 iterate: false,
1529 };
1530
1531 let included = backend
1532 .resolve_includes(&tenant, &[observation], &[include])
1533 .await
1534 .unwrap();
1535
1536 assert_eq!(included.len(), 1);
1538 assert_eq!(included[0].resource_type(), "Patient");
1539 assert_eq!(included[0].id(), "p1");
1540 }
1541
1542 #[tokio::test]
1543 async fn test_resolve_includes_with_target_type_filter() {
1544 let backend = create_test_backend();
1545 let tenant = create_test_tenant();
1546
1547 backend
1549 .create(
1550 &tenant,
1551 "Patient",
1552 json!({"id": "p1"}),
1553 FhirVersion::default(),
1554 )
1555 .await
1556 .unwrap();
1557 backend
1558 .create(
1559 &tenant,
1560 "Practitioner",
1561 json!({"id": "pr1"}),
1562 FhirVersion::default(),
1563 )
1564 .await
1565 .unwrap();
1566
1567 let observation = backend
1568 .create(
1569 &tenant,
1570 "Observation",
1571 json!({
1572 "id": "o1",
1573 "subject": {"reference": "Patient/p1"},
1574 "performer": [{"reference": "Practitioner/pr1"}]
1575 }),
1576 FhirVersion::default(),
1577 )
1578 .await
1579 .unwrap();
1580
1581 let include = IncludeDirective {
1583 include_type: crate::types::IncludeType::Include,
1584 source_type: "Observation".to_string(),
1585 search_param: "subject".to_string(),
1586 target_type: Some("Patient".to_string()),
1587 iterate: false,
1588 };
1589
1590 let included = backend
1591 .resolve_includes(&tenant, &[observation], &[include])
1592 .await
1593 .unwrap();
1594
1595 assert_eq!(included.len(), 1);
1596 assert_eq!(included[0].resource_type(), "Patient");
1597 }
1598
1599 #[tokio::test]
1600 async fn test_resolve_includes_empty_resources() {
1601 let backend = create_test_backend();
1602 let tenant = create_test_tenant();
1603
1604 let include = IncludeDirective {
1605 include_type: crate::types::IncludeType::Include,
1606 source_type: "Observation".to_string(),
1607 search_param: "subject".to_string(),
1608 target_type: None,
1609 iterate: false,
1610 };
1611
1612 let included = backend
1613 .resolve_includes(&tenant, &[], &[include])
1614 .await
1615 .unwrap();
1616
1617 assert!(included.is_empty());
1618 }
1619
1620 #[tokio::test]
1621 async fn test_resolve_includes_tenant_isolation() {
1622 let backend = create_test_backend();
1623 let tenant1 =
1624 TenantContext::new(TenantId::new("tenant-1"), TenantPermissions::full_access());
1625 let tenant2 =
1626 TenantContext::new(TenantId::new("tenant-2"), TenantPermissions::full_access());
1627
1628 backend
1630 .create(
1631 &tenant1,
1632 "Patient",
1633 json!({"id": "p1"}),
1634 FhirVersion::default(),
1635 )
1636 .await
1637 .unwrap();
1638
1639 let observation = backend
1641 .create(
1642 &tenant2,
1643 "Observation",
1644 json!({
1645 "id": "o1",
1646 "subject": {"reference": "Patient/p1"}
1647 }),
1648 FhirVersion::default(),
1649 )
1650 .await
1651 .unwrap();
1652
1653 let include = IncludeDirective {
1654 include_type: crate::types::IncludeType::Include,
1655 source_type: "Observation".to_string(),
1656 search_param: "subject".to_string(),
1657 target_type: None,
1658 iterate: false,
1659 };
1660
1661 let included = backend
1663 .resolve_includes(&tenant2, &[observation], &[include])
1664 .await
1665 .unwrap();
1666
1667 assert!(included.is_empty());
1668 }
1669
1670 #[tokio::test]
1675 async fn test_resolve_revincludes_basic() {
1676 let backend = create_test_backend();
1677 let tenant = create_test_tenant();
1678
1679 let patient = backend
1681 .create(
1682 &tenant,
1683 "Patient",
1684 json!({"id": "p1"}),
1685 FhirVersion::default(),
1686 )
1687 .await
1688 .unwrap();
1689
1690 backend
1692 .create(
1693 &tenant,
1694 "Observation",
1695 json!({
1696 "id": "o1",
1697 "subject": {"reference": "Patient/p1"}
1698 }),
1699 FhirVersion::default(),
1700 )
1701 .await
1702 .unwrap();
1703 backend
1704 .create(
1705 &tenant,
1706 "Observation",
1707 json!({
1708 "id": "o2",
1709 "subject": {"reference": "Patient/p1"}
1710 }),
1711 FhirVersion::default(),
1712 )
1713 .await
1714 .unwrap();
1715
1716 backend
1718 .create(
1719 &tenant,
1720 "Observation",
1721 json!({
1722 "id": "o3",
1723 "subject": {"reference": "Patient/p2"}
1724 }),
1725 FhirVersion::default(),
1726 )
1727 .await
1728 .unwrap();
1729
1730 let revinclude = IncludeDirective {
1731 include_type: crate::types::IncludeType::Revinclude,
1732 source_type: "Observation".to_string(),
1733 search_param: "subject".to_string(),
1734 target_type: None,
1735 iterate: false,
1736 };
1737
1738 let included = backend
1739 .resolve_revincludes(&tenant, &[patient], &[revinclude])
1740 .await
1741 .unwrap();
1742
1743 assert_eq!(included.len(), 2);
1745 assert!(included.iter().all(|r| r.resource_type() == "Observation"));
1746 let ids: Vec<&str> = included.iter().map(|r| r.id()).collect();
1747 assert!(ids.contains(&"o1"));
1748 assert!(ids.contains(&"o2"));
1749 }
1750
1751 #[tokio::test]
1752 async fn test_resolve_revincludes_empty() {
1753 let backend = create_test_backend();
1754 let tenant = create_test_tenant();
1755
1756 let patient = backend
1757 .create(
1758 &tenant,
1759 "Patient",
1760 json!({"id": "p1"}),
1761 FhirVersion::default(),
1762 )
1763 .await
1764 .unwrap();
1765
1766 let revinclude = IncludeDirective {
1767 include_type: crate::types::IncludeType::Revinclude,
1768 source_type: "Observation".to_string(),
1769 search_param: "subject".to_string(),
1770 target_type: None,
1771 iterate: false,
1772 };
1773
1774 let included = backend
1776 .resolve_revincludes(&tenant, &[patient], &[revinclude])
1777 .await
1778 .unwrap();
1779
1780 assert!(included.is_empty());
1781 }
1782
1783 #[tokio::test]
1788 async fn test_resolve_chain_simple() {
1789 let backend = create_test_backend();
1790 let tenant = create_test_tenant();
1791 let tenant_id = tenant.tenant_id().as_str();
1792
1793 backend
1795 .create(
1796 &tenant,
1797 "Patient",
1798 json!({"id": "p1", "name": [{"family": "Smith"}]}),
1799 FhirVersion::default(),
1800 )
1801 .await
1802 .unwrap();
1803 backend
1804 .create(
1805 &tenant,
1806 "Patient",
1807 json!({"id": "p2", "name": [{"family": "Jones"}]}),
1808 FhirVersion::default(),
1809 )
1810 .await
1811 .unwrap();
1812
1813 backend
1815 .create(
1816 &tenant,
1817 "Observation",
1818 json!({
1819 "id": "o1",
1820 "subject": {"reference": "Patient/p1"}
1821 }),
1822 FhirVersion::default(),
1823 )
1824 .await
1825 .unwrap();
1826 backend
1827 .create(
1828 &tenant,
1829 "Observation",
1830 json!({
1831 "id": "o2",
1832 "subject": {"reference": "Patient/p2"}
1833 }),
1834 FhirVersion::default(),
1835 )
1836 .await
1837 .unwrap();
1838
1839 {
1842 let conn = backend.get_connection().unwrap();
1843 conn.execute(
1845 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
1846 VALUES (?1, 'Patient', 'p1', 'name', 'Smith')",
1847 params![tenant_id],
1848 ).unwrap();
1849 conn.execute(
1850 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
1851 VALUES (?1, 'Patient', 'p2', 'name', 'Jones')",
1852 params![tenant_id],
1853 ).unwrap();
1854 conn.execute(
1856 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
1857 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
1858 params![tenant_id],
1859 ).unwrap();
1860 conn.execute(
1861 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
1862 VALUES (?1, 'Observation', 'o2', 'subject', 'Patient/p2')",
1863 params![tenant_id],
1864 ).unwrap();
1865 }
1866
1867 let matching_ids = backend
1869 .resolve_chain(&tenant, "Observation", "subject.name", "Smith")
1870 .await
1871 .unwrap();
1872
1873 assert_eq!(matching_ids.len(), 1);
1874 assert!(matching_ids.contains(&"o1".to_string()));
1875 }
1876
1877 #[tokio::test]
1878 async fn test_resolve_chain_no_match() {
1879 let backend = create_test_backend();
1880 let tenant = create_test_tenant();
1881 let tenant_id = tenant.tenant_id().as_str();
1882
1883 backend
1885 .create(
1886 &tenant,
1887 "Patient",
1888 json!({"id": "p1", "name": [{"family": "Smith"}]}),
1889 FhirVersion::default(),
1890 )
1891 .await
1892 .unwrap();
1893
1894 backend
1896 .create(
1897 &tenant,
1898 "Observation",
1899 json!({
1900 "id": "o1",
1901 "subject": {"reference": "Patient/p1"}
1902 }),
1903 FhirVersion::default(),
1904 )
1905 .await
1906 .unwrap();
1907
1908 {
1910 let conn = backend.get_connection().unwrap();
1911 conn.execute(
1912 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
1913 VALUES (?1, 'Patient', 'p1', 'name', 'Smith')",
1914 params![tenant_id],
1915 ).unwrap();
1916 conn.execute(
1917 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
1918 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
1919 params![tenant_id],
1920 ).unwrap();
1921 }
1922
1923 let matching_ids = backend
1925 .resolve_chain(&tenant, "Observation", "subject.name", "Nonexistent")
1926 .await
1927 .unwrap();
1928
1929 assert!(matching_ids.is_empty());
1930 }
1931
1932 #[tokio::test]
1933 async fn test_resolve_reverse_chain() {
1934 let backend = create_test_backend();
1935 let tenant = create_test_tenant();
1936 let tenant_id = tenant.tenant_id().as_str();
1937
1938 backend
1940 .create(
1941 &tenant,
1942 "Patient",
1943 json!({"id": "p1"}),
1944 FhirVersion::default(),
1945 )
1946 .await
1947 .unwrap();
1948 backend
1949 .create(
1950 &tenant,
1951 "Patient",
1952 json!({"id": "p2"}),
1953 FhirVersion::default(),
1954 )
1955 .await
1956 .unwrap();
1957
1958 backend
1960 .create(
1961 &tenant,
1962 "Observation",
1963 json!({
1964 "id": "o1",
1965 "subject": {"reference": "Patient/p1"},
1966 "code": {"coding": [{"code": "8867-4"}]}
1967 }),
1968 FhirVersion::default(),
1969 )
1970 .await
1971 .unwrap();
1972 backend
1973 .create(
1974 &tenant,
1975 "Observation",
1976 json!({
1977 "id": "o2",
1978 "subject": {"reference": "Patient/p2"},
1979 "code": {"coding": [{"code": "other"}]}
1980 }),
1981 FhirVersion::default(),
1982 )
1983 .await
1984 .unwrap();
1985
1986 {
1989 let conn = backend.get_connection().unwrap();
1990 conn.execute(
1992 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
1993 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
1994 params![tenant_id],
1995 ).unwrap();
1996 conn.execute(
1997 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
1998 VALUES (?1, 'Observation', 'o2', 'subject', 'Patient/p2')",
1999 params![tenant_id],
2000 ).unwrap();
2001 conn.execute(
2003 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_code)
2004 VALUES (?1, 'Observation', 'o1', 'code', '8867-4')",
2005 params![tenant_id],
2006 ).unwrap();
2007 conn.execute(
2008 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_code)
2009 VALUES (?1, 'Observation', 'o2', 'code', 'other')",
2010 params![tenant_id],
2011 ).unwrap();
2012 }
2013
2014 let reverse_chain = ReverseChainedParameter::terminal(
2016 "Observation",
2017 "subject",
2018 "code",
2019 crate::types::SearchValue::eq("8867-4"),
2020 );
2021
2022 let matching_ids = backend
2023 .resolve_reverse_chain(&tenant, "Patient", &reverse_chain)
2024 .await
2025 .unwrap();
2026
2027 assert_eq!(matching_ids.len(), 1);
2029 assert!(matching_ids.contains(&"p1".to_string()));
2030 }
2031
2032 #[tokio::test]
2033 async fn test_resolve_chain_multi_level() {
2034 let backend = create_test_backend();
2036 let tenant = create_test_tenant();
2037 let tenant_id = tenant.tenant_id().as_str();
2038
2039 backend
2041 .create(
2042 &tenant,
2043 "Organization",
2044 json!({"id": "org1", "name": "General Hospital"}),
2045 FhirVersion::default(),
2046 )
2047 .await
2048 .unwrap();
2049
2050 backend
2052 .create(
2053 &tenant,
2054 "Patient",
2055 json!({"id": "p1", "managingOrganization": {"reference": "Organization/org1"}}),
2056 FhirVersion::default(),
2057 )
2058 .await
2059 .unwrap();
2060
2061 backend
2063 .create(
2064 &tenant,
2065 "Observation",
2066 json!({"id": "o1", "subject": {"reference": "Patient/p1"}}),
2067 FhirVersion::default(),
2068 )
2069 .await
2070 .unwrap();
2071
2072 {
2074 let conn = backend.get_connection().unwrap();
2075 conn.execute(
2077 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
2078 VALUES (?1, 'Organization', 'org1', 'name', 'General Hospital')",
2079 params![tenant_id],
2080 ).unwrap();
2081 conn.execute(
2083 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2084 VALUES (?1, 'Patient', 'p1', 'organization', 'Organization/org1')",
2085 params![tenant_id],
2086 ).unwrap();
2087 conn.execute(
2089 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2090 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
2091 params![tenant_id],
2092 ).unwrap();
2093 }
2094
2095 let matching_ids = backend
2097 .resolve_chain(
2098 &tenant,
2099 "Observation",
2100 "subject.organization.name",
2101 "Hospital",
2102 )
2103 .await
2104 .unwrap();
2105
2106 assert_eq!(matching_ids.len(), 1);
2107 assert!(matching_ids.contains(&"o1".to_string()));
2108 }
2109
2110 #[tokio::test]
2111 async fn test_resolve_chain_with_type_modifier() {
2112 let backend = create_test_backend();
2114 let tenant = create_test_tenant();
2115 let tenant_id = tenant.tenant_id().as_str();
2116
2117 backend
2119 .create(
2120 &tenant,
2121 "Patient",
2122 json!({"id": "p1", "name": [{"family": "Smith"}]}),
2123 FhirVersion::default(),
2124 )
2125 .await
2126 .unwrap();
2127
2128 backend
2130 .create(
2131 &tenant,
2132 "Observation",
2133 json!({"id": "o1", "subject": {"reference": "Patient/p1"}}),
2134 FhirVersion::default(),
2135 )
2136 .await
2137 .unwrap();
2138
2139 {
2141 let conn = backend.get_connection().unwrap();
2142 conn.execute(
2143 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_string)
2144 VALUES (?1, 'Patient', 'p1', 'name', 'Smith')",
2145 params![tenant_id],
2146 ).unwrap();
2147 conn.execute(
2148 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2149 VALUES (?1, 'Observation', 'o1', 'subject', 'Patient/p1')",
2150 params![tenant_id],
2151 ).unwrap();
2152 }
2153
2154 let matching_ids = backend
2156 .resolve_chain(&tenant, "Observation", "subject:Patient.name", "Smith")
2157 .await
2158 .unwrap();
2159
2160 assert_eq!(matching_ids.len(), 1);
2161 assert!(matching_ids.contains(&"o1".to_string()));
2162 }
2163
2164 #[tokio::test]
2165 async fn test_chain_invalid_param_error() {
2166 let backend = create_test_backend();
2168 let tenant = create_test_tenant();
2169
2170 let result = backend
2172 .resolve_chain(&tenant, "Observation", "invalid.param", "value")
2173 .await;
2174
2175 assert!(result.is_err());
2177 }
2178
2179 #[test]
2184 fn test_parse_reference_simple() {
2185 let backend = SqliteBackend::in_memory().unwrap();
2186
2187 let result = backend.parse_reference("Patient/123");
2188 assert_eq!(result, Some(("Patient".to_string(), "123".to_string())));
2189 }
2190
2191 #[test]
2192 fn test_parse_reference_url() {
2193 let backend = SqliteBackend::in_memory().unwrap();
2194
2195 let result = backend.parse_reference("http://example.com/fhir/Patient/456");
2196 assert_eq!(result, Some(("Patient".to_string(), "456".to_string())));
2197 }
2198
2199 #[test]
2200 fn test_infer_target_type() {
2201 let backend = SqliteBackend::in_memory().unwrap();
2202
2203 assert_eq!(
2204 backend.infer_target_type("Observation", "patient"),
2205 "Patient"
2206 );
2207 assert_eq!(
2208 backend.infer_target_type("Observation", "subject"),
2209 "Patient"
2210 );
2211 assert_eq!(
2212 backend.infer_target_type("Encounter", "practitioner"),
2213 "Practitioner"
2214 );
2215 assert_eq!(
2216 backend.infer_target_type("Patient", "organization"),
2217 "Organization"
2218 );
2219 assert_eq!(backend.infer_target_type("Observation", "custom"), "Custom");
2221 }
2222
2223 #[tokio::test]
2228 async fn test_token_search_system_and_code() {
2229 let backend = create_test_backend();
2230 let tenant = create_test_tenant();
2231 let tenant_id = tenant.tenant_id().as_str();
2232
2233 backend
2235 .create(
2236 &tenant,
2237 "DocumentReference",
2238 json!({
2239 "resourceType": "DocumentReference",
2240 "id": "doc1",
2241 "status": "current",
2242 "type": {
2243 "coding": [{
2244 "system": "http://loinc.org",
2245 "code": "86533-7",
2246 "display": "Patient Living will"
2247 }]
2248 },
2249 "subject": {"reference": "Patient/p1"}
2250 }),
2251 FhirVersion::default(),
2252 )
2253 .await
2254 .unwrap();
2255
2256 backend
2257 .create(
2258 &tenant,
2259 "DocumentReference",
2260 json!({
2261 "resourceType": "DocumentReference",
2262 "id": "doc2",
2263 "status": "current",
2264 "type": {
2265 "coding": [{
2266 "system": "http://loinc.org",
2267 "code": "34117-2",
2268 "display": "History and physical note"
2269 }]
2270 },
2271 "subject": {"reference": "Patient/p1"}
2272 }),
2273 FhirVersion::default(),
2274 )
2275 .await
2276 .unwrap();
2277
2278 {
2280 let conn = backend.get_connection().unwrap();
2281 conn.execute(
2282 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2283 VALUES (?1, 'DocumentReference', 'doc1', 'type', 'http://loinc.org', '86533-7')",
2284 params![tenant_id],
2285 ).unwrap();
2286 conn.execute(
2287 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2288 VALUES (?1, 'DocumentReference', 'doc2', 'type', 'http://loinc.org', '34117-2')",
2289 params![tenant_id],
2290 ).unwrap();
2291 }
2292
2293 let mut query = SearchQuery::new("DocumentReference");
2295 query.parameters.push(SearchParameter {
2296 name: "type".to_string(),
2297 param_type: crate::types::SearchParamType::Token,
2298 modifier: None,
2299 values: vec![SearchValue::eq("http://loinc.org|86533-7")],
2300 chain: vec![],
2301 components: vec![],
2302 });
2303
2304 let result = backend.search(&tenant, &query).await.unwrap();
2305
2306 assert_eq!(
2307 result.resources.items.len(),
2308 1,
2309 "Should find exactly 1 DocumentReference with type http://loinc.org|86533-7"
2310 );
2311 assert_eq!(result.resources.items[0].id(), "doc1");
2312 }
2313
2314 #[tokio::test]
2315 async fn test_token_search_code_only() {
2316 let backend = create_test_backend();
2317 let tenant = create_test_tenant();
2318 let tenant_id = tenant.tenant_id().as_str();
2319
2320 backend
2322 .create(
2323 &tenant,
2324 "DocumentReference",
2325 json!({
2326 "resourceType": "DocumentReference",
2327 "id": "doc1",
2328 "status": "current",
2329 "type": {
2330 "coding": [{
2331 "system": "http://loinc.org",
2332 "code": "86533-7"
2333 }]
2334 }
2335 }),
2336 FhirVersion::default(),
2337 )
2338 .await
2339 .unwrap();
2340
2341 {
2343 let conn = backend.get_connection().unwrap();
2344 conn.execute(
2345 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2346 VALUES (?1, 'DocumentReference', 'doc1', 'type', 'http://loinc.org', '86533-7')",
2347 params![tenant_id],
2348 ).unwrap();
2349 }
2350
2351 let mut query = SearchQuery::new("DocumentReference");
2353 query.parameters.push(SearchParameter {
2354 name: "type".to_string(),
2355 param_type: crate::types::SearchParamType::Token,
2356 modifier: None,
2357 values: vec![SearchValue::eq("86533-7")],
2358 chain: vec![],
2359 components: vec![],
2360 });
2361
2362 let result = backend.search(&tenant, &query).await.unwrap();
2363
2364 assert_eq!(
2365 result.resources.items.len(),
2366 1,
2367 "Code-only search should find the document regardless of system"
2368 );
2369 assert_eq!(result.resources.items[0].id(), "doc1");
2370 }
2371
2372 #[tokio::test]
2373 async fn test_token_search_wrong_system() {
2374 let backend = create_test_backend();
2375 let tenant = create_test_tenant();
2376 let tenant_id = tenant.tenant_id().as_str();
2377
2378 backend
2380 .create(
2381 &tenant,
2382 "DocumentReference",
2383 json!({
2384 "resourceType": "DocumentReference",
2385 "id": "doc1",
2386 "status": "current"
2387 }),
2388 FhirVersion::default(),
2389 )
2390 .await
2391 .unwrap();
2392
2393 {
2395 let conn = backend.get_connection().unwrap();
2396 conn.execute(
2397 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2398 VALUES (?1, 'DocumentReference', 'doc1', 'type', 'http://loinc.org', '86533-7')",
2399 params![tenant_id],
2400 ).unwrap();
2401 }
2402
2403 let mut query = SearchQuery::new("DocumentReference");
2405 query.parameters.push(SearchParameter {
2406 name: "type".to_string(),
2407 param_type: crate::types::SearchParamType::Token,
2408 modifier: None,
2409 values: vec![SearchValue::eq("http://snomed.info/sct|86533-7")],
2410 chain: vec![],
2411 components: vec![],
2412 });
2413
2414 let result = backend.search(&tenant, &query).await.unwrap();
2415
2416 assert_eq!(
2417 result.resources.items.len(),
2418 0,
2419 "Search with wrong system should return no results"
2420 );
2421 }
2422
2423 #[tokio::test]
2424 async fn test_token_search_combined_with_reference() {
2425 let backend = create_test_backend();
2426 let tenant = create_test_tenant();
2427 let tenant_id = tenant.tenant_id().as_str();
2428
2429 backend
2431 .create(
2432 &tenant,
2433 "DocumentReference",
2434 json!({
2435 "resourceType": "DocumentReference",
2436 "id": "doc1",
2437 "status": "current",
2438 "type": {
2439 "coding": [{"system": "http://loinc.org", "code": "86533-7"}]
2440 },
2441 "subject": {"reference": "Patient/p1"}
2442 }),
2443 FhirVersion::default(),
2444 )
2445 .await
2446 .unwrap();
2447
2448 backend
2449 .create(
2450 &tenant,
2451 "DocumentReference",
2452 json!({
2453 "resourceType": "DocumentReference",
2454 "id": "doc2",
2455 "status": "current",
2456 "type": {
2457 "coding": [{"system": "http://loinc.org", "code": "86533-7"}]
2458 },
2459 "subject": {"reference": "Patient/p2"}
2460 }),
2461 FhirVersion::default(),
2462 )
2463 .await
2464 .unwrap();
2465
2466 {
2468 let conn = backend.get_connection().unwrap();
2469 conn.execute(
2471 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2472 VALUES (?1, 'DocumentReference', 'doc1', 'type', 'http://loinc.org', '86533-7')",
2473 params![tenant_id],
2474 ).unwrap();
2475 conn.execute(
2476 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_token_system, value_token_code)
2477 VALUES (?1, 'DocumentReference', 'doc2', 'type', 'http://loinc.org', '86533-7')",
2478 params![tenant_id],
2479 ).unwrap();
2480 conn.execute(
2482 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2483 VALUES (?1, 'DocumentReference', 'doc1', 'patient', 'Patient/p1')",
2484 params![tenant_id],
2485 ).unwrap();
2486 conn.execute(
2487 "INSERT INTO search_index (tenant_id, resource_type, resource_id, param_name, value_reference)
2488 VALUES (?1, 'DocumentReference', 'doc2', 'patient', 'Patient/p2')",
2489 params![tenant_id],
2490 ).unwrap();
2491 }
2492
2493 let mut query = SearchQuery::new("DocumentReference");
2495 query.parameters.push(SearchParameter {
2496 name: "patient".to_string(),
2497 param_type: crate::types::SearchParamType::Reference,
2498 modifier: None,
2499 values: vec![SearchValue::eq("p1")],
2500 chain: vec![],
2501 components: vec![],
2502 });
2503 query.parameters.push(SearchParameter {
2504 name: "type".to_string(),
2505 param_type: crate::types::SearchParamType::Token,
2506 modifier: None,
2507 values: vec![SearchValue::eq("http://loinc.org|86533-7")],
2508 chain: vec![],
2509 components: vec![],
2510 });
2511
2512 let result = backend.search(&tenant, &query).await.unwrap();
2513
2514 assert_eq!(
2515 result.resources.items.len(),
2516 1,
2517 "Combined patient + type search should find exactly doc1"
2518 );
2519 assert_eq!(result.resources.items[0].id(), "doc1");
2520 }
2521}