helios_sof/
lib.rs

1//! # SQL-on-FHIR Implementation
2//!
3//! This crate provides a complete implementation of the [SQL-on-FHIR
4//! specification](https://sql-on-fhir.org/ig/latest),
5//! enabling the transformation of FHIR resources into tabular data using declarative
6//! ViewDefinitions. It supports all major FHIR versions (R4, R4B, R5, R6) through
7//! a version-agnostic abstraction layer.
8
9//!
10//! There are three consumers of this crate:
11//! - [sof_cli](../sof_cli/index.html) - A command-line interface for the SQL-on-FHIR implementation,
12//!   allowing users to execute ViewDefinition transformations on FHIR Bundle resources
13//!   and output the results in various formats.
14//! - [sof_server](../sof_server/index.html) - A stateless HTTP server implementation for the SQL-on-FHIR specification,
15//!   enabling HTTP-based access to ViewDefinition transformation capabilities.
16//! - [hfs](../hfs/index.html) - The full featured Helios FHIR Server.
17//!
18//! ## Architecture
19//!
20//! The SOF crate is organized around these key components:
21//! - **Version-agnostic enums** ([`SofViewDefinition`], [`SofBundle`]): Multi-version containers
22//! - **Processing engine** ([`run_view_definition`]): Core transformation logic
23//! - **Output formats** ([`ContentType`]): Support for CSV, JSON, NDJSON, and Parquet
24//! - **Trait abstractions** ([`ViewDefinitionTrait`], [`BundleTrait`]): Version independence
25//!
26//! ## Key Features
27//!
28//! - **Multi-version FHIR support**: Works with R4, R4B, R5, and R6 resources
29//! - **FHIRPath evaluation**: Complex path expressions for data extraction
30//! - **forEach iteration**: Supports flattening of nested FHIR structures
31//! - **unionAll operations**: Combines multiple select statements
32//! - **Collection handling**: Proper array serialization for multi-valued fields
33//! - **Output formats**: CSV (with/without headers), JSON, NDJSON, Parquet support
34//!
35//! ## Usage Example
36//!
37//! ```rust
38//! use helios_sof::{SofViewDefinition, SofBundle, ContentType, run_view_definition};
39//! use helios_fhir::FhirVersion;
40//!
41//! # #[cfg(feature = "R4")]
42//! # {
43//! // Parse a ViewDefinition and Bundle from JSON
44//! let view_definition_json = r#"{
45//!     "resourceType": "ViewDefinition",
46//!     "status": "active",
47//!     "resource": "Patient",
48//!     "select": [{
49//!         "column": [{
50//!             "name": "id",
51//!             "path": "id"
52//!         }, {
53//!             "name": "name",
54//!             "path": "name.family"
55//!         }]
56//!     }]
57//! }"#;
58//!
59//! let bundle_json = r#"{
60//!     "resourceType": "Bundle",
61//!     "type": "collection",
62//!     "entry": [{
63//!         "resource": {
64//!             "resourceType": "Patient",
65//!             "id": "example",
66//!             "name": [{
67//!                 "family": "Doe",
68//!                 "given": ["John"]
69//!             }]
70//!         }
71//!     }]
72//! }"#;
73//!
74//! let view_definition: helios_fhir::r4::ViewDefinition = serde_json::from_str(view_definition_json)?;
75//! let bundle: helios_fhir::r4::Bundle = serde_json::from_str(bundle_json)?;
76//!
77//! // Wrap in version-agnostic containers
78//! let sof_view = SofViewDefinition::R4(view_definition);
79//! let sof_bundle = SofBundle::R4(bundle);
80//!
81//! // Transform to CSV with headers
82//! let csv_output = run_view_definition(
83//!     sof_view,
84//!     sof_bundle,
85//!     ContentType::CsvWithHeader
86//! )?;
87//!
88//! // Check the CSV output
89//! let csv_string = String::from_utf8(csv_output)?;
90//! assert!(csv_string.contains("id,name"));
91//! // CSV values are quoted
92//! assert!(csv_string.contains("example") && csv_string.contains("Doe"));
93//! # }
94//! # Ok::<(), Box<dyn std::error::Error>>(())
95//! ```
96//!
97//! ## Advanced Features
98//!
99//! ### forEach Iteration
100//!
101//! ViewDefinitions can iterate over collections using `forEach` and `forEachOrNull`:
102//!
103//! ```json
104//! {
105//!   "select": [{
106//!     "forEach": "name",
107//!     "column": [{
108//!       "name": "family_name",
109//!       "path": "family"
110//!     }]
111//!   }]
112//! }
113//! ```
114//!
115//! ### Constants and Variables
116//!
117//! Define reusable values in ViewDefinitions:
118//!
119//! ```json
120//! {
121//!   "constant": [{
122//!     "name": "system",
123//!     "valueString": "http://loinc.org"
124//!   }],
125//!   "select": [{
126//!     "where": [{
127//!       "path": "code.coding.system = %system"
128//!     }]
129//!   }]
130//! }
131//! ```
132//!
133//! ### Where Clauses
134//!
135//! Filter resources using FHIRPath expressions:
136//!
137//! ```json
138//! {
139//!   "where": [{
140//!     "path": "active = true"
141//!   }, {
142//!     "path": "birthDate.exists()"
143//!   }]
144//! }
145//! ```
146//!
147//! ## Error Handling
148//!
149//! The crate provides comprehensive error handling through [`SofError`]:
150//!
151//! ```rust,no_run
152//! use helios_sof::{SofError, SofViewDefinition, SofBundle, ContentType, run_view_definition};
153//!
154//! # let view = SofViewDefinition::R4(helios_fhir::r4::ViewDefinition::default());
155//! # let bundle = SofBundle::R4(helios_fhir::r4::Bundle::default());
156//! # let content_type = ContentType::Json;
157//! match run_view_definition(view, bundle, content_type) {
158//!     Ok(output) => {
159//!         // Process successful transformation
160//!     },
161//!     Err(SofError::InvalidViewDefinition(msg)) => {
162//!         eprintln!("ViewDefinition validation failed: {}", msg);
163//!     },
164//!     Err(SofError::FhirPathError(msg)) => {
165//!         eprintln!("FHIRPath evaluation failed: {}", msg);
166//!     },
167//!     Err(e) => {
168//!         eprintln!("Other error: {}", e);
169//!     }
170//! }
171//! ```
172//! ## Feature Flags
173//!
174//! Enable support for specific FHIR versions:
175//! - `R4`: FHIR 4.0.1 support
176//! - `R4B`: FHIR 4.3.0 support
177//! - `R5`: FHIR 5.0.0 support
178//! - `R6`: FHIR 6.0.0 support
179
180pub mod traits;
181
182use chrono::{DateTime, Utc};
183use helios_fhirpath::{EvaluationContext, EvaluationResult, evaluate_expression};
184use serde::{Deserialize, Serialize};
185use std::collections::HashMap;
186use thiserror::Error;
187use traits::*;
188
189// Re-export commonly used types and traits for easier access
190pub use helios_fhir::FhirVersion;
191pub use traits::{BundleTrait, ResourceTrait, ViewDefinitionTrait};
192
193/// Multi-version ViewDefinition container supporting version-agnostic operations.
194///
195/// This enum provides a unified interface for working with ViewDefinition resources
196/// across different FHIR specification versions. It enables applications to handle
197/// multiple FHIR versions simultaneously while maintaining type safety.
198///
199/// # Supported Versions
200///
201/// - **R4**: FHIR 4.0.1 ViewDefinition (normative)
202/// - **R4B**: FHIR 4.3.0 ViewDefinition (ballot)
203/// - **R5**: FHIR 5.0.0 ViewDefinition (ballot)
204/// - **R6**: FHIR 6.0.0 ViewDefinition (draft)
205///
206/// # Examples
207///
208/// ```rust
209/// use helios_sof::{SofViewDefinition, ContentType};
210/// # #[cfg(feature = "R4")]
211/// use helios_fhir::r4::ViewDefinition;
212///
213/// # #[cfg(feature = "R4")]
214/// # {
215/// // Parse from JSON
216/// let json = r#"{
217///     "resourceType": "ViewDefinition",
218///     "resource": "Patient",
219///     "select": [{
220///         "column": [{
221///             "name": "id",
222///             "path": "id"
223///         }]
224///     }]
225/// }"#;
226///
227/// let view_def: ViewDefinition = serde_json::from_str(json)?;
228/// let sof_view = SofViewDefinition::R4(view_def);
229///
230/// // Check version
231/// assert_eq!(sof_view.version(), helios_fhir::FhirVersion::R4);
232/// # }
233/// # Ok::<(), Box<dyn std::error::Error>>(())
234/// ```
235#[derive(Debug, Clone)]
236pub enum SofViewDefinition {
237    #[cfg(feature = "R4")]
238    R4(helios_fhir::r4::ViewDefinition),
239    #[cfg(feature = "R4B")]
240    R4B(helios_fhir::r4b::ViewDefinition),
241    #[cfg(feature = "R5")]
242    R5(helios_fhir::r5::ViewDefinition),
243    #[cfg(feature = "R6")]
244    R6(helios_fhir::r6::ViewDefinition),
245}
246
247impl SofViewDefinition {
248    /// Returns the FHIR specification version of this ViewDefinition.
249    ///
250    /// This method provides version detection for multi-version applications,
251    /// enabling version-specific processing logic and compatibility checks.
252    ///
253    /// # Returns
254    ///
255    /// The `FhirVersion` enum variant corresponding to this ViewDefinition's specification.
256    ///
257    /// # Examples
258    ///
259    /// ```rust
260    /// use helios_sof::SofViewDefinition;
261    /// use helios_fhir::FhirVersion;
262    ///
263    /// # #[cfg(feature = "R5")]
264    /// # {
265    /// # let view_def = helios_fhir::r5::ViewDefinition::default();
266    /// let sof_view = SofViewDefinition::R5(view_def);
267    /// assert_eq!(sof_view.version(), helios_fhir::FhirVersion::R5);
268    /// # }
269    /// ```
270    pub fn version(&self) -> helios_fhir::FhirVersion {
271        match self {
272            #[cfg(feature = "R4")]
273            SofViewDefinition::R4(_) => helios_fhir::FhirVersion::R4,
274            #[cfg(feature = "R4B")]
275            SofViewDefinition::R4B(_) => helios_fhir::FhirVersion::R4B,
276            #[cfg(feature = "R5")]
277            SofViewDefinition::R5(_) => helios_fhir::FhirVersion::R5,
278            #[cfg(feature = "R6")]
279            SofViewDefinition::R6(_) => helios_fhir::FhirVersion::R6,
280        }
281    }
282}
283
284/// Multi-version Bundle container supporting version-agnostic operations.
285///
286/// This enum provides a unified interface for working with FHIR Bundle resources
287/// across different FHIR specification versions. Bundles contain the actual FHIR
288/// resources that will be processed by ViewDefinitions.
289///
290/// # Supported Versions
291///
292/// - **R4**: FHIR 4.0.1 Bundle (normative)
293/// - **R4B**: FHIR 4.3.0 Bundle (ballot)
294/// - **R5**: FHIR 5.0.0 Bundle (ballot)
295/// - **R6**: FHIR 6.0.0 Bundle (draft)
296///
297/// # Examples
298///
299/// ```rust
300/// use helios_sof::SofBundle;
301/// # #[cfg(feature = "R4")]
302/// use helios_fhir::r4::Bundle;
303///
304/// # #[cfg(feature = "R4")]
305/// # {
306/// // Parse from JSON
307/// let json = r#"{
308///     "resourceType": "Bundle",
309///     "type": "collection",
310///     "entry": [{
311///         "resource": {
312///             "resourceType": "Patient",
313///             "id": "example"
314///         }
315///     }]
316/// }"#;
317///
318/// let bundle: Bundle = serde_json::from_str(json)?;
319/// let sof_bundle = SofBundle::R4(bundle);
320///
321/// // Check version compatibility
322/// assert_eq!(sof_bundle.version(), helios_fhir::FhirVersion::R4);
323/// # }
324/// # Ok::<(), Box<dyn std::error::Error>>(())
325/// ```
326#[derive(Debug, Clone)]
327pub enum SofBundle {
328    #[cfg(feature = "R4")]
329    R4(helios_fhir::r4::Bundle),
330    #[cfg(feature = "R4B")]
331    R4B(helios_fhir::r4b::Bundle),
332    #[cfg(feature = "R5")]
333    R5(helios_fhir::r5::Bundle),
334    #[cfg(feature = "R6")]
335    R6(helios_fhir::r6::Bundle),
336}
337
338impl SofBundle {
339    /// Returns the FHIR specification version of this Bundle.
340    ///
341    /// This method provides version detection for multi-version applications,
342    /// ensuring that ViewDefinitions and Bundles use compatible FHIR versions.
343    ///
344    /// # Returns
345    ///
346    /// The `FhirVersion` enum variant corresponding to this Bundle's specification.
347    ///
348    /// # Examples
349    ///
350    /// ```rust
351    /// use helios_sof::SofBundle;
352    /// use helios_fhir::FhirVersion;
353    ///
354    /// # #[cfg(feature = "R4")]
355    /// # {
356    /// # let bundle = helios_fhir::r4::Bundle::default();
357    /// let sof_bundle = SofBundle::R4(bundle);
358    /// assert_eq!(sof_bundle.version(), helios_fhir::FhirVersion::R4);
359    /// # }
360    /// ```
361    pub fn version(&self) -> helios_fhir::FhirVersion {
362        match self {
363            #[cfg(feature = "R4")]
364            SofBundle::R4(_) => helios_fhir::FhirVersion::R4,
365            #[cfg(feature = "R4B")]
366            SofBundle::R4B(_) => helios_fhir::FhirVersion::R4B,
367            #[cfg(feature = "R5")]
368            SofBundle::R5(_) => helios_fhir::FhirVersion::R5,
369            #[cfg(feature = "R6")]
370            SofBundle::R6(_) => helios_fhir::FhirVersion::R6,
371        }
372    }
373}
374
375/// Multi-version CapabilityStatement container supporting version-agnostic operations.
376///
377/// This enum provides a unified interface for working with CapabilityStatement resources
378/// across different FHIR specification versions. It enables applications to handle
379/// multiple FHIR versions simultaneously while maintaining type safety.
380///
381/// # Supported Versions
382///
383/// - **R4**: FHIR 4.0.1 CapabilityStatement (normative)
384/// - **R4B**: FHIR 4.3.0 CapabilityStatement (ballot)
385/// - **R5**: FHIR 5.0.0 CapabilityStatement (ballot)
386/// - **R6**: FHIR 6.0.0 CapabilityStatement (draft)
387#[derive(Debug, Clone, Serialize, Deserialize)]
388#[serde(untagged)]
389pub enum SofCapabilityStatement {
390    #[cfg(feature = "R4")]
391    R4(helios_fhir::r4::CapabilityStatement),
392    #[cfg(feature = "R4B")]
393    R4B(helios_fhir::r4b::CapabilityStatement),
394    #[cfg(feature = "R5")]
395    R5(helios_fhir::r5::CapabilityStatement),
396    #[cfg(feature = "R6")]
397    R6(helios_fhir::r6::CapabilityStatement),
398}
399
400impl SofCapabilityStatement {
401    /// Returns the FHIR specification version of this CapabilityStatement.
402    pub fn version(&self) -> helios_fhir::FhirVersion {
403        match self {
404            #[cfg(feature = "R4")]
405            SofCapabilityStatement::R4(_) => helios_fhir::FhirVersion::R4,
406            #[cfg(feature = "R4B")]
407            SofCapabilityStatement::R4B(_) => helios_fhir::FhirVersion::R4B,
408            #[cfg(feature = "R5")]
409            SofCapabilityStatement::R5(_) => helios_fhir::FhirVersion::R5,
410            #[cfg(feature = "R6")]
411            SofCapabilityStatement::R6(_) => helios_fhir::FhirVersion::R6,
412        }
413    }
414}
415
416/// Type alias for the version-independent Parameters container.
417///
418/// This alias provides backward compatibility while using the unified
419/// VersionIndependentParameters from the helios_fhir crate.
420pub type SofParameters = helios_fhir::VersionIndependentParameters;
421
422/// Comprehensive error type for SQL-on-FHIR operations.
423///
424/// This enum covers all possible error conditions that can occur during
425/// ViewDefinition processing, from validation failures to output formatting issues.
426/// Each variant provides specific context about the error to aid in debugging.
427///
428/// # Error Categories
429///
430/// - **Validation**: ViewDefinition structure and logic validation
431/// - **Evaluation**: FHIRPath expression evaluation failures
432/// - **I/O**: File and serialization operations
433/// - **Format**: Output format conversion issues
434///
435/// # Examples
436///
437/// ```rust,no_run
438/// use helios_sof::{SofError, SofViewDefinition, SofBundle, ContentType, run_view_definition};
439///
440/// # let view = SofViewDefinition::R4(helios_fhir::r4::ViewDefinition::default());
441/// # let bundle = SofBundle::R4(helios_fhir::r4::Bundle::default());
442/// # let content_type = ContentType::Json;
443/// match run_view_definition(view, bundle, content_type) {
444///     Ok(output) => {
445///         println!("Transformation successful");
446///     },
447///     Err(SofError::InvalidViewDefinition(msg)) => {
448///         eprintln!("ViewDefinition validation failed: {}", msg);
449///     },
450///     Err(SofError::FhirPathError(msg)) => {
451///         eprintln!("FHIRPath evaluation error: {}", msg);
452///     },
453///     Err(SofError::UnsupportedContentType(format)) => {
454///         eprintln!("Unsupported output format: {}", format);
455///     },
456///     Err(e) => {
457///         eprintln!("Other error: {}", e);
458///     }
459/// }
460/// ```
461#[derive(Debug, Error)]
462pub enum SofError {
463    /// ViewDefinition structure or logic validation failed.
464    ///
465    /// This error occurs when a ViewDefinition contains invalid or inconsistent
466    /// configuration, such as missing required fields, invalid FHIRPath expressions,
467    /// or incompatible select/unionAll structures.
468    #[error("Invalid ViewDefinition: {0}")]
469    InvalidViewDefinition(String),
470
471    /// FHIRPath expression evaluation failed.
472    ///
473    /// This error occurs when a FHIRPath expression in a ViewDefinition cannot
474    /// be evaluated, either due to syntax errors or runtime evaluation issues.
475    #[error("FHIRPath evaluation error: {0}")]
476    FhirPathError(String),
477
478    /// JSON serialization/deserialization failed.
479    ///
480    /// This error occurs when parsing input JSON or serializing output data fails,
481    /// typically due to malformed JSON or incompatible data structures.
482    #[error("Serialization error: {0}")]
483    SerializationError(#[from] serde_json::Error),
484
485    /// CSV processing failed.
486    ///
487    /// This error occurs during CSV output generation, such as when writing
488    /// headers or data rows to the CSV format.
489    #[error("CSV error: {0}")]
490    CsvError(#[from] csv::Error),
491
492    /// File I/O operation failed.
493    ///
494    /// This error occurs when reading input files or writing output files fails,
495    /// typically due to permission issues or missing files.
496    #[error("IO error: {0}")]
497    IoError(#[from] std::io::Error),
498
499    /// Unsupported output content type requested.
500    ///
501    /// This error occurs when an invalid or unimplemented content type is
502    /// specified for output formatting.
503    #[error("Unsupported content type: {0}")]
504    UnsupportedContentType(String),
505
506    /// CSV writer internal error.
507    ///
508    /// This error occurs when the CSV writer encounters an internal issue
509    /// that prevents successful output generation.
510    #[error("CSV writer error: {0}")]
511    CsvWriterError(String),
512}
513
514/// Supported output content types for ViewDefinition transformations.
515///
516/// This enum defines the available output formats for transformed FHIR data.
517/// Each format has specific characteristics and use cases for different
518/// integration scenarios.
519///
520/// # Format Descriptions
521///
522/// - **CSV**: Comma-separated values without headers
523/// - **CSV with Headers**: Comma-separated values with column headers
524/// - **JSON**: Pretty-printed JSON array of objects
525/// - **NDJSON**: Newline-delimited JSON (one object per line)
526/// - **Parquet**: Apache Parquet columnar format (planned)
527///
528/// # Examples
529///
530/// ```rust
531/// use helios_sof::ContentType;
532///
533/// // Parse from string
534/// let csv_type = ContentType::from_string("text/csv")?;
535/// assert_eq!(csv_type, ContentType::CsvWithHeader);  // Default includes headers
536///
537/// let json_type = ContentType::from_string("application/json")?;
538/// assert_eq!(json_type, ContentType::Json);
539///
540/// // CSV without headers
541/// let csv_no_headers = ContentType::from_string("text/csv;header=false")?;
542/// assert_eq!(csv_no_headers, ContentType::Csv);
543/// # Ok::<(), helios_sof::SofError>(())
544/// ```
545#[derive(Debug, Clone, Copy, PartialEq, Eq)]
546pub enum ContentType {
547    /// Comma-separated values format without headers
548    Csv,
549    /// Comma-separated values format with column headers
550    CsvWithHeader,
551    /// Pretty-printed JSON array format
552    Json,
553    /// Newline-delimited JSON format (NDJSON)
554    NdJson,
555    /// Apache Parquet columnar format (not yet implemented)
556    Parquet,
557}
558
559impl ContentType {
560    /// Parse a content type from its MIME type string representation.
561    ///
562    /// This method converts standard MIME type strings to the corresponding
563    /// ContentType enum variants. It supports the SQL-on-FHIR specification's
564    /// recommended content types.
565    ///
566    /// # Supported MIME Types
567    ///
568    /// - `"text/csv"` → [`ContentType::Csv`]
569    /// - `"text/csv"` → [`ContentType::CsvWithHeader`] (default: headers included)
570    /// - `"text/csv;header=true"` → [`ContentType::CsvWithHeader`]
571    /// - `"text/csv;header=false"` → [`ContentType::Csv`]
572    /// - `"application/json"` → [`ContentType::Json`]
573    /// - `"application/ndjson"` → [`ContentType::NdJson`]
574    /// - `"application/parquet"` → [`ContentType::Parquet`]
575    ///
576    /// # Arguments
577    ///
578    /// * `s` - The MIME type string to parse
579    ///
580    /// # Returns
581    ///
582    /// * `Ok(ContentType)` - Successfully parsed content type
583    /// * `Err(SofError::UnsupportedContentType)` - Unknown or unsupported MIME type
584    ///
585    /// # Examples
586    ///
587    /// ```rust
588    /// use helios_sof::ContentType;
589    ///
590    /// // Shortened format names
591    /// let csv = ContentType::from_string("csv")?;
592    /// assert_eq!(csv, ContentType::CsvWithHeader);
593    ///
594    /// let json = ContentType::from_string("json")?;
595    /// assert_eq!(json, ContentType::Json);
596    ///
597    /// let ndjson = ContentType::from_string("ndjson")?;
598    /// assert_eq!(ndjson, ContentType::NdJson);
599    ///
600    /// // Full MIME types still supported
601    /// let csv_mime = ContentType::from_string("text/csv")?;
602    /// assert_eq!(csv_mime, ContentType::CsvWithHeader);
603    ///
604    /// // CSV with headers explicitly
605    /// let csv_headers = ContentType::from_string("text/csv;header=true")?;
606    /// assert_eq!(csv_headers, ContentType::CsvWithHeader);
607    ///
608    /// // CSV without headers
609    /// let csv_no_headers = ContentType::from_string("text/csv;header=false")?;
610    /// assert_eq!(csv_no_headers, ContentType::Csv);
611    ///
612    /// // JSON format
613    /// let json_mime = ContentType::from_string("application/json")?;
614    /// assert_eq!(json_mime, ContentType::Json);
615    ///
616    /// // Error for unsupported type
617    /// assert!(ContentType::from_string("text/plain").is_err());
618    /// # Ok::<(), helios_sof::SofError>(())
619    /// ```
620    pub fn from_string(s: &str) -> Result<Self, SofError> {
621        match s {
622            // Shortened format names
623            "csv" => Ok(ContentType::CsvWithHeader),
624            "json" => Ok(ContentType::Json),
625            "ndjson" => Ok(ContentType::NdJson),
626            "parquet" => Ok(ContentType::Parquet),
627            // Full MIME types (for Accept header compatibility)
628            "text/csv;header=false" => Ok(ContentType::Csv),
629            "text/csv" | "text/csv;header=true" => Ok(ContentType::CsvWithHeader),
630            "application/json" => Ok(ContentType::Json),
631            "application/ndjson" => Ok(ContentType::NdJson),
632            "application/parquet" => Ok(ContentType::Parquet),
633            _ => Err(SofError::UnsupportedContentType(s.to_string())),
634        }
635    }
636}
637
638/// Returns the FHIR version string for the newest enabled version.
639///
640/// This function provides the version string that should be used in CapabilityStatements
641/// and other FHIR resources that need to specify their version.
642pub fn get_fhir_version_string() -> &'static str {
643    let newest_version = get_newest_enabled_fhir_version();
644
645    match newest_version {
646        #[cfg(feature = "R4")]
647        helios_fhir::FhirVersion::R4 => "4.0.1",
648        #[cfg(feature = "R4B")]
649        helios_fhir::FhirVersion::R4B => "4.3.0",
650        #[cfg(feature = "R5")]
651        helios_fhir::FhirVersion::R5 => "5.0.0",
652        #[cfg(feature = "R6")]
653        helios_fhir::FhirVersion::R6 => "6.0.0",
654    }
655}
656
657/// Returns the newest FHIR version that is enabled at compile time.
658///
659/// This function uses compile-time feature detection to determine which FHIR
660/// version should be used when multiple versions are enabled. The priority order
661/// is: R6 > R5 > R4B > R4, where newer versions take precedence.
662///
663/// # Examples
664///
665/// ```rust
666/// use helios_sof::{get_newest_enabled_fhir_version, FhirVersion};
667///
668/// # #[cfg(any(feature = "R4", feature = "R4B", feature = "R5", feature = "R6"))]
669/// # {
670/// let version = get_newest_enabled_fhir_version();
671/// // If R5 and R4 are both enabled, this returns R5
672/// # }
673/// ```
674///
675/// # Panics
676///
677/// This function will panic at compile time if no FHIR version features are enabled.
678pub fn get_newest_enabled_fhir_version() -> helios_fhir::FhirVersion {
679    #[cfg(feature = "R6")]
680    return helios_fhir::FhirVersion::R6;
681
682    #[cfg(all(feature = "R5", not(feature = "R6")))]
683    return helios_fhir::FhirVersion::R5;
684
685    #[cfg(all(feature = "R4B", not(feature = "R5"), not(feature = "R6")))]
686    return helios_fhir::FhirVersion::R4B;
687
688    #[cfg(all(
689        feature = "R4",
690        not(feature = "R4B"),
691        not(feature = "R5"),
692        not(feature = "R6")
693    ))]
694    return helios_fhir::FhirVersion::R4;
695
696    #[cfg(not(any(feature = "R4", feature = "R4B", feature = "R5", feature = "R6")))]
697    panic!("At least one FHIR version feature must be enabled");
698}
699
700/// A single row of processed tabular data from ViewDefinition transformation.
701///
702/// This struct represents one row in the output table, containing values for
703/// each column defined in the ViewDefinition. Values are stored as optional
704/// JSON values to handle nullable fields and diverse FHIR data types.
705///
706/// # Structure
707///
708/// Each `ProcessedRow` contains a vector of optional JSON values, where:
709/// - `Some(value)` represents a non-null column value
710/// - `None` represents a null/missing column value
711/// - The order matches the column order in [`ProcessedResult::columns`]
712///
713/// # Examples
714///
715/// ```rust
716/// use helios_sof::ProcessedRow;
717/// use serde_json::Value;
718///
719/// let row = ProcessedRow {
720///     values: vec![
721///         Some(Value::String("patient-123".to_string())),
722///         Some(Value::String("Doe".to_string())),
723///         None, // Missing birth date
724///         Some(Value::Bool(true)),
725///     ]
726/// };
727/// ```
728#[derive(Debug, Clone, Serialize, Deserialize)]
729pub struct ProcessedRow {
730    /// Column values for this row, ordered according to ProcessedResult::columns
731    pub values: Vec<Option<serde_json::Value>>,
732}
733
734/// Complete result of ViewDefinition transformation containing columns and data rows.
735///
736/// This struct represents the tabular output from processing a ViewDefinition
737/// against a Bundle of FHIR resources. It contains both the column definitions
738/// and the actual data rows in a format ready for serialization to various
739/// output formats.
740///
741/// # Structure
742///
743/// - [`columns`](Self::columns): Ordered list of column names from the ViewDefinition
744/// - [`rows`](Self::rows): Data rows where each row contains values in column order
745///
746/// # Examples
747///
748/// ```rust
749/// use helios_sof::{ProcessedResult, ProcessedRow};
750/// use serde_json::Value;
751///
752/// let result = ProcessedResult {
753///     columns: vec![
754///         "patient_id".to_string(),
755///         "family_name".to_string(),
756///         "given_name".to_string(),
757///     ],
758///     rows: vec![
759///         ProcessedRow {
760///             values: vec![
761///                 Some(Value::String("patient-1".to_string())),
762///                 Some(Value::String("Smith".to_string())),
763///                 Some(Value::String("John".to_string())),
764///             ]
765///         },
766///         ProcessedRow {
767///             values: vec![
768///                 Some(Value::String("patient-2".to_string())),
769///                 Some(Value::String("Doe".to_string())),
770///                 None, // Missing given name
771///             ]
772///         },
773///     ]
774/// };
775///
776/// assert_eq!(result.columns.len(), 3);
777/// assert_eq!(result.rows.len(), 2);
778/// ```
779#[derive(Debug, Clone, Serialize, Deserialize)]
780pub struct ProcessedResult {
781    /// Ordered list of column names as defined in the ViewDefinition
782    pub columns: Vec<String>,
783    /// Data rows containing values for each column
784    pub rows: Vec<ProcessedRow>,
785}
786
787/// Execute a SQL-on-FHIR ViewDefinition transformation on a FHIR Bundle.
788///
789/// This is the main entry point for SQL-on-FHIR transformations. It processes
790/// a ViewDefinition against a Bundle of FHIR resources and produces output in
791/// the specified format. The function handles version compatibility, validation,
792/// FHIRPath evaluation, and output formatting.
793///
794/// # Arguments
795///
796/// * `view_definition` - The ViewDefinition containing transformation logic
797/// * `bundle` - The Bundle containing FHIR resources to process
798/// * `content_type` - The desired output format
799///
800/// # Returns
801///
802/// * `Ok(Vec<u8>)` - Formatted output bytes ready for writing to file or stdout
803/// * `Err(SofError)` - Detailed error information about what went wrong
804///
805/// # Validation
806///
807/// The function performs comprehensive validation:
808/// - FHIR version compatibility between ViewDefinition and Bundle
809/// - ViewDefinition structure and logic validation
810/// - FHIRPath expression syntax and evaluation
811/// - Output format compatibility
812///
813/// # Examples
814///
815/// ```rust
816/// use helios_sof::{SofViewDefinition, SofBundle, ContentType, run_view_definition};
817///
818/// # #[cfg(feature = "R4")]
819/// # {
820/// // Create a simple ViewDefinition
821/// let view_json = serde_json::json!({
822///     "resourceType": "ViewDefinition",
823///     "status": "active",
824///     "resource": "Patient",
825///     "select": [{
826///         "column": [{
827///             "name": "id",
828///             "path": "id"
829///         }]
830///     }]
831/// });
832/// let view_def: helios_fhir::r4::ViewDefinition = serde_json::from_value(view_json)?;
833///
834/// // Create a simple Bundle
835/// let bundle_json = serde_json::json!({
836///     "resourceType": "Bundle",
837///     "type": "collection",
838///     "entry": []
839/// });
840/// let bundle: helios_fhir::r4::Bundle = serde_json::from_value(bundle_json)?;
841///
842/// let sof_view = SofViewDefinition::R4(view_def);
843/// let sof_bundle = SofBundle::R4(bundle);
844///
845/// // Generate CSV with headers
846/// let csv_output = run_view_definition(
847///     sof_view,
848///     sof_bundle,
849///     ContentType::CsvWithHeader
850/// )?;
851///
852/// // Write to file or stdout
853/// std::fs::write("output.csv", csv_output)?;
854/// # }
855/// # Ok::<(), Box<dyn std::error::Error>>(())
856/// ```
857///
858/// # Error Handling
859///
860/// Common error scenarios:
861///
862/// ```rust,no_run
863/// use helios_sof::{SofError, SofViewDefinition, SofBundle, ContentType, run_view_definition};
864///
865/// # let view = SofViewDefinition::R4(helios_fhir::r4::ViewDefinition::default());
866/// # let bundle = SofBundle::R4(helios_fhir::r4::Bundle::default());
867/// # let content_type = ContentType::Json;
868/// match run_view_definition(view, bundle, content_type) {
869///     Ok(output) => {
870///         println!("Success: {} bytes generated", output.len());
871///     },
872///     Err(SofError::InvalidViewDefinition(msg)) => {
873///         eprintln!("ViewDefinition error: {}", msg);
874///     },
875///     Err(SofError::FhirPathError(msg)) => {
876///         eprintln!("FHIRPath error: {}", msg);
877///     },
878///     Err(e) => {
879///         eprintln!("Other error: {}", e);
880///     }
881/// }
882/// ```
883pub fn run_view_definition(
884    view_definition: SofViewDefinition,
885    bundle: SofBundle,
886    content_type: ContentType,
887) -> Result<Vec<u8>, SofError> {
888    run_view_definition_with_options(view_definition, bundle, content_type, RunOptions::default())
889}
890
891/// Options for filtering and controlling ViewDefinition execution
892#[derive(Debug, Clone, Default)]
893pub struct RunOptions {
894    /// Filter resources modified after this time
895    pub since: Option<DateTime<Utc>>,
896    /// Limit the number of results
897    pub limit: Option<usize>,
898    /// Page number for pagination (1-based)
899    pub page: Option<usize>,
900}
901
902/// Execute a ViewDefinition transformation with additional filtering options.
903///
904/// This function extends the basic `run_view_definition` with support for:
905/// - Filtering resources by modification time (`since`)
906/// - Limiting results (`limit`)
907/// - Pagination (`page`)
908///
909/// # Arguments
910///
911/// * `view_definition` - The ViewDefinition to execute
912/// * `bundle` - The Bundle containing resources to transform
913/// * `content_type` - Desired output format
914/// * `options` - Additional filtering and control options
915///
916/// # Returns
917///
918/// The transformed data in the requested format, with filtering applied.
919pub fn run_view_definition_with_options(
920    view_definition: SofViewDefinition,
921    bundle: SofBundle,
922    content_type: ContentType,
923    options: RunOptions,
924) -> Result<Vec<u8>, SofError> {
925    // Filter bundle resources by since parameter before processing
926    let filtered_bundle = if let Some(since) = options.since {
927        filter_bundle_by_since(bundle, since)?
928    } else {
929        bundle
930    };
931
932    // Process the ViewDefinition to generate tabular data
933    let processed_result = process_view_definition(view_definition, filtered_bundle)?;
934
935    // Apply pagination if needed
936    let processed_result = if options.limit.is_some() || options.page.is_some() {
937        apply_pagination_to_result(processed_result, options.limit, options.page)?
938    } else {
939        processed_result
940    };
941
942    // Format the result according to the requested content type
943    format_output(processed_result, content_type)
944}
945
946fn process_view_definition(
947    view_definition: SofViewDefinition,
948    bundle: SofBundle,
949) -> Result<ProcessedResult, SofError> {
950    // Ensure both resources use the same FHIR version
951    if view_definition.version() != bundle.version() {
952        return Err(SofError::InvalidViewDefinition(
953            "ViewDefinition and Bundle must use the same FHIR version".to_string(),
954        ));
955    }
956
957    match (view_definition, bundle) {
958        #[cfg(feature = "R4")]
959        (SofViewDefinition::R4(vd), SofBundle::R4(bundle)) => {
960            process_view_definition_generic(vd, bundle)
961        }
962        #[cfg(feature = "R4B")]
963        (SofViewDefinition::R4B(vd), SofBundle::R4B(bundle)) => {
964            process_view_definition_generic(vd, bundle)
965        }
966        #[cfg(feature = "R5")]
967        (SofViewDefinition::R5(vd), SofBundle::R5(bundle)) => {
968            process_view_definition_generic(vd, bundle)
969        }
970        #[cfg(feature = "R6")]
971        (SofViewDefinition::R6(vd), SofBundle::R6(bundle)) => {
972            process_view_definition_generic(vd, bundle)
973        }
974        // This case should never happen due to the version check above,
975        // but is needed for exhaustive pattern matching when multiple features are enabled
976        #[cfg(any(
977            all(feature = "R4", any(feature = "R4B", feature = "R5", feature = "R6")),
978            all(feature = "R4B", any(feature = "R5", feature = "R6")),
979            all(feature = "R5", feature = "R6")
980        ))]
981        _ => {
982            unreachable!("Version mismatch should have been caught by the version check above")
983        }
984    }
985}
986
987// Generic version-agnostic constant extraction
988fn extract_view_definition_constants<VD: ViewDefinitionTrait>(
989    view_definition: &VD,
990) -> Result<HashMap<String, EvaluationResult>, SofError> {
991    let mut variables = HashMap::new();
992
993    if let Some(constants) = view_definition.constants() {
994        for constant in constants {
995            let name = constant
996                .name()
997                .ok_or_else(|| {
998                    SofError::InvalidViewDefinition("Constant name is required".to_string())
999                })?
1000                .to_string();
1001
1002            let eval_result = constant.to_evaluation_result()?;
1003            // Constants are referenced with % prefix in FHIRPath expressions
1004            variables.insert(format!("%{}", name), eval_result);
1005        }
1006    }
1007
1008    Ok(variables)
1009}
1010
1011// Generic version-agnostic ViewDefinition processing
1012fn process_view_definition_generic<VD, B>(
1013    view_definition: VD,
1014    bundle: B,
1015) -> Result<ProcessedResult, SofError>
1016where
1017    VD: ViewDefinitionTrait,
1018    B: BundleTrait,
1019    B::Resource: ResourceTrait,
1020{
1021    validate_view_definition(&view_definition)?;
1022
1023    // Step 1: Extract constants/variables from ViewDefinition
1024    let variables = extract_view_definition_constants(&view_definition)?;
1025
1026    // Step 2: Filter resources by type and profile
1027    let target_resource_type = view_definition
1028        .resource()
1029        .ok_or_else(|| SofError::InvalidViewDefinition("Resource type is required".to_string()))?;
1030
1031    let filtered_resources = filter_resources(&bundle, target_resource_type)?;
1032
1033    // Step 3: Apply where clauses to filter resources
1034    let filtered_resources = apply_where_clauses(
1035        filtered_resources,
1036        view_definition.where_clauses(),
1037        &variables,
1038    )?;
1039
1040    // Step 4: Process all select clauses to generate rows with forEach support
1041    let select_clauses = view_definition.select().ok_or_else(|| {
1042        SofError::InvalidViewDefinition("At least one select clause is required".to_string())
1043    })?;
1044
1045    // Generate rows for each resource using the forEach-aware approach
1046    let (all_columns, rows) =
1047        generate_rows_from_selects(&filtered_resources, select_clauses, &variables)?;
1048
1049    Ok(ProcessedResult {
1050        columns: all_columns,
1051        rows,
1052    })
1053}
1054
1055// Generic version-agnostic validation
1056fn validate_view_definition<VD: ViewDefinitionTrait>(view_def: &VD) -> Result<(), SofError> {
1057    // Basic validation
1058    if view_def.resource().is_none_or(|s| s.is_empty()) {
1059        return Err(SofError::InvalidViewDefinition(
1060            "ViewDefinition must specify a resource type".to_string(),
1061        ));
1062    }
1063
1064    if view_def.select().is_none_or(|s| s.is_empty()) {
1065        return Err(SofError::InvalidViewDefinition(
1066            "ViewDefinition must have at least one select".to_string(),
1067        ));
1068    }
1069
1070    // Validate where clauses
1071    if let Some(where_clauses) = view_def.where_clauses() {
1072        validate_where_clauses(where_clauses)?;
1073    }
1074
1075    // Validate selects
1076    if let Some(selects) = view_def.select() {
1077        for select in selects {
1078            validate_select(select)?;
1079        }
1080    }
1081
1082    Ok(())
1083}
1084
1085// Generic where clause validation
1086fn validate_where_clauses<W: ViewDefinitionWhereTrait>(
1087    where_clauses: &[W],
1088) -> Result<(), SofError> {
1089    // Basic validation - just ensure paths are provided
1090    // Type checking will be done during actual evaluation
1091    for where_clause in where_clauses {
1092        if where_clause.path().is_none() {
1093            return Err(SofError::InvalidViewDefinition(
1094                "Where clause must have a path specified".to_string(),
1095            ));
1096        }
1097    }
1098    Ok(())
1099}
1100
1101// Generic helper - no longer needs to be version-specific
1102fn can_be_coerced_to_boolean(result: &EvaluationResult) -> bool {
1103    // Check if the result can be meaningfully used as a boolean in a where clause
1104    match result {
1105        // Boolean values are obviously OK
1106        EvaluationResult::Boolean(_, _) => true,
1107
1108        // Empty is OK (evaluates to false)
1109        EvaluationResult::Empty => true,
1110
1111        // Collections are OK - they evaluate based on whether they're empty or not
1112        EvaluationResult::Collection { .. } => true,
1113
1114        // Other types cannot be meaningfully coerced to boolean for where clauses
1115        // This includes: String, Integer, Decimal, Date, DateTime, Time, Quantity, Object
1116        _ => false,
1117    }
1118}
1119
1120// Generic select validation
1121fn validate_select<S: ViewDefinitionSelectTrait>(select: &S) -> Result<(), SofError> {
1122    validate_select_with_context(select, false)
1123}
1124
1125fn validate_select_with_context<S: ViewDefinitionSelectTrait>(
1126    select: &S,
1127    in_foreach_context: bool,
1128) -> Result<(), SofError>
1129where
1130    S::Select: ViewDefinitionSelectTrait,
1131{
1132    // Determine if we're entering a forEach context at this level
1133    let entering_foreach = select.for_each().is_some() || select.for_each_or_null().is_some();
1134    let current_foreach_context = in_foreach_context || entering_foreach;
1135
1136    // Validate collection attribute with the current forEach context
1137    if let Some(columns) = select.column() {
1138        for column in columns {
1139            if let Some(collection_value) = column.collection() {
1140                if !collection_value && !current_foreach_context {
1141                    return Err(SofError::InvalidViewDefinition(
1142                        "Column 'collection' attribute must be true when specified".to_string(),
1143                    ));
1144                }
1145            }
1146        }
1147    }
1148
1149    // Validate unionAll column consistency
1150    if let Some(union_selects) = select.union_all() {
1151        validate_union_all_columns(union_selects)?;
1152    }
1153
1154    // Recursively validate nested selects
1155    if let Some(nested_selects) = select.select() {
1156        for nested_select in nested_selects {
1157            validate_select_with_context(nested_select, current_foreach_context)?;
1158        }
1159    }
1160
1161    // Validate unionAll selects with forEach context
1162    if let Some(union_selects) = select.union_all() {
1163        for union_select in union_selects {
1164            validate_select_with_context(union_select, current_foreach_context)?;
1165        }
1166    }
1167
1168    Ok(())
1169}
1170
1171// Generic union validation
1172fn validate_union_all_columns<S: ViewDefinitionSelectTrait>(
1173    union_selects: &[S],
1174) -> Result<(), SofError> {
1175    if union_selects.len() < 2 {
1176        return Ok(());
1177    }
1178
1179    // Get column names and order from first select
1180    let first_select = &union_selects[0];
1181    let first_columns = get_column_names(first_select)?;
1182
1183    // Validate all other selects have the same column names in the same order
1184    for (index, union_select) in union_selects.iter().enumerate().skip(1) {
1185        let current_columns = get_column_names(union_select)?;
1186
1187        if current_columns != first_columns {
1188            if current_columns.len() != first_columns.len()
1189                || !current_columns
1190                    .iter()
1191                    .all(|name| first_columns.contains(name))
1192            {
1193                return Err(SofError::InvalidViewDefinition(format!(
1194                    "UnionAll branch {} has different column names than first branch",
1195                    index
1196                )));
1197            } else {
1198                return Err(SofError::InvalidViewDefinition(format!(
1199                    "UnionAll branch {} has columns in different order than first branch",
1200                    index
1201                )));
1202            }
1203        }
1204    }
1205
1206    Ok(())
1207}
1208
1209// Generic column name extraction
1210fn get_column_names<S: ViewDefinitionSelectTrait>(select: &S) -> Result<Vec<String>, SofError> {
1211    let mut column_names = Vec::new();
1212
1213    // Collect direct column names
1214    if let Some(columns) = select.column() {
1215        for column in columns {
1216            if let Some(name) = column.name() {
1217                column_names.push(name.to_string());
1218            }
1219        }
1220    }
1221
1222    // If this select has unionAll but no direct columns, get columns from first unionAll branch
1223    if column_names.is_empty() {
1224        if let Some(union_selects) = select.union_all() {
1225            if !union_selects.is_empty() {
1226                return get_column_names(&union_selects[0]);
1227            }
1228        }
1229    }
1230
1231    Ok(column_names)
1232}
1233
1234// Generic resource filtering
1235fn filter_resources<'a, B: BundleTrait>(
1236    bundle: &'a B,
1237    resource_type: &str,
1238) -> Result<Vec<&'a B::Resource>, SofError> {
1239    Ok(bundle
1240        .entries()
1241        .into_iter()
1242        .filter(|resource| resource.resource_name() == resource_type)
1243        .collect())
1244}
1245
1246// Generic where clause application
1247fn apply_where_clauses<'a, R, W>(
1248    resources: Vec<&'a R>,
1249    where_clauses: Option<&[W]>,
1250    variables: &HashMap<String, EvaluationResult>,
1251) -> Result<Vec<&'a R>, SofError>
1252where
1253    R: ResourceTrait,
1254    W: ViewDefinitionWhereTrait,
1255{
1256    if let Some(wheres) = where_clauses {
1257        let mut filtered = Vec::new();
1258
1259        for resource in resources {
1260            let mut include_resource = true;
1261
1262            // All where clauses must evaluate to true for the resource to be included
1263            for where_clause in wheres {
1264                let fhir_resource = resource.to_fhir_resource();
1265                let mut context = EvaluationContext::new(vec![fhir_resource]);
1266
1267                // Add variables to the context
1268                for (name, value) in variables {
1269                    context.set_variable_result(name, value.clone());
1270                }
1271
1272                let path = where_clause.path().ok_or_else(|| {
1273                    SofError::InvalidViewDefinition("Where clause path is required".to_string())
1274                })?;
1275
1276                match evaluate_expression(path, &context) {
1277                    Ok(result) => {
1278                        // Check if the result can be meaningfully used as a boolean
1279                        if !can_be_coerced_to_boolean(&result) {
1280                            return Err(SofError::InvalidViewDefinition(format!(
1281                                "Where clause path '{}' returns type '{}' which cannot be used as a boolean condition. \
1282                                 Where clauses must return boolean values, collections, or empty results.",
1283                                path,
1284                                result.type_name()
1285                            )));
1286                        }
1287
1288                        // Check if result is truthy (non-empty and not false)
1289                        if !is_truthy(&result) {
1290                            include_resource = false;
1291                            break;
1292                        }
1293                    }
1294                    Err(e) => {
1295                        return Err(SofError::FhirPathError(format!(
1296                            "Error evaluating where clause '{}': {}",
1297                            path, e
1298                        )));
1299                    }
1300                }
1301            }
1302
1303            if include_resource {
1304                filtered.push(resource);
1305            }
1306        }
1307
1308        Ok(filtered)
1309    } else {
1310        Ok(resources)
1311    }
1312}
1313
1314// Removed generate_rows_per_resource_r4 - replaced with new forEach-aware implementation
1315
1316// Removed generate_rows_with_for_each_r4 - replaced with new forEach-aware implementation
1317
1318// Helper functions for FHIRPath result processing
1319fn is_truthy(result: &EvaluationResult) -> bool {
1320    match result {
1321        EvaluationResult::Empty => false,
1322        EvaluationResult::Boolean(b, _) => *b,
1323        EvaluationResult::Collection { items, .. } => !items.is_empty(),
1324        _ => true, // Non-empty, non-false values are truthy
1325    }
1326}
1327
1328fn fhirpath_result_to_json_value_collection(result: EvaluationResult) -> Option<serde_json::Value> {
1329    match result {
1330        EvaluationResult::Empty => Some(serde_json::Value::Array(vec![])),
1331        EvaluationResult::Collection { items, .. } => {
1332            // Always return array for collection columns, even if empty
1333            let values: Vec<serde_json::Value> = items
1334                .into_iter()
1335                .filter_map(fhirpath_result_to_json_value)
1336                .collect();
1337            Some(serde_json::Value::Array(values))
1338        }
1339        // For non-collection results in collection columns, wrap in array
1340        single_result => {
1341            if let Some(json_val) = fhirpath_result_to_json_value(single_result) {
1342                Some(serde_json::Value::Array(vec![json_val]))
1343            } else {
1344                Some(serde_json::Value::Array(vec![]))
1345            }
1346        }
1347    }
1348}
1349
1350fn fhirpath_result_to_json_value(result: EvaluationResult) -> Option<serde_json::Value> {
1351    match result {
1352        EvaluationResult::Empty => None,
1353        EvaluationResult::Boolean(b, _) => Some(serde_json::Value::Bool(b)),
1354        EvaluationResult::Integer(i, _) => {
1355            Some(serde_json::Value::Number(serde_json::Number::from(i)))
1356        }
1357        EvaluationResult::Decimal(d, _) => {
1358            // Check if this Decimal represents a whole number
1359            if d.fract().is_zero() {
1360                // Convert to integer if no fractional part
1361                if let Ok(i) = d.to_string().parse::<i64>() {
1362                    Some(serde_json::Value::Number(serde_json::Number::from(i)))
1363                } else {
1364                    // Handle very large numbers as strings
1365                    Some(serde_json::Value::String(d.to_string()))
1366                }
1367            } else {
1368                // Convert Decimal to a float for fractional numbers
1369                if let Ok(f) = d.to_string().parse::<f64>() {
1370                    if let Some(num) = serde_json::Number::from_f64(f) {
1371                        Some(serde_json::Value::Number(num))
1372                    } else {
1373                        Some(serde_json::Value::String(d.to_string()))
1374                    }
1375                } else {
1376                    Some(serde_json::Value::String(d.to_string()))
1377                }
1378            }
1379        }
1380        EvaluationResult::String(s, _) => Some(serde_json::Value::String(s)),
1381        EvaluationResult::Date(s, _) => Some(serde_json::Value::String(s)),
1382        EvaluationResult::DateTime(s, _) => {
1383            // Remove "@" prefix from datetime strings if present
1384            let cleaned = s.strip_prefix("@").unwrap_or(&s);
1385            Some(serde_json::Value::String(cleaned.to_string()))
1386        }
1387        EvaluationResult::Time(s, _) => {
1388            // Remove "@T" prefix from time strings if present
1389            let cleaned = s.strip_prefix("@T").unwrap_or(&s);
1390            Some(serde_json::Value::String(cleaned.to_string()))
1391        }
1392        EvaluationResult::Collection { items, .. } => {
1393            if items.len() == 1 {
1394                // Single item collection - unwrap to the item itself
1395                fhirpath_result_to_json_value(items.into_iter().next().unwrap())
1396            } else if items.is_empty() {
1397                None
1398            } else {
1399                // Multiple items - convert to array
1400                let values: Vec<serde_json::Value> = items
1401                    .into_iter()
1402                    .filter_map(fhirpath_result_to_json_value)
1403                    .collect();
1404                Some(serde_json::Value::Array(values))
1405            }
1406        }
1407        EvaluationResult::Object { map, .. } => {
1408            let mut json_map = serde_json::Map::new();
1409            for (k, v) in map {
1410                if let Some(json_val) = fhirpath_result_to_json_value(v) {
1411                    json_map.insert(k, json_val);
1412                }
1413            }
1414            Some(serde_json::Value::Object(json_map))
1415        }
1416        // Handle other result types as strings
1417        _ => Some(serde_json::Value::String(format!("{:?}", result))),
1418    }
1419}
1420
1421fn extract_iteration_items(result: EvaluationResult) -> Vec<EvaluationResult> {
1422    match result {
1423        EvaluationResult::Collection { items, .. } => items,
1424        EvaluationResult::Empty => Vec::new(),
1425        single_item => vec![single_item],
1426    }
1427}
1428
1429// Generic row generation functions
1430
1431fn generate_rows_from_selects<R, S>(
1432    resources: &[&R],
1433    selects: &[S],
1434    variables: &HashMap<String, EvaluationResult>,
1435) -> Result<(Vec<String>, Vec<ProcessedRow>), SofError>
1436where
1437    R: ResourceTrait,
1438    S: ViewDefinitionSelectTrait,
1439    S::Select: ViewDefinitionSelectTrait,
1440{
1441    let mut all_columns = Vec::new();
1442    let mut all_rows = Vec::new();
1443
1444    // For each resource, generate all possible row combinations
1445    for resource in resources {
1446        let resource_rows =
1447            generate_rows_for_resource(*resource, selects, &mut all_columns, variables)?;
1448        all_rows.extend(resource_rows);
1449    }
1450
1451    Ok((all_columns, all_rows))
1452}
1453
1454fn generate_rows_for_resource<R, S>(
1455    resource: &R,
1456    selects: &[S],
1457    all_columns: &mut Vec<String>,
1458    variables: &HashMap<String, EvaluationResult>,
1459) -> Result<Vec<ProcessedRow>, SofError>
1460where
1461    R: ResourceTrait,
1462    S: ViewDefinitionSelectTrait,
1463    S::Select: ViewDefinitionSelectTrait,
1464{
1465    let fhir_resource = resource.to_fhir_resource();
1466    let mut context = EvaluationContext::new(vec![fhir_resource]);
1467
1468    // Add variables to the context
1469    for (name, value) in variables {
1470        context.set_variable_result(name, value.clone());
1471    }
1472
1473    // Generate all possible row combinations for this resource
1474    let row_combinations = generate_row_combinations(&context, selects, all_columns, variables)?;
1475
1476    Ok(row_combinations)
1477}
1478
1479#[derive(Debug, Clone)]
1480struct RowCombination {
1481    values: Vec<Option<serde_json::Value>>,
1482}
1483
1484fn generate_row_combinations<S>(
1485    context: &EvaluationContext,
1486    selects: &[S],
1487    all_columns: &mut Vec<String>,
1488    variables: &HashMap<String, EvaluationResult>,
1489) -> Result<Vec<ProcessedRow>, SofError>
1490where
1491    S: ViewDefinitionSelectTrait,
1492    S::Select: ViewDefinitionSelectTrait,
1493{
1494    // First pass: collect all column names to ensure consistent ordering
1495    collect_all_columns(selects, all_columns)?;
1496
1497    // Second pass: generate all row combinations
1498    let mut row_combinations = vec![RowCombination {
1499        values: vec![None; all_columns.len()],
1500    }];
1501
1502    for select in selects {
1503        row_combinations =
1504            expand_select_combinations(context, select, &row_combinations, all_columns, variables)?;
1505    }
1506
1507    // Convert to ProcessedRow format
1508    Ok(row_combinations
1509        .into_iter()
1510        .map(|combo| ProcessedRow {
1511            values: combo.values,
1512        })
1513        .collect())
1514}
1515
1516fn collect_all_columns<S>(selects: &[S], all_columns: &mut Vec<String>) -> Result<(), SofError>
1517where
1518    S: ViewDefinitionSelectTrait,
1519{
1520    for select in selects {
1521        // Add columns from this select
1522        if let Some(columns) = select.column() {
1523            for col in columns {
1524                if let Some(name) = col.name() {
1525                    if !all_columns.contains(&name.to_string()) {
1526                        all_columns.push(name.to_string());
1527                    }
1528                }
1529            }
1530        }
1531
1532        // Recursively collect from nested selects
1533        if let Some(nested_selects) = select.select() {
1534            collect_all_columns(nested_selects, all_columns)?;
1535        }
1536
1537        // Collect from unionAll
1538        if let Some(union_selects) = select.union_all() {
1539            collect_all_columns(union_selects, all_columns)?;
1540        }
1541    }
1542    Ok(())
1543}
1544
1545fn expand_select_combinations<S>(
1546    context: &EvaluationContext,
1547    select: &S,
1548    existing_combinations: &[RowCombination],
1549    all_columns: &[String],
1550    variables: &HashMap<String, EvaluationResult>,
1551) -> Result<Vec<RowCombination>, SofError>
1552where
1553    S: ViewDefinitionSelectTrait,
1554    S::Select: ViewDefinitionSelectTrait,
1555{
1556    // Handle forEach and forEachOrNull
1557    if let Some(for_each_path) = select.for_each() {
1558        return expand_for_each_combinations(
1559            context,
1560            select,
1561            existing_combinations,
1562            all_columns,
1563            for_each_path,
1564            false,
1565            variables,
1566        );
1567    }
1568
1569    if let Some(for_each_or_null_path) = select.for_each_or_null() {
1570        return expand_for_each_combinations(
1571            context,
1572            select,
1573            existing_combinations,
1574            all_columns,
1575            for_each_or_null_path,
1576            true,
1577            variables,
1578        );
1579    }
1580
1581    // Handle regular columns (no forEach)
1582    let mut new_combinations = Vec::new();
1583
1584    for existing_combo in existing_combinations {
1585        let mut new_combo = existing_combo.clone();
1586
1587        // Add values from this select's columns
1588        if let Some(columns) = select.column() {
1589            for col in columns {
1590                if let Some(col_name) = col.name() {
1591                    if let Some(col_index) = all_columns.iter().position(|name| name == col_name) {
1592                        let path = col.path().ok_or_else(|| {
1593                            SofError::InvalidViewDefinition("Column path is required".to_string())
1594                        })?;
1595
1596                        match evaluate_expression(path, context) {
1597                            Ok(result) => {
1598                                // Check if this column is marked as a collection
1599                                let is_collection = col.collection().unwrap_or(false);
1600
1601                                new_combo.values[col_index] = if is_collection {
1602                                    fhirpath_result_to_json_value_collection(result)
1603                                } else {
1604                                    fhirpath_result_to_json_value(result)
1605                                };
1606                            }
1607                            Err(e) => {
1608                                return Err(SofError::FhirPathError(format!(
1609                                    "Error evaluating column '{}' with path '{}': {}",
1610                                    col_name, path, e
1611                                )));
1612                            }
1613                        }
1614                    }
1615                }
1616            }
1617        }
1618
1619        new_combinations.push(new_combo);
1620    }
1621
1622    // Handle nested selects
1623    if let Some(nested_selects) = select.select() {
1624        for nested_select in nested_selects {
1625            new_combinations = expand_select_combinations(
1626                context,
1627                nested_select,
1628                &new_combinations,
1629                all_columns,
1630                variables,
1631            )?;
1632        }
1633    }
1634
1635    // Handle unionAll
1636    if let Some(union_selects) = select.union_all() {
1637        let mut union_combinations = Vec::new();
1638
1639        // Process each unionAll select independently, using the combinations that already have
1640        // values from this select's columns and nested selects
1641        for union_select in union_selects {
1642            let select_combinations = expand_select_combinations(
1643                context,
1644                union_select,
1645                &new_combinations,
1646                all_columns,
1647                variables,
1648            )?;
1649            union_combinations.extend(select_combinations);
1650        }
1651
1652        // unionAll replaces new_combinations with the union results
1653        // If no union results, this resource should be filtered out (no rows for this resource)
1654        new_combinations = union_combinations;
1655    }
1656
1657    Ok(new_combinations)
1658}
1659
1660fn expand_for_each_combinations<S>(
1661    context: &EvaluationContext,
1662    select: &S,
1663    existing_combinations: &[RowCombination],
1664    all_columns: &[String],
1665    for_each_path: &str,
1666    allow_null: bool,
1667    variables: &HashMap<String, EvaluationResult>,
1668) -> Result<Vec<RowCombination>, SofError>
1669where
1670    S: ViewDefinitionSelectTrait,
1671    S::Select: ViewDefinitionSelectTrait,
1672{
1673    // Evaluate the forEach expression to get iteration items
1674    let for_each_result = evaluate_expression(for_each_path, context).map_err(|e| {
1675        SofError::FhirPathError(format!(
1676            "Error evaluating forEach expression '{}': {}",
1677            for_each_path, e
1678        ))
1679    })?;
1680
1681    let iteration_items = extract_iteration_items(for_each_result);
1682
1683    if iteration_items.is_empty() {
1684        if allow_null {
1685            // forEachOrNull: generate null rows
1686            let mut new_combinations = Vec::new();
1687            for existing_combo in existing_combinations {
1688                let mut new_combo = existing_combo.clone();
1689
1690                // Set column values to null for this forEach scope
1691                if let Some(columns) = select.column() {
1692                    for col in columns {
1693                        if let Some(col_name) = col.name() {
1694                            if let Some(col_index) =
1695                                all_columns.iter().position(|name| name == col_name)
1696                            {
1697                                new_combo.values[col_index] = None;
1698                            }
1699                        }
1700                    }
1701                }
1702
1703                new_combinations.push(new_combo);
1704            }
1705            return Ok(new_combinations);
1706        } else {
1707            // forEach with empty collection: no rows
1708            return Ok(Vec::new());
1709        }
1710    }
1711
1712    let mut new_combinations = Vec::new();
1713
1714    // For each iteration item, create new combinations
1715    for item in &iteration_items {
1716        // Create a new context with the iteration item
1717        let _item_context = create_iteration_context(item, variables);
1718
1719        for existing_combo in existing_combinations {
1720            let mut new_combo = existing_combo.clone();
1721
1722            // Evaluate columns in the context of the iteration item
1723            if let Some(columns) = select.column() {
1724                for col in columns {
1725                    if let Some(col_name) = col.name() {
1726                        if let Some(col_index) =
1727                            all_columns.iter().position(|name| name == col_name)
1728                        {
1729                            let path = col.path().ok_or_else(|| {
1730                                SofError::InvalidViewDefinition(
1731                                    "Column path is required".to_string(),
1732                                )
1733                            })?;
1734
1735                            // Use the iteration item directly for path evaluation
1736                            let result = if path == "$this" {
1737                                // Special case: $this refers to the current iteration item
1738                                item.clone()
1739                            } else {
1740                                // Evaluate the path on the iteration item
1741                                evaluate_path_on_item(path, item, variables)?
1742                            };
1743
1744                            // Check if this column is marked as a collection
1745                            let is_collection = col.collection().unwrap_or(false);
1746
1747                            new_combo.values[col_index] = if is_collection {
1748                                fhirpath_result_to_json_value_collection(result)
1749                            } else {
1750                                fhirpath_result_to_json_value(result)
1751                            };
1752                        }
1753                    }
1754                }
1755            }
1756
1757            new_combinations.push(new_combo);
1758        }
1759    }
1760
1761    // Handle nested selects with the forEach context
1762    if let Some(nested_selects) = select.select() {
1763        let mut final_combinations = Vec::new();
1764
1765        for item in &iteration_items {
1766            let item_context = create_iteration_context(item, variables);
1767
1768            // For each iteration item, we need to start with the combinations that have
1769            // the correct column values for this forEach scope
1770            for existing_combo in existing_combinations {
1771                // Find the combination that corresponds to this iteration item
1772                // by looking at the values we set for columns in this forEach scope
1773                let mut base_combo = existing_combo.clone();
1774
1775                // Update the base combination with column values for this iteration item
1776                if let Some(columns) = select.column() {
1777                    for col in columns {
1778                        if let Some(col_name) = col.name() {
1779                            if let Some(col_index) =
1780                                all_columns.iter().position(|name| name == col_name)
1781                            {
1782                                let path = col.path().ok_or_else(|| {
1783                                    SofError::InvalidViewDefinition(
1784                                        "Column path is required".to_string(),
1785                                    )
1786                                })?;
1787
1788                                let result = if path == "$this" {
1789                                    item.clone()
1790                                } else {
1791                                    evaluate_path_on_item(path, item, variables)?
1792                                };
1793
1794                                // Check if this column is marked as a collection
1795                                let is_collection = col.collection().unwrap_or(false);
1796
1797                                base_combo.values[col_index] = if is_collection {
1798                                    fhirpath_result_to_json_value_collection(result)
1799                                } else {
1800                                    fhirpath_result_to_json_value(result)
1801                                };
1802                            }
1803                        }
1804                    }
1805                }
1806
1807                // Start with this base combination for nested processing
1808                let mut item_combinations = vec![base_combo];
1809
1810                // Process nested selects
1811                for nested_select in nested_selects {
1812                    item_combinations = expand_select_combinations(
1813                        &item_context,
1814                        nested_select,
1815                        &item_combinations,
1816                        all_columns,
1817                        variables,
1818                    )?;
1819                }
1820
1821                final_combinations.extend(item_combinations);
1822            }
1823        }
1824
1825        new_combinations = final_combinations;
1826    }
1827
1828    // Handle unionAll within forEach context
1829    if let Some(union_selects) = select.union_all() {
1830        let mut union_combinations = Vec::new();
1831
1832        for item in &iteration_items {
1833            let item_context = create_iteration_context(item, variables);
1834
1835            // For each iteration item, process all unionAll selects
1836            for existing_combo in existing_combinations {
1837                let mut base_combo = existing_combo.clone();
1838
1839                // Update the base combination with column values for this iteration item
1840                if let Some(columns) = select.column() {
1841                    for col in columns {
1842                        if let Some(col_name) = col.name() {
1843                            if let Some(col_index) =
1844                                all_columns.iter().position(|name| name == col_name)
1845                            {
1846                                let path = col.path().ok_or_else(|| {
1847                                    SofError::InvalidViewDefinition(
1848                                        "Column path is required".to_string(),
1849                                    )
1850                                })?;
1851
1852                                let result = if path == "$this" {
1853                                    item.clone()
1854                                } else {
1855                                    evaluate_path_on_item(path, item, variables)?
1856                                };
1857
1858                                // Check if this column is marked as a collection
1859                                let is_collection = col.collection().unwrap_or(false);
1860
1861                                base_combo.values[col_index] = if is_collection {
1862                                    fhirpath_result_to_json_value_collection(result)
1863                                } else {
1864                                    fhirpath_result_to_json_value(result)
1865                                };
1866                            }
1867                        }
1868                    }
1869                }
1870
1871                // Also evaluate columns from nested selects and add them to base_combo
1872                if let Some(nested_selects) = select.select() {
1873                    for nested_select in nested_selects {
1874                        if let Some(nested_columns) = nested_select.column() {
1875                            for col in nested_columns {
1876                                if let Some(col_name) = col.name() {
1877                                    if let Some(col_index) =
1878                                        all_columns.iter().position(|name| name == col_name)
1879                                    {
1880                                        let path = col.path().ok_or_else(|| {
1881                                            SofError::InvalidViewDefinition(
1882                                                "Column path is required".to_string(),
1883                                            )
1884                                        })?;
1885
1886                                        let result = if path == "$this" {
1887                                            item.clone()
1888                                        } else {
1889                                            evaluate_path_on_item(path, item, variables)?
1890                                        };
1891
1892                                        // Check if this column is marked as a collection
1893                                        let is_collection = col.collection().unwrap_or(false);
1894
1895                                        base_combo.values[col_index] = if is_collection {
1896                                            fhirpath_result_to_json_value_collection(result)
1897                                        } else {
1898                                            fhirpath_result_to_json_value(result)
1899                                        };
1900                                    }
1901                                }
1902                            }
1903                        }
1904                    }
1905                }
1906
1907                // Process each unionAll select independently for this iteration item
1908                for union_select in union_selects {
1909                    let mut select_combinations = vec![base_combo.clone()];
1910                    select_combinations = expand_select_combinations(
1911                        &item_context,
1912                        union_select,
1913                        &select_combinations,
1914                        all_columns,
1915                        variables,
1916                    )?;
1917                    union_combinations.extend(select_combinations);
1918                }
1919            }
1920        }
1921
1922        // unionAll replaces new_combinations with the union results
1923        // If no union results, filter out this resource (no rows for this resource)
1924        new_combinations = union_combinations;
1925    }
1926
1927    Ok(new_combinations)
1928}
1929
1930// Generic helper functions
1931fn evaluate_path_on_item(
1932    path: &str,
1933    item: &EvaluationResult,
1934    variables: &HashMap<String, EvaluationResult>,
1935) -> Result<EvaluationResult, SofError> {
1936    // Create a temporary context with the iteration item as the root resource
1937    let mut temp_context = match item {
1938        EvaluationResult::Object { .. } => {
1939            // Convert the iteration item to a resource-like structure for FHIRPath evaluation
1940            // For simplicity, we'll create a basic context where the item is available for evaluation
1941            let mut context = EvaluationContext::new(vec![]);
1942            context.this = Some(item.clone());
1943            context
1944        }
1945        _ => EvaluationContext::new(vec![]),
1946    };
1947
1948    // Add variables to the temporary context
1949    for (name, value) in variables {
1950        temp_context.set_variable_result(name, value.clone());
1951    }
1952
1953    // Evaluate the FHIRPath expression in the context of the iteration item
1954    match evaluate_expression(path, &temp_context) {
1955        Ok(result) => Ok(result),
1956        Err(_e) => {
1957            // If FHIRPath evaluation fails, try simple property access as fallback
1958            match item {
1959                EvaluationResult::Object { map, .. } => {
1960                    if let Some(value) = map.get(path) {
1961                        Ok(value.clone())
1962                    } else {
1963                        Ok(EvaluationResult::Empty)
1964                    }
1965                }
1966                _ => Ok(EvaluationResult::Empty),
1967            }
1968        }
1969    }
1970}
1971
1972fn create_iteration_context(
1973    item: &EvaluationResult,
1974    variables: &HashMap<String, EvaluationResult>,
1975) -> EvaluationContext {
1976    // Create a new context with the iteration item as the root
1977    let mut context = EvaluationContext::new(vec![]);
1978    context.this = Some(item.clone());
1979
1980    // Preserve variables from the parent context
1981    for (name, value) in variables {
1982        context.set_variable_result(name, value.clone());
1983    }
1984
1985    context
1986}
1987
1988/// Filter a bundle's resources by their lastUpdated metadata
1989fn filter_bundle_by_since(bundle: SofBundle, since: DateTime<Utc>) -> Result<SofBundle, SofError> {
1990    match bundle {
1991        #[cfg(feature = "R4")]
1992        SofBundle::R4(mut b) => {
1993            if let Some(entries) = b.entry.as_mut() {
1994                entries.retain(|entry| {
1995                    entry
1996                        .resource
1997                        .as_ref()
1998                        .and_then(|r| r.get_last_updated())
1999                        .map(|last_updated| last_updated > since)
2000                        .unwrap_or(false)
2001                });
2002            }
2003            Ok(SofBundle::R4(b))
2004        }
2005        #[cfg(feature = "R4B")]
2006        SofBundle::R4B(mut b) => {
2007            if let Some(entries) = b.entry.as_mut() {
2008                entries.retain(|entry| {
2009                    entry
2010                        .resource
2011                        .as_ref()
2012                        .and_then(|r| r.get_last_updated())
2013                        .map(|last_updated| last_updated > since)
2014                        .unwrap_or(false)
2015                });
2016            }
2017            Ok(SofBundle::R4B(b))
2018        }
2019        #[cfg(feature = "R5")]
2020        SofBundle::R5(mut b) => {
2021            if let Some(entries) = b.entry.as_mut() {
2022                entries.retain(|entry| {
2023                    entry
2024                        .resource
2025                        .as_ref()
2026                        .and_then(|r| r.get_last_updated())
2027                        .map(|last_updated| last_updated > since)
2028                        .unwrap_or(false)
2029                });
2030            }
2031            Ok(SofBundle::R5(b))
2032        }
2033        #[cfg(feature = "R6")]
2034        SofBundle::R6(mut b) => {
2035            if let Some(entries) = b.entry.as_mut() {
2036                entries.retain(|entry| {
2037                    entry
2038                        .resource
2039                        .as_ref()
2040                        .and_then(|r| r.get_last_updated())
2041                        .map(|last_updated| last_updated > since)
2042                        .unwrap_or(false)
2043                });
2044            }
2045            Ok(SofBundle::R6(b))
2046        }
2047    }
2048}
2049
2050/// Apply pagination to processed results
2051fn apply_pagination_to_result(
2052    mut result: ProcessedResult,
2053    limit: Option<usize>,
2054    page: Option<usize>,
2055) -> Result<ProcessedResult, SofError> {
2056    if let Some(limit) = limit {
2057        let page_num = page.unwrap_or(1);
2058        if page_num == 0 {
2059            return Err(SofError::InvalidViewDefinition(
2060                "Page number must be greater than 0".to_string(),
2061            ));
2062        }
2063
2064        let start_index = (page_num - 1) * limit;
2065        if start_index >= result.rows.len() {
2066            // Return empty result if page is beyond data
2067            result.rows.clear();
2068        } else {
2069            let end_index = std::cmp::min(start_index + limit, result.rows.len());
2070            result.rows = result.rows[start_index..end_index].to_vec();
2071        }
2072    }
2073
2074    Ok(result)
2075}
2076
2077fn format_output(result: ProcessedResult, content_type: ContentType) -> Result<Vec<u8>, SofError> {
2078    match content_type {
2079        ContentType::Csv | ContentType::CsvWithHeader => {
2080            format_csv(result, content_type == ContentType::CsvWithHeader)
2081        }
2082        ContentType::Json => format_json(result),
2083        ContentType::NdJson => format_ndjson(result),
2084        ContentType::Parquet => Err(SofError::UnsupportedContentType(
2085            "Parquet not yet implemented".to_string(),
2086        )),
2087    }
2088}
2089
2090fn format_csv(result: ProcessedResult, include_header: bool) -> Result<Vec<u8>, SofError> {
2091    let mut wtr = csv::Writer::from_writer(vec![]);
2092
2093    if include_header {
2094        wtr.write_record(&result.columns)?;
2095    }
2096
2097    for row in result.rows {
2098        let record: Vec<String> = row
2099            .values
2100            .iter()
2101            .map(|v| match v {
2102                Some(val) => {
2103                    // For string values, extract the raw string instead of JSON serializing
2104                    if let serde_json::Value::String(s) = val {
2105                        s.clone()
2106                    } else {
2107                        // For non-string values, use JSON serialization
2108                        serde_json::to_string(val).unwrap_or_default()
2109                    }
2110                }
2111                None => String::new(),
2112            })
2113            .collect();
2114        wtr.write_record(&record)?;
2115    }
2116
2117    wtr.into_inner()
2118        .map_err(|e| SofError::CsvWriterError(e.to_string()))
2119}
2120
2121fn format_json(result: ProcessedResult) -> Result<Vec<u8>, SofError> {
2122    let mut output = Vec::new();
2123
2124    for row in result.rows {
2125        let mut row_obj = serde_json::Map::new();
2126        for (i, column) in result.columns.iter().enumerate() {
2127            let value = row
2128                .values
2129                .get(i)
2130                .and_then(|v| v.as_ref())
2131                .cloned()
2132                .unwrap_or(serde_json::Value::Null);
2133            row_obj.insert(column.clone(), value);
2134        }
2135        output.push(serde_json::Value::Object(row_obj));
2136    }
2137
2138    Ok(serde_json::to_vec_pretty(&output)?)
2139}
2140
2141fn format_ndjson(result: ProcessedResult) -> Result<Vec<u8>, SofError> {
2142    let mut output = Vec::new();
2143
2144    for row in result.rows {
2145        let mut row_obj = serde_json::Map::new();
2146        for (i, column) in result.columns.iter().enumerate() {
2147            let value = row
2148                .values
2149                .get(i)
2150                .and_then(|v| v.as_ref())
2151                .cloned()
2152                .unwrap_or(serde_json::Value::Null);
2153            row_obj.insert(column.clone(), value);
2154        }
2155        let line = serde_json::to_string(&serde_json::Value::Object(row_obj))?;
2156        output.extend_from_slice(line.as_bytes());
2157        output.push(b'\n');
2158    }
2159
2160    Ok(output)
2161}