Skip to main content

helios_persistence/backends/sqlite/
search_impl.rs

1//! Search implementation for SQLite backend.
2//!
3//! This module provides search functionality for the SQLite backend including:
4//! - Basic single-type search
5//! - Multi-type search
6//! - _include and _revinclude support
7//! - Chained search parameter support
8//! - Search parameter filtering using the search_index table
9
10use 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        // Get count with default
51        let count = query.count.unwrap_or(100) as usize;
52
53        // Check for cursor-based pagination
54        let cursor = query
55            .cursor
56            .as_ref()
57            .and_then(|c| PageCursor::decode(c).ok());
58
59        // Determine param offset based on pagination mode
60        // Cursor pagination: ?1=tenant, ?2=type, ?3=timestamp, ?4=id -> offset=4
61        // Non-cursor: ?1=tenant, ?2=type -> offset=2
62        let param_offset = if cursor.is_some() { 4 } else { 2 };
63
64        // Build the search filter subquery if there are search parameters
65        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                // The QueryBuilder returns a SELECT DISTINCT resource_id query
71                // We use this as a subquery to filter the resources table
72                Some(fragment)
73            } else {
74                None
75            }
76        } else {
77            None
78        };
79
80        // Build query based on pagination mode
81        let (sql, has_previous, search_params) = if let Some(ref cursor) = cursor {
82            // Cursor-based pagination using keyset
83            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            // Offset-based pagination (legacy support)
143            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            // First page (no cursor, no offset)
171            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        // Build the parameter list for binding
202        // Base params are always tenant_id and resource_type
203        // For cursor pagination, add cursor_timestamp and cursor_id
204        // Then append any search params from the QueryBuilder
205        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                // Build params: [tenant_id, resource_type, cursor_timestamp, cursor_id, ...search_params]
210                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                // Add search params
218                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                // Build params: [tenant_id, resource_type, ...search_params]
245                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                // Add search params
251                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        // For backward pagination, reverse the results to maintain DESC order
305        if cursor
306            .as_ref()
307            .map(|c| c.direction() == CursorDirection::Previous)
308            .unwrap_or(false)
309        {
310            resources.reverse();
311        }
312
313        // Check if there are more results (we fetched one extra)
314        let has_next = resources.len() > count;
315        if has_next {
316            resources.pop(); // Remove the extra one
317        }
318
319        // Generate cursors for pagination
320        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        // Build the search filter if there are search parameters
371        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            // Add search params
384            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        // Get count and offset with defaults
430        let count = query.count.unwrap_or(100) as usize;
431        let offset = query.offset.unwrap_or(0) as usize;
432
433        // Build the type filter
434        let type_filter = if resource_types.is_empty() {
435            // No filter - search all types
436            String::new()
437        } else {
438            // Filter to specific types
439            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        // Check if there are more results
509        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 each resource, extract references for the include parameter
550            for resource in resources {
551                // Skip if source type doesn't match
552                if resource.resource_type() != include.source_type {
553                    continue;
554                }
555
556                // Extract references from the resource based on the search parameter
557                let refs = self.extract_references(resource.content(), &include.search_param);
558
559                for reference in refs {
560                    // Parse the reference (e.g., "Patient/123")
561                    if let Some((ref_type, ref_id)) = self.parse_reference(&reference) {
562                        // Apply target type filter if specified
563                        if let Some(ref target) = include.target_type {
564                            if ref_type != *target {
565                                continue;
566                            }
567                        }
568
569                        // Skip if we've already included this resource
570                        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                        // Fetch the referenced resource
577                        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            // Build the list of references to search for
611            let mut reference_values: Vec<String> = Vec::new();
612            for resource in resources {
613                // For _revinclude, we look for resources that reference our results
614                // The reference format is typically "ResourceType/id"
615                reference_values.push(format!("{}/{}", resource.resource_type(), resource.id()));
616                // Also check just the ID in case the reference doesn't include the type
617                reference_values.push(resource.id().to_string());
618            }
619
620            if reference_values.is_empty() {
621                continue;
622            }
623
624            // Search for resources of source_type that reference our resources
625            let reference_pattern = reference_values
626                .iter()
627                .map(|r| format!("%{}%", r.replace('%', "\\%").replace('_', "\\_")))
628                .collect::<Vec<_>>();
629
630            // Build SQL to find resources containing any of the references
631            // We search in the JSON data for the search_param field containing a reference
632            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            // Build params: tenant_id, source_type, then all the patterns
648            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                // Skip if we've already included this resource
676                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                // Verify this resource actually references one of our results via the search_param
685                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        // Create the chain query builder with registry access
736        let builder = ChainQueryBuilder::new(tenant_id, base_type, self.get_search_registry())
737            .with_param_offset(2); // After ?1 (tenant) and ?2 (resource_type)
738
739        // Parse the chain
740        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        // Build the SQL fragment
748        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        // Execute the query to get matching IDs
757        // The fragment generates: r.id IN (SELECT ...)
758        // We need to wrap it in a proper SELECT FROM resources
759        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        // Bind parameters: tenant_id, resource_type, then fragment params
766        let mut stmt = conn
767            .prepare(&sql)
768            .map_err(|e| internal_error(format!("Failed to prepare chain query: {}", e)))?;
769
770        // Build parameter vector for rusqlite
771        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        // Create the chain query builder with registry access
810        let builder = ChainQueryBuilder::new(tenant_id, base_type, self.get_search_registry())
811            .with_param_offset(2); // After ?1 (tenant) and ?2 (resource_type)
812
813        // Build the SQL fragment for reverse chain
814        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        // Execute the query to get matching IDs
825        // The fragment generates: r.id IN (SELECT ...)
826        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        // Build parameter vector for rusqlite
837        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
865// Helper methods for search implementations
866impl SqliteBackend {
867    /// Extract timestamp and ID from a cursor for keyset pagination.
868    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    /// Extract references from a resource for a given search parameter.
883    fn extract_references(&self, content: &serde_json::Value, search_param: &str) -> Vec<String> {
884        let mut refs = Vec::new();
885
886        // Try direct field access (e.g., "subject" -> content.subject)
887        if let Some(value) = content.get(search_param) {
888            self.collect_references_from_value(value, &mut refs);
889        }
890
891        // Try common reference field patterns
892        // Many FHIR references are in fields like "patient", "subject", "performer", etc.
893        // and contain a "reference" sub-field
894        refs
895    }
896
897    /// Recursively collect reference strings from a JSON value.
898    #[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                // Check for "reference" field
903                if let Some(serde_json::Value::String(ref_str)) = obj.get("reference") {
904                    refs.push(ref_str.clone());
905                }
906                // Recurse into object fields
907                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    /// Parse a reference string into (type, id).
921    fn parse_reference(&self, reference: &str) -> Option<(String, String)> {
922        // Handle formats:
923        // - "Patient/123"
924        // - "http://example.com/fhir/Patient/123"
925        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            // For URL format, path is reversed
933            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    /// Fetch a single resource by type and ID.
944    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    /// Verify that a resource contains a reference to one of the given values.
993    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            // Check full reference
1002            if reference_values.iter().any(|v| ref_str.contains(v)) {
1003                return true;
1004            }
1005            // Check just the ID part
1006            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    /// Infer the target resource type for a reference parameter.
1016    #[allow(dead_code)]
1017    fn infer_target_type(&self, _base_type: &str, reference_param: &str) -> String {
1018        // This is a simplified mapping - a real implementation would use
1019        // search parameter definitions from the FHIR specification
1020        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                // Default: capitalize first letter
1029                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    /// Find resources matching a simple field value search using the search index.
1039    #[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        // Use the pre-computed search_index table instead
1049        // This is consistent with our PrecomputedIndex strategy
1050
1051        // Handle token format (system|code or just code)
1052        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                // system|code format
1056                (
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                // |code format (no system)
1065                (
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        // Query the search_index table for matching resources
1079        // Search across string, token code, and reference values
1080        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    /// Get all resources of a type for a tenant.
1112    #[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        // Create some resources
1206        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    // ========================================================================
1277    // Cursor Pagination Tests
1278    // ========================================================================
1279
1280    #[tokio::test]
1281    async fn test_cursor_pagination_basic() {
1282        let backend = create_test_backend();
1283        let tenant = create_test_tenant();
1284
1285        // Create 5 resources
1286        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        // First page with limit of 2
1299        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        // Second page using cursor
1307        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        // Third page (last)
1318        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        // Verify no overlapping IDs
1329        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        // Create 3 resources
1348        for _ in 0..3 {
1349            backend
1350                .create(&tenant, "Patient", json!({}), FhirVersion::default())
1351                .await
1352                .unwrap();
1353        }
1354
1355        // Request more than available
1356        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    // ========================================================================
1378    // MultiTypeSearchProvider Tests
1379    // ========================================================================
1380
1381    #[tokio::test]
1382    async fn test_search_multi_all_types() {
1383        let backend = create_test_backend();
1384        let tenant = create_test_tenant();
1385
1386        // Create different resource types
1387        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        // Search all types (empty list)
1405        let query = SearchQuery::new("Patient"); // Type in query doesn't matter for multi
1406        let result = backend.search_multi(&tenant, &[], &query).await.unwrap();
1407
1408        // Should find all 4 resources
1409        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        // Create different resource types
1418        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        // Search only Patient and Observation
1436        let query = SearchQuery::new("Patient");
1437        let result = backend
1438            .search_multi(&tenant, &["Patient", "Observation"], &query)
1439            .await
1440            .unwrap();
1441
1442        // Should find 3 resources
1443        assert_eq!(result.resources.items.len(), 3);
1444
1445        // Verify types
1446        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    // ========================================================================
1488    // IncludeProvider Tests
1489    // ========================================================================
1490
1491    #[tokio::test]
1492    async fn test_resolve_includes_basic() {
1493        let backend = create_test_backend();
1494        let tenant = create_test_tenant();
1495
1496        // Create a patient
1497        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        // Create an observation that references the patient
1508        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        // Resolve includes for the observation
1523        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        // Should include the patient
1537        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        // Create resources
1548        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        // Include only Patient references
1582        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        // Create patient in tenant 1
1629        backend
1630            .create(
1631                &tenant1,
1632                "Patient",
1633                json!({"id": "p1"}),
1634                FhirVersion::default(),
1635            )
1636            .await
1637            .unwrap();
1638
1639        // Create observation in tenant 2 that "references" patient in tenant 1
1640        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        // Should NOT include the patient from tenant 1
1662        let included = backend
1663            .resolve_includes(&tenant2, &[observation], &[include])
1664            .await
1665            .unwrap();
1666
1667        assert!(included.is_empty());
1668    }
1669
1670    // ========================================================================
1671    // RevincludeProvider Tests
1672    // ========================================================================
1673
1674    #[tokio::test]
1675    async fn test_resolve_revincludes_basic() {
1676        let backend = create_test_backend();
1677        let tenant = create_test_tenant();
1678
1679        // Create a patient
1680        let patient = backend
1681            .create(
1682                &tenant,
1683                "Patient",
1684                json!({"id": "p1"}),
1685                FhirVersion::default(),
1686            )
1687            .await
1688            .unwrap();
1689
1690        // Create observations that reference the patient
1691        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        // Also create an observation for a different patient
1717        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        // Should include 2 observations
1744        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        // No observations exist
1775        let included = backend
1776            .resolve_revincludes(&tenant, &[patient], &[revinclude])
1777            .await
1778            .unwrap();
1779
1780        assert!(included.is_empty());
1781    }
1782
1783    // ========================================================================
1784    // ChainedSearchProvider Tests
1785    // ========================================================================
1786
1787    #[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        // Create patients
1794        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        // Create observations
1814        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        // Manually insert search index entries since FHIRPath extraction
1840        // may not fully populate them due to unsupported functions
1841        {
1842            let conn = backend.get_connection().unwrap();
1843            // Insert patient names
1844            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            // Insert observation subject references
1855            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        // Find observations where patient.name contains "Smith"
1868        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        // Create patient
1884        backend
1885            .create(
1886                &tenant,
1887                "Patient",
1888                json!({"id": "p1", "name": [{"family": "Smith"}]}),
1889                FhirVersion::default(),
1890            )
1891            .await
1892            .unwrap();
1893
1894        // Create observation
1895        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        // Manually insert search index entries
1909        {
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        // Search for non-existent name
1924        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        // Create patients
1939        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        // Create observations with codes
1959        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        // Manually insert search index entries since FHIRPath extraction
1987        // may not fully populate them due to unsupported functions
1988        {
1989            let conn = backend.get_connection().unwrap();
1990            // Insert subject references for observations
1991            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            // Insert code tokens for observations
2002            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        // _has:Observation:subject:code=8867-4
2015        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        // Should find p1 (referenced by observation with code 8867-4)
2028        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        // Test 3-level chain: Observation?subject.organization.name=Hospital
2035        let backend = create_test_backend();
2036        let tenant = create_test_tenant();
2037        let tenant_id = tenant.tenant_id().as_str();
2038
2039        // Create organization
2040        backend
2041            .create(
2042                &tenant,
2043                "Organization",
2044                json!({"id": "org1", "name": "General Hospital"}),
2045                FhirVersion::default(),
2046            )
2047            .await
2048            .unwrap();
2049
2050        // Create patient linked to organization
2051        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        // Create observation linked to patient
2062        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        // Manually insert search index entries
2073        {
2074            let conn = backend.get_connection().unwrap();
2075            // Organization name
2076            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            // Patient -> Organization reference
2082            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            // Observation -> Patient reference
2088            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        // Find observations where patient's organization name contains "Hospital"
2096        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        // Test chain with explicit type: Observation?subject:Patient.name=Smith
2113        let backend = create_test_backend();
2114        let tenant = create_test_tenant();
2115        let tenant_id = tenant.tenant_id().as_str();
2116
2117        // Create patient
2118        backend
2119            .create(
2120                &tenant,
2121                "Patient",
2122                json!({"id": "p1", "name": [{"family": "Smith"}]}),
2123                FhirVersion::default(),
2124            )
2125            .await
2126            .unwrap();
2127
2128        // Create observation
2129        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        // Manually insert search index entries
2140        {
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        // Use explicit type modifier
2155        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        // Test that invalid chain parameters return an error
2167        let backend = create_test_backend();
2168        let tenant = create_test_tenant();
2169
2170        // Try a chain with non-existent parameter
2171        let result = backend
2172            .resolve_chain(&tenant, "Observation", "invalid.param", "value")
2173            .await;
2174
2175        // Should return an error due to unknown parameter
2176        assert!(result.is_err());
2177    }
2178
2179    // ========================================================================
2180    // Helper Method Tests
2181    // ========================================================================
2182
2183    #[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        // Unknown param - capitalize first letter
2220        assert_eq!(backend.infer_target_type("Observation", "custom"), "Custom");
2221    }
2222
2223    // ========================================================================
2224    // Token Search with system|code Tests
2225    // ========================================================================
2226
2227    #[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        // Create two DocumentReferences with different type codes
2234        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        // Insert token search index entries for both documents
2279        {
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        // Search with system|code: should find only doc1
2294        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        // Create a DocumentReference
2321        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        // Insert token search index entry
2342        {
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        // Search with code only (no system): should still find doc1
2352        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        // Create a DocumentReference
2379        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        // Insert token with loinc.org system
2394        {
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        // Search with wrong system: should return 0
2404        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        // Create two DocumentReferences for different patients
2430        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        // Insert search index entries for both
2467        {
2468            let conn = backend.get_connection().unwrap();
2469            // Type tokens
2470            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            // Patient references
2481            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        // Search with both patient AND type (system|code)
2494        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}