Skip to main content

trustformers_debug/
data_export.rs

1//! Data export capabilities for debugging tools
2//!
3//! This module provides comprehensive data export functionality supporting
4//! multiple formats including CSV, Excel, JSON, HDF5, and more.
5
6use anyhow::Result;
7use chrono::{DateTime, Utc};
8use oxisql_sqlite_compat::blocking::SqliteConnectionBlocking;
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11use uuid::Uuid;
12
13/// Data export manager for debugging tools
14#[derive(Debug, Clone)]
15pub struct DataExportManager {
16    /// Export configuration
17    config: ExportConfig,
18    /// Active export jobs
19    active_jobs: HashMap<Uuid, ExportJob>,
20    /// Export history
21    export_history: Vec<ExportRecord>,
22    /// Supported formats
23    supported_formats: Vec<ExportFormat>,
24}
25
26/// Export configuration
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ExportConfig {
29    /// Default export directory
30    pub default_directory: String,
31    /// Maximum file size (bytes)
32    pub max_file_size: u64,
33    /// Enable compression
34    pub enable_compression: bool,
35    /// Default format
36    pub default_format: ExportFormat,
37    /// Include metadata
38    pub include_metadata: bool,
39    /// Export templates
40    pub templates: Vec<ExportTemplate>,
41}
42
43/// Export job tracking
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ExportJob {
46    /// Job identifier
47    pub id: Uuid,
48    /// Job name
49    pub name: String,
50    /// Export format
51    pub format: ExportFormat,
52    /// Output path
53    pub output_path: String,
54    /// Job status
55    pub status: ExportStatus,
56    /// Progress percentage
57    pub progress: f64,
58    /// Start time
59    pub started_at: DateTime<Utc>,
60    /// Completion time
61    pub completed_at: Option<DateTime<Utc>>,
62    /// Data size (bytes)
63    pub data_size: u64,
64    /// Error message (if failed)
65    pub error_message: Option<String>,
66    /// Export options
67    pub options: ExportOptions,
68}
69
70/// Export record for history
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ExportRecord {
73    /// Record identifier
74    pub id: Uuid,
75    /// Export job ID
76    pub job_id: Uuid,
77    /// Export timestamp
78    pub timestamp: DateTime<Utc>,
79    /// File path
80    pub file_path: String,
81    /// File size
82    pub file_size: u64,
83    /// Export format
84    pub format: ExportFormat,
85    /// Success status
86    pub success: bool,
87    /// Duration (seconds)
88    pub duration: f64,
89}
90
91/// Export template for common configurations
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ExportTemplate {
94    /// Template identifier
95    pub id: String,
96    /// Template name
97    pub name: String,
98    /// Description
99    pub description: String,
100    /// Export format
101    pub format: ExportFormat,
102    /// Export options
103    pub options: ExportOptions,
104    /// Data filters
105    pub filters: DataFilters,
106    /// Template tags
107    pub tags: Vec<String>,
108}
109
110/// Export options
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ExportOptions {
113    /// Include headers (for CSV/Excel)
114    pub include_headers: bool,
115    /// Date format
116    pub date_format: String,
117    /// Precision for floats
118    pub float_precision: u32,
119    /// Field separator (for CSV)
120    pub separator: String,
121    /// Compression level (0-9)
122    pub compression_level: u32,
123    /// Include metadata
124    pub include_metadata: bool,
125    /// Custom formatting options
126    pub custom_options: HashMap<String, serde_json::Value>,
127}
128
129/// Data filters for selective export
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct DataFilters {
132    /// Date range filter
133    pub date_range: Option<DateRange>,
134    /// Include specific data types
135    pub data_types: Vec<DataType>,
136    /// Exclude fields
137    pub exclude_fields: Vec<String>,
138    /// Include only fields
139    pub include_fields: Option<Vec<String>>,
140    /// Custom filters
141    pub custom_filters: HashMap<String, serde_json::Value>,
142}
143
144/// Date range for filtering
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct DateRange {
147    /// Start date
148    pub start: DateTime<Utc>,
149    /// End date
150    pub end: DateTime<Utc>,
151}
152
153/// Export formats supported
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash, Eq)]
155pub enum ExportFormat {
156    /// Comma-separated values
157    Csv,
158    /// Excel workbook
159    Excel,
160    /// JSON format
161    Json,
162    /// Pretty-printed JSON
163    JsonPretty,
164    /// HDF5 format
165    Hdf5,
166    /// Parquet format
167    Parquet,
168    /// XML format
169    Xml,
170    /// YAML format
171    Yaml,
172    /// SQLite database
173    Sqlite,
174    /// MessagePack
175    MessagePack,
176    /// Apache Arrow
177    Arrow,
178    /// Custom format
179    Custom(String),
180}
181
182/// Export status
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub enum ExportStatus {
185    Pending,
186    InProgress,
187    Completed,
188    Failed,
189    Cancelled,
190}
191
192/// Data types that can be exported
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub enum DataType {
195    TensorData,
196    GradientData,
197    PerformanceMetrics,
198    MemoryProfiles,
199    ActivityLogs,
200    AnnotationData,
201    CommentData,
202    ModelDiagnostics,
203    TrainingDynamics,
204    ArchitectureAnalysis,
205    Custom(String),
206}
207
208/// Exportable data container
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ExportableData {
211    /// Data identifier
212    pub id: Uuid,
213    /// Data name
214    pub name: String,
215    /// Data type
216    pub data_type: DataType,
217    /// Creation timestamp
218    pub timestamp: DateTime<Utc>,
219    /// Data content
220    pub content: ExportDataContent,
221    /// Metadata
222    pub metadata: HashMap<String, serde_json::Value>,
223    /// Data size
224    pub size: u64,
225}
226
227/// Content of exportable data
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub enum ExportDataContent {
230    /// Tabular data
231    Table(TableData),
232    /// Time series data
233    TimeSeries(TimeSeriesData),
234    /// Key-value pairs
235    KeyValue(HashMap<String, serde_json::Value>),
236    /// Structured data
237    Structured(serde_json::Value),
238    /// Binary data
239    Binary(Vec<u8>),
240    /// Text data
241    Text(String),
242}
243
244/// Tabular data structure
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct TableData {
247    /// Column headers
248    pub headers: Vec<String>,
249    /// Data rows
250    pub rows: Vec<Vec<serde_json::Value>>,
251    /// Column types
252    pub column_types: HashMap<String, ColumnType>,
253}
254
255/// Time series data structure
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct TimeSeriesData {
258    /// Timestamps
259    pub timestamps: Vec<DateTime<Utc>>,
260    /// Data series
261    pub series: HashMap<String, Vec<f64>>,
262    /// Series metadata
263    pub metadata: HashMap<String, String>,
264}
265
266/// Column data types
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub enum ColumnType {
269    Integer,
270    Float,
271    String,
272    Boolean,
273    DateTime,
274    Binary,
275}
276
277impl DataExportManager {
278    /// Create a new data export manager
279    pub fn new(config: ExportConfig) -> Self {
280        let supported_formats = vec![
281            ExportFormat::Csv,
282            ExportFormat::Excel,
283            ExportFormat::Json,
284            ExportFormat::JsonPretty,
285            ExportFormat::Xml,
286            ExportFormat::Yaml,
287            ExportFormat::Sqlite,
288        ];
289
290        Self {
291            config,
292            active_jobs: HashMap::new(),
293            export_history: Vec::new(),
294            supported_formats,
295        }
296    }
297
298    /// Start an export job
299    pub fn start_export(
300        &mut self,
301        name: String,
302        data: Vec<ExportableData>,
303        format: ExportFormat,
304        output_path: String,
305        options: ExportOptions,
306    ) -> Result<Uuid> {
307        let job_id = Uuid::new_v4();
308
309        // Calculate total data size
310        let data_size: u64 = data.iter().map(|d| d.size).sum();
311
312        // Check file size limit
313        if data_size > self.config.max_file_size {
314            return Err(anyhow::anyhow!("Data size exceeds maximum file size limit"));
315        }
316
317        let job = ExportJob {
318            id: job_id,
319            name: name.clone(),
320            format: format.clone(),
321            output_path: output_path.clone(),
322            status: ExportStatus::Pending,
323            progress: 0.0,
324            started_at: Utc::now(),
325            completed_at: None,
326            data_size,
327            error_message: None,
328            options: options.clone(),
329        };
330
331        self.active_jobs.insert(job_id, job);
332
333        // Start the actual export process
334        self.execute_export(job_id, data, options)?;
335
336        Ok(job_id)
337    }
338
339    /// Execute the export process
340    fn execute_export(
341        &mut self,
342        job_id: Uuid,
343        data: Vec<ExportableData>,
344        options: ExportOptions,
345    ) -> Result<()> {
346        // Extract job info to avoid multiple mutable borrows
347        let (format, output_path) = {
348            if let Some(job) = self.active_jobs.get_mut(&job_id) {
349                job.status = ExportStatus::InProgress;
350                (job.format.clone(), job.output_path.clone())
351            } else {
352                return Err(anyhow::anyhow!("Export job not found"));
353            }
354        };
355
356        let result = match format {
357            ExportFormat::Csv => self.export_csv(&data, &output_path, &options),
358            ExportFormat::Json => self.export_json(&data, &output_path, &options),
359            ExportFormat::JsonPretty => self.export_json_pretty(&data, &output_path, &options),
360            ExportFormat::Excel => self.export_excel(&data, &output_path, &options),
361            ExportFormat::Xml => self.export_xml(&data, &output_path, &options),
362            ExportFormat::Yaml => self.export_yaml(&data, &output_path, &options),
363            ExportFormat::Sqlite => self.export_sqlite(&data, &output_path, &options),
364            // Binary/columnar formats that require optional external crates or services.
365            // These are intentionally not implemented to keep the core dependency set
366            // minimal.  Callers should use JSON or CSV as a universal fallback.
367            ExportFormat::Hdf5 => Err(anyhow::anyhow!(
368                "HDF5 export is not supported in this build. Use JSON or CSV instead."
369            )),
370            ExportFormat::Parquet => Err(anyhow::anyhow!(
371                "Parquet export is not supported in this build. Use JSON or CSV instead."
372            )),
373            ExportFormat::MessagePack => Err(anyhow::anyhow!(
374                "MessagePack export is not supported in this build. Use JSON instead."
375            )),
376            ExportFormat::Arrow => Err(anyhow::anyhow!(
377                "Apache Arrow export is not supported in this build. Use JSON or CSV instead."
378            )),
379            ExportFormat::Custom(ref name) => Err(anyhow::anyhow!(
380                "Custom export format '{}' is not registered. \
381                 Register a handler or use one of the built-in formats.",
382                name
383            )),
384        };
385
386        // Update job status
387        if let Some(job) = self.active_jobs.get_mut(&job_id) {
388            match result {
389                Ok(_) => {
390                    job.status = ExportStatus::Completed;
391                    job.progress = 100.0;
392                    job.completed_at = Some(Utc::now());
393
394                    // Add to history by cloning the job
395                    let job_copy = job.clone();
396                    self.add_export_record(&job_copy);
397                },
398                Err(e) => {
399                    job.status = ExportStatus::Failed;
400                    job.error_message = Some(e.to_string());
401                },
402            }
403        }
404
405        Ok(())
406    }
407
408    /// Export to CSV format
409    fn export_csv(
410        &mut self,
411        data: &[ExportableData],
412        output_path: &str,
413        options: &ExportOptions,
414    ) -> Result<()> {
415        use std::fs::File;
416        use std::io::Write;
417
418        let mut file = File::create(output_path)?;
419
420        for item in data {
421            match &item.content {
422                ExportDataContent::Table(table_data) => {
423                    // Write headers
424                    if options.include_headers {
425                        let header_line = table_data.headers.join(&options.separator);
426                        writeln!(file, "{}", header_line)?;
427                    }
428
429                    // Write data rows
430                    for row in &table_data.rows {
431                        let row_values: Vec<String> =
432                            row.iter().map(|v| self.format_value_for_csv(v, options)).collect();
433                        let row_line = row_values.join(&options.separator);
434                        writeln!(file, "{}", row_line)?;
435                    }
436                },
437                ExportDataContent::TimeSeries(ts_data) => {
438                    // Write time series data
439                    if options.include_headers {
440                        let mut headers = vec!["timestamp".to_string()];
441                        headers.extend(ts_data.series.keys().cloned());
442                        let header_line = headers.join(&options.separator);
443                        writeln!(file, "{}", header_line)?;
444                    }
445
446                    for (i, timestamp) in ts_data.timestamps.iter().enumerate() {
447                        let mut row = vec![timestamp.format(&options.date_format).to_string()];
448                        for series_name in ts_data.series.keys() {
449                            if let Some(series) = ts_data.series.get(series_name) {
450                                if let Some(value) = series.get(i) {
451                                    row.push(format!(
452                                        "{:.precision$}",
453                                        value,
454                                        precision = options.float_precision as usize
455                                    ));
456                                } else {
457                                    row.push("".to_string());
458                                }
459                            }
460                        }
461                        let row_line = row.join(&options.separator);
462                        writeln!(file, "{}", row_line)?;
463                    }
464                },
465                _ => {
466                    // Convert other formats to JSON and then to CSV-like representation
467                    let json_str = serde_json::to_string(&item.content)?;
468                    writeln!(file, "{}", json_str)?;
469                },
470            }
471        }
472
473        Ok(())
474    }
475
476    /// Export to JSON format
477    fn export_json(
478        &mut self,
479        data: &[ExportableData],
480        output_path: &str,
481        _options: &ExportOptions,
482    ) -> Result<()> {
483        use std::fs::File;
484
485        let file = File::create(output_path)?;
486        serde_json::to_writer(file, data)?;
487        Ok(())
488    }
489
490    /// Export to pretty JSON format
491    fn export_json_pretty(
492        &mut self,
493        data: &[ExportableData],
494        output_path: &str,
495        _options: &ExportOptions,
496    ) -> Result<()> {
497        use std::fs::File;
498
499        let file = File::create(output_path)?;
500        serde_json::to_writer_pretty(file, data)?;
501        Ok(())
502    }
503
504    /// Export to Excel (.xlsx) format as a real Office Open XML workbook
505    fn export_excel(
506        &mut self,
507        data: &[ExportableData],
508        output_path: &str,
509        options: &ExportOptions,
510    ) -> Result<()> {
511        use oxiarc_archive::zip::ZipWriter;
512
513        let rows = build_xlsx_rows(data, options)?;
514        let sheet_xml = build_xlsx_sheet(&rows);
515
516        let mut buffer: Vec<u8> = Vec::new();
517        {
518            let mut writer = ZipWriter::new(&mut buffer);
519            writer
520                .add_file("[Content_Types].xml", XLSX_CONTENT_TYPES.as_bytes())
521                .map_err(|e| anyhow::anyhow!("xlsx: failed to write [Content_Types].xml: {e}"))?;
522            writer
523                .add_file("_rels/.rels", XLSX_ROOT_RELS.as_bytes())
524                .map_err(|e| anyhow::anyhow!("xlsx: failed to write _rels/.rels: {e}"))?;
525            writer
526                .add_file("xl/workbook.xml", XLSX_WORKBOOK.as_bytes())
527                .map_err(|e| anyhow::anyhow!("xlsx: failed to write xl/workbook.xml: {e}"))?;
528            writer
529                .add_file("xl/_rels/workbook.xml.rels", XLSX_WORKBOOK_RELS.as_bytes())
530                .map_err(|e| {
531                    anyhow::anyhow!("xlsx: failed to write xl/_rels/workbook.xml.rels: {e}")
532                })?;
533            writer.add_file("xl/worksheets/sheet1.xml", sheet_xml.as_bytes()).map_err(|e| {
534                anyhow::anyhow!("xlsx: failed to write xl/worksheets/sheet1.xml: {e}")
535            })?;
536            writer
537                .finish()
538                .map_err(|e| anyhow::anyhow!("xlsx: failed to finalize workbook package: {e}"))?;
539        }
540
541        std::fs::write(output_path, &buffer)?;
542        Ok(())
543    }
544
545    /// Export to XML format
546    fn export_xml(
547        &mut self,
548        data: &[ExportableData],
549        output_path: &str,
550        _options: &ExportOptions,
551    ) -> Result<()> {
552        use std::fs::File;
553        use std::io::Write;
554
555        let mut file = File::create(output_path)?;
556
557        writeln!(file, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
558        writeln!(file, "<export_data>")?;
559
560        for item in data {
561            writeln!(
562                file,
563                "  <data_item id=\"{}\" type=\"{:?}\">",
564                item.id, item.data_type
565            )?;
566            writeln!(file, "    <name>{}</name>", item.name)?;
567            writeln!(
568                file,
569                "    <timestamp>{}</timestamp>",
570                item.timestamp.to_rfc3339()
571            )?;
572            writeln!(file, "    <size>{}</size>", item.size)?;
573
574            // Convert content to XML (simplified)
575            let content_json = serde_json::to_string(&item.content)?;
576            writeln!(file, "    <content><![CDATA[{}]]></content>", content_json)?;
577
578            writeln!(file, "  </data_item>")?;
579        }
580
581        writeln!(file, "</export_data>")?;
582        Ok(())
583    }
584
585    /// Export to YAML format
586    fn export_yaml(
587        &mut self,
588        data: &[ExportableData],
589        output_path: &str,
590        _options: &ExportOptions,
591    ) -> Result<()> {
592        use std::fs::File;
593
594        let file = File::create(output_path)?;
595        serde_json::to_writer_pretty(file, data)?;
596        Ok(())
597    }
598
599    /// Export to a real SQLite database file.
600    ///
601    /// Uses the COOLJAPAN Pure-Rust `oxisql-sqlite-compat` backend (a C-free fork
602    /// of Limbo) — never `libsqlite3`/`rusqlite`. Each [`ExportableData`] item is
603    /// written into its own table:
604    ///
605    /// * [`ExportDataContent::Table`] → a table with one column per header,
606    ///   with SQL column types taken from the explicit `column_types` map or
607    ///   inferred from the data (`INTEGER` / `REAL` / `TEXT`).
608    /// * [`ExportDataContent::TimeSeries`] → a table with a `timestamp` column
609    ///   plus one `REAL` column per series.
610    /// * Any other content → a single-column `content TEXT` table holding the
611    ///   JSON serialization so nothing is silently dropped.
612    fn export_sqlite(
613        &mut self,
614        data: &[ExportableData],
615        output_path: &str,
616        options: &ExportOptions,
617    ) -> Result<()> {
618        // Create the database file fresh so the export is deterministic.
619        if std::path::Path::new(output_path).exists() {
620            std::fs::remove_file(output_path).map_err(|e| {
621                anyhow::anyhow!("failed to clear existing SQLite file '{output_path}': {e}")
622            })?;
623        }
624
625        let conn = SqliteConnectionBlocking::open(output_path)
626            .map_err(|e| anyhow::anyhow!("failed to open SQLite database '{output_path}': {e}"))?;
627
628        let mut used_names: HashSet<String> = HashSet::new();
629        for (index, item) in data.iter().enumerate() {
630            let table_name = unique_table_name(&item.name, index, &mut used_names);
631            match &item.content {
632                ExportDataContent::Table(table) => {
633                    write_sqlite_table(
634                        &conn,
635                        &table_name,
636                        &table.headers,
637                        &table.rows,
638                        &table.column_types,
639                    )?;
640                },
641                ExportDataContent::TimeSeries(ts) => {
642                    write_sqlite_timeseries(&conn, &table_name, ts, options)?;
643                },
644                other => {
645                    let json = serde_json::to_string(other)?;
646                    conn.execute(
647                        &format!("CREATE TABLE IF NOT EXISTS \"{table_name}\" (content TEXT)"),
648                        &[],
649                    )
650                    .map_err(|e| anyhow::anyhow!("CREATE TABLE '{table_name}' failed: {e}"))?;
651                    conn.execute(
652                        &format!(
653                            "INSERT INTO \"{table_name}\" (content) VALUES ({})",
654                            quote_sql_string(&json)
655                        ),
656                        &[],
657                    )
658                    .map_err(|e| anyhow::anyhow!("INSERT into '{table_name}' failed: {e}"))?;
659                },
660            }
661        }
662
663        Ok(())
664    }
665
666    /// Helper function to format values for CSV
667    fn format_value_for_csv(&self, value: &serde_json::Value, options: &ExportOptions) -> String {
668        match value {
669            serde_json::Value::Number(n) => {
670                if let Some(f) = n.as_f64() {
671                    format!(
672                        "{:.precision$}",
673                        f,
674                        precision = options.float_precision as usize
675                    )
676                } else {
677                    n.to_string()
678                }
679            },
680            serde_json::Value::String(s) => {
681                // Escape quotes and commas
682                if s.contains(',') || s.contains('"') || s.contains('\n') {
683                    format!("\"{}\"", s.replace('"', "\"\""))
684                } else {
685                    s.clone()
686                }
687            },
688            _ => value.to_string(),
689        }
690    }
691
692    /// Add export record to history
693    fn add_export_record(&mut self, job: &ExportJob) {
694        let record = ExportRecord {
695            id: Uuid::new_v4(),
696            job_id: job.id,
697            timestamp: Utc::now(),
698            file_path: job.output_path.clone(),
699            file_size: job.data_size,
700            format: job.format.clone(),
701            success: matches!(job.status, ExportStatus::Completed),
702            duration: job
703                .completed_at
704                .map(|end| (end - job.started_at).num_milliseconds() as f64 / 1000.0)
705                .unwrap_or(0.0),
706        };
707
708        self.export_history.push(record);
709    }
710
711    /// Get export job status
712    pub fn get_job_status(&self, job_id: Uuid) -> Option<&ExportJob> {
713        self.active_jobs.get(&job_id)
714    }
715
716    /// Get export history
717    pub fn get_export_history(&self) -> &[ExportRecord] {
718        &self.export_history
719    }
720
721    /// Create export template
722    pub fn create_template(
723        &mut self,
724        name: String,
725        description: String,
726        format: ExportFormat,
727        options: ExportOptions,
728        filters: DataFilters,
729        tags: Vec<String>,
730    ) -> String {
731        let template_id = Uuid::new_v4().to_string();
732
733        let template = ExportTemplate {
734            id: template_id.clone(),
735            name,
736            description,
737            format,
738            options,
739            filters,
740            tags,
741        };
742
743        self.config.templates.push(template);
744        template_id
745    }
746
747    /// Apply export template
748    pub fn apply_template(
749        &self,
750        template_id: &str,
751    ) -> Option<(&ExportFormat, &ExportOptions, &DataFilters)> {
752        self.config
753            .templates
754            .iter()
755            .find(|t| t.id == template_id)
756            .map(|t| (&t.format, &t.options, &t.filters))
757    }
758
759    /// Get supported formats
760    pub fn get_supported_formats(&self) -> &[ExportFormat] {
761        &self.supported_formats
762    }
763
764    /// Cancel export job
765    pub fn cancel_job(&mut self, job_id: Uuid) -> Result<()> {
766        if let Some(job) = self.active_jobs.get_mut(&job_id) {
767            if matches!(job.status, ExportStatus::Pending | ExportStatus::InProgress) {
768                job.status = ExportStatus::Cancelled;
769                Ok(())
770            } else {
771                Err(anyhow::anyhow!("Job cannot be cancelled in current status"))
772            }
773        } else {
774            Err(anyhow::anyhow!("Job not found"))
775        }
776    }
777
778    /// Get export statistics
779    pub fn get_export_statistics(&self) -> ExportStatistics {
780        let total_exports = self.export_history.len();
781        let successful_exports = self.export_history.iter().filter(|r| r.success).count();
782        let total_size: u64 = self.export_history.iter().map(|r| r.file_size).sum();
783        let avg_duration = if total_exports > 0 {
784            self.export_history.iter().map(|r| r.duration).sum::<f64>() / total_exports as f64
785        } else {
786            0.0
787        };
788
789        let format_stats: HashMap<ExportFormat, usize> =
790            self.export_history.iter().fold(HashMap::new(), |mut acc, record| {
791                *acc.entry(record.format.clone()).or_insert(0) += 1;
792                acc
793            });
794
795        ExportStatistics {
796            total_exports,
797            successful_exports,
798            failed_exports: total_exports - successful_exports,
799            total_size_bytes: total_size,
800            average_duration_seconds: avg_duration,
801            format_statistics: format_stats,
802            active_jobs: self.active_jobs.len(),
803        }
804    }
805}
806
807/// OOXML `[Content_Types].xml` part declaring the package content types.
808const XLSX_CONTENT_TYPES: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
809<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/></Types>"#;
810
811/// OOXML `_rels/.rels` package-level relationships part.
812const XLSX_ROOT_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
813<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#;
814
815/// OOXML `xl/workbook.xml` part defining a single worksheet.
816const XLSX_WORKBOOK: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
817<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets></workbook>"#;
818
819/// OOXML `xl/_rels/workbook.xml.rels` part linking the workbook to its worksheet.
820const XLSX_WORKBOOK_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
821<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/></Relationships>"#;
822
823/// A single spreadsheet cell value to emit into a worksheet.
824enum XlsxCell {
825    /// Inline (shared-string-free) text cell, emitted as `<c t="inlineStr">`.
826    Inline(String),
827    /// Numeric cell, emitted as `<c><v>..</v></c>` with the pre-formatted literal.
828    Number(String),
829    /// Empty placeholder cell.
830    Empty,
831}
832
833/// XML-escape `&`, `<`, `>`, `"`, `'` for safe inclusion in OOXML parts.
834fn xlsx_xml_escape(input: &str) -> String {
835    let mut out = String::with_capacity(input.len());
836    for ch in input.chars() {
837        match ch {
838            '&' => out.push_str("&amp;"),
839            '<' => out.push_str("&lt;"),
840            '>' => out.push_str("&gt;"),
841            '"' => out.push_str("&quot;"),
842            '\'' => out.push_str("&apos;"),
843            _ => out.push(ch),
844        }
845    }
846    out
847}
848
849/// Convert a zero-based column index to an Excel column name (0 -> "A", 26 -> "AA").
850fn xlsx_column_name(index: usize) -> String {
851    let mut n = index + 1;
852    let mut name = String::new();
853    while n > 0 {
854        let rem = (n - 1) % 26;
855        name.insert(0, (b'A' + rem as u8) as char);
856        n = (n - 1) / 26;
857    }
858    name
859}
860
861/// Map a JSON value to a worksheet cell, mirroring `format_value_for_csv` numeric rules.
862fn xlsx_value_to_cell(value: &serde_json::Value, options: &ExportOptions) -> XlsxCell {
863    match value {
864        serde_json::Value::Number(n) => {
865            if let Some(f) = n.as_f64() {
866                XlsxCell::Number(format!(
867                    "{:.precision$}",
868                    f,
869                    precision = options.float_precision as usize
870                ))
871            } else {
872                XlsxCell::Number(n.to_string())
873            }
874        },
875        serde_json::Value::String(s) => XlsxCell::Inline(s.clone()),
876        _ => XlsxCell::Inline(value.to_string()),
877    }
878}
879
880/// Build the worksheet rows from the export data, mirroring `export_csv` layout.
881fn build_xlsx_rows(data: &[ExportableData], options: &ExportOptions) -> Result<Vec<Vec<XlsxCell>>> {
882    let mut rows: Vec<Vec<XlsxCell>> = Vec::new();
883    for item in data {
884        match &item.content {
885            ExportDataContent::Table(table_data) => {
886                if options.include_headers {
887                    rows.push(
888                        table_data.headers.iter().map(|h| XlsxCell::Inline(h.clone())).collect(),
889                    );
890                }
891                for row in &table_data.rows {
892                    rows.push(row.iter().map(|v| xlsx_value_to_cell(v, options)).collect());
893                }
894            },
895            ExportDataContent::TimeSeries(ts_data) => {
896                if options.include_headers {
897                    let mut header = vec![XlsxCell::Inline("timestamp".to_string())];
898                    header.extend(ts_data.series.keys().map(|k| XlsxCell::Inline(k.clone())));
899                    rows.push(header);
900                }
901                for (i, timestamp) in ts_data.timestamps.iter().enumerate() {
902                    let mut cells = vec![XlsxCell::Inline(
903                        timestamp.format(&options.date_format).to_string(),
904                    )];
905                    for series_name in ts_data.series.keys() {
906                        if let Some(series) = ts_data.series.get(series_name) {
907                            if let Some(value) = series.get(i) {
908                                cells.push(XlsxCell::Number(format!(
909                                    "{:.precision$}",
910                                    value,
911                                    precision = options.float_precision as usize
912                                )));
913                            } else {
914                                cells.push(XlsxCell::Empty);
915                            }
916                        }
917                    }
918                    rows.push(cells);
919                }
920            },
921            _ => {
922                let json_str = serde_json::to_string(&item.content)?;
923                rows.push(vec![XlsxCell::Inline(json_str)]);
924            },
925        }
926    }
927    Ok(rows)
928}
929
930/// Render worksheet rows into the `xl/worksheets/sheet1.xml` OOXML part.
931fn build_xlsx_sheet(rows: &[Vec<XlsxCell>]) -> String {
932    let mut sheet = String::new();
933    sheet.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
934    sheet.push_str(
935        "<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">",
936    );
937    sheet.push_str("<sheetData>");
938    for (row_idx, row) in rows.iter().enumerate() {
939        let row_num = row_idx + 1;
940        sheet.push_str(&format!("<row r=\"{row_num}\">"));
941        for (col_idx, cell) in row.iter().enumerate() {
942            let cell_ref = format!("{}{row_num}", xlsx_column_name(col_idx));
943            match cell {
944                XlsxCell::Inline(text) => {
945                    sheet.push_str(&format!(
946                        "<c r=\"{cell_ref}\" t=\"inlineStr\"><is><t xml:space=\"preserve\">{}</t></is></c>",
947                        xlsx_xml_escape(text)
948                    ));
949                },
950                XlsxCell::Number(num) => {
951                    sheet.push_str(&format!("<c r=\"{cell_ref}\"><v>{num}</v></c>"));
952                },
953                XlsxCell::Empty => {
954                    sheet.push_str(&format!("<c r=\"{cell_ref}\"/>"));
955                },
956            }
957        }
958        sheet.push_str("</row>");
959    }
960    sheet.push_str("</sheetData></worksheet>");
961    sheet
962}
963
964// ── SQLite export helpers ───────────────────────────────────────────────────────
965
966/// Sanitize an arbitrary name into a safe, double-quotable SQL identifier.
967///
968/// Non-alphanumeric characters become `_`; a leading non-alphabetic character is
969/// prefixed so the identifier is always valid.
970fn sanitize_sql_identifier(name: &str) -> String {
971    let mut sanitized: String = name
972        .chars()
973        .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
974        .collect();
975    let needs_prefix = sanitized
976        .chars()
977        .next()
978        .map(|c| !(c.is_ascii_alphabetic() || c == '_'))
979        .unwrap_or(true);
980    if needs_prefix {
981        sanitized = format!("t_{sanitized}");
982    }
983    sanitized
984}
985
986/// Produce a collision-free table name for the export, recording it in `used`.
987fn unique_table_name(name: &str, index: usize, used: &mut HashSet<String>) -> String {
988    let base = sanitize_sql_identifier(name);
989    if used.insert(base.clone()) {
990        return base;
991    }
992    let candidate = format!("{base}_{index}");
993    used.insert(candidate.clone());
994    candidate
995}
996
997/// Quote and escape a string for use as a SQL string literal.
998fn quote_sql_string(value: &str) -> String {
999    format!("'{}'", value.replace('\'', "''"))
1000}
1001
1002/// Convert a single JSON value into an inline SQL literal.
1003///
1004/// * `null`  → `NULL`
1005/// * `bool`  → `1` / `0` (SQLite has no native boolean)
1006/// * number  → verbatim numeric literal
1007/// * string  → single-quote-escaped string literal
1008/// * array / object → JSON text stored as a string literal
1009fn json_value_to_sql_literal(value: &serde_json::Value) -> String {
1010    match value {
1011        serde_json::Value::Null => "NULL".to_string(),
1012        serde_json::Value::Bool(b) => if *b { "1" } else { "0" }.to_string(),
1013        serde_json::Value::Number(n) => n.to_string(),
1014        serde_json::Value::String(s) => quote_sql_string(s),
1015        other => quote_sql_string(&other.to_string()),
1016    }
1017}
1018
1019/// Infer a SQL column affinity (`INTEGER` / `REAL` / `TEXT`) from the data in a
1020/// column when no explicit [`ColumnType`] is supplied.
1021fn infer_sql_column_type(rows: &[Vec<serde_json::Value>], col: usize) -> &'static str {
1022    let mut integers = 0usize;
1023    let mut floats = 0usize;
1024    let mut bools = 0usize;
1025    let mut others = 0usize;
1026    for row in rows {
1027        match row.get(col) {
1028            None | Some(serde_json::Value::Null) => {},
1029            Some(serde_json::Value::Number(n)) => {
1030                if n.is_i64() || n.is_u64() {
1031                    integers += 1;
1032                } else {
1033                    floats += 1;
1034                }
1035            },
1036            Some(serde_json::Value::Bool(_)) => bools += 1,
1037            Some(_) => others += 1,
1038        }
1039    }
1040    if others > 0 {
1041        "TEXT"
1042    } else if floats > 0 {
1043        "REAL"
1044    } else if integers > 0 || bools > 0 {
1045        "INTEGER"
1046    } else {
1047        "TEXT"
1048    }
1049}
1050
1051/// Map an explicit [`ColumnType`] to its SQL affinity.
1052fn column_type_to_sql(column_type: &ColumnType) -> &'static str {
1053    match column_type {
1054        ColumnType::Integer | ColumnType::Boolean => "INTEGER",
1055        ColumnType::Float => "REAL",
1056        ColumnType::Binary => "BLOB",
1057        ColumnType::String | ColumnType::DateTime => "TEXT",
1058    }
1059}
1060
1061/// Create and populate a SQLite table from [`TableData`].
1062fn write_sqlite_table(
1063    conn: &SqliteConnectionBlocking,
1064    table_name: &str,
1065    headers: &[String],
1066    rows: &[Vec<serde_json::Value>],
1067    column_types: &HashMap<String, ColumnType>,
1068) -> Result<()> {
1069    if headers.is_empty() {
1070        return Ok(());
1071    }
1072
1073    let column_idents: Vec<String> = headers.iter().map(|h| sanitize_sql_identifier(h)).collect();
1074    let column_defs: Vec<String> = headers
1075        .iter()
1076        .enumerate()
1077        .map(|(idx, header)| {
1078            let sql_type = column_types
1079                .get(header)
1080                .map(column_type_to_sql)
1081                .unwrap_or_else(|| infer_sql_column_type(rows, idx));
1082            format!("\"{}\" {}", column_idents[idx], sql_type)
1083        })
1084        .collect();
1085
1086    let create = format!(
1087        "CREATE TABLE IF NOT EXISTS \"{table_name}\" ({})",
1088        column_defs.join(", ")
1089    );
1090    conn.execute(&create, &[])
1091        .map_err(|e| anyhow::anyhow!("CREATE TABLE '{table_name}' failed: {e}"))?;
1092
1093    let column_list =
1094        column_idents.iter().map(|c| format!("\"{c}\"")).collect::<Vec<_>>().join(", ");
1095
1096    for row in rows {
1097        let values: Vec<String> = (0..headers.len())
1098            .map(|idx| match row.get(idx) {
1099                Some(value) => json_value_to_sql_literal(value),
1100                None => "NULL".to_string(),
1101            })
1102            .collect();
1103        let insert = format!(
1104            "INSERT INTO \"{table_name}\" ({column_list}) VALUES ({})",
1105            values.join(", ")
1106        );
1107        conn.execute(&insert, &[])
1108            .map_err(|e| anyhow::anyhow!("INSERT into '{table_name}' failed: {e}"))?;
1109    }
1110
1111    Ok(())
1112}
1113
1114/// Create and populate a SQLite table from [`TimeSeriesData`].
1115fn write_sqlite_timeseries(
1116    conn: &SqliteConnectionBlocking,
1117    table_name: &str,
1118    ts: &TimeSeriesData,
1119    options: &ExportOptions,
1120) -> Result<()> {
1121    // Deterministic series ordering.
1122    let mut series_names: Vec<String> = ts.series.keys().cloned().collect();
1123    series_names.sort();
1124
1125    let mut column_defs = vec!["\"timestamp\" TEXT".to_string()];
1126    for name in &series_names {
1127        column_defs.push(format!("\"{}\" REAL", sanitize_sql_identifier(name)));
1128    }
1129    let create = format!(
1130        "CREATE TABLE IF NOT EXISTS \"{table_name}\" ({})",
1131        column_defs.join(", ")
1132    );
1133    conn.execute(&create, &[])
1134        .map_err(|e| anyhow::anyhow!("CREATE TABLE '{table_name}' failed: {e}"))?;
1135
1136    let mut column_list = vec!["\"timestamp\"".to_string()];
1137    for name in &series_names {
1138        column_list.push(format!("\"{}\"", sanitize_sql_identifier(name)));
1139    }
1140    let column_list = column_list.join(", ");
1141
1142    for (i, timestamp) in ts.timestamps.iter().enumerate() {
1143        let mut values = vec![quote_sql_string(
1144            &timestamp.format(&options.date_format).to_string(),
1145        )];
1146        for name in &series_names {
1147            match ts.series.get(name).and_then(|series| series.get(i)) {
1148                Some(value) => values.push(value.to_string()),
1149                None => values.push("NULL".to_string()),
1150            }
1151        }
1152        let insert = format!(
1153            "INSERT INTO \"{table_name}\" ({column_list}) VALUES ({})",
1154            values.join(", ")
1155        );
1156        conn.execute(&insert, &[])
1157            .map_err(|e| anyhow::anyhow!("INSERT into '{table_name}' failed: {e}"))?;
1158    }
1159
1160    Ok(())
1161}
1162
1163/// Export statistics
1164#[derive(Debug, Clone, Serialize, Deserialize)]
1165pub struct ExportStatistics {
1166    pub total_exports: usize,
1167    pub successful_exports: usize,
1168    pub failed_exports: usize,
1169    pub total_size_bytes: u64,
1170    pub average_duration_seconds: f64,
1171    pub format_statistics: HashMap<ExportFormat, usize>,
1172    pub active_jobs: usize,
1173}
1174
1175impl Default for ExportConfig {
1176    fn default() -> Self {
1177        Self {
1178            default_directory: "./exports".to_string(),
1179            max_file_size: 1024 * 1024 * 1024, // 1GB
1180            enable_compression: true,
1181            default_format: ExportFormat::Json,
1182            include_metadata: true,
1183            templates: Vec::new(),
1184        }
1185    }
1186}
1187
1188impl Default for ExportOptions {
1189    fn default() -> Self {
1190        Self {
1191            include_headers: true,
1192            date_format: "%Y-%m-%d %H:%M:%S UTC".to_string(),
1193            float_precision: 6,
1194            separator: ",".to_string(),
1195            compression_level: 6,
1196            include_metadata: true,
1197            custom_options: HashMap::new(),
1198        }
1199    }
1200}
1201
1202impl Default for DataFilters {
1203    fn default() -> Self {
1204        Self {
1205            date_range: None,
1206            data_types: vec![
1207                DataType::TensorData,
1208                DataType::GradientData,
1209                DataType::PerformanceMetrics,
1210            ],
1211            exclude_fields: Vec::new(),
1212            include_fields: None,
1213            custom_filters: HashMap::new(),
1214        }
1215    }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220    use super::*;
1221    use tempfile::tempdir;
1222
1223    // These values are test data, not approximations of mathematical constants
1224    #[allow(clippy::approx_constant)]
1225    fn create_test_data() -> Vec<ExportableData> {
1226        let table_data = TableData {
1227            headers: vec![
1228                "id".to_string(),
1229                "value".to_string(),
1230                "timestamp".to_string(),
1231            ],
1232            rows: vec![
1233                vec![
1234                    serde_json::Value::Number(serde_json::Number::from(1)),
1235                    serde_json::Value::Number(
1236                        serde_json::Number::from_f64(3.14).expect("operation failed in test"),
1237                    ),
1238                    serde_json::Value::String("2023-01-01T12:00:00Z".to_string()),
1239                ],
1240                vec![
1241                    serde_json::Value::Number(serde_json::Number::from(2)),
1242                    serde_json::Value::Number(
1243                        serde_json::Number::from_f64(2.71).expect("operation failed in test"),
1244                    ),
1245                    serde_json::Value::String("2023-01-01T12:01:00Z".to_string()),
1246                ],
1247            ],
1248            column_types: HashMap::new(),
1249        };
1250
1251        vec![ExportableData {
1252            id: Uuid::new_v4(),
1253            name: "Test Data".to_string(),
1254            data_type: DataType::TensorData,
1255            timestamp: Utc::now(),
1256            content: ExportDataContent::Table(table_data),
1257            metadata: HashMap::new(),
1258            size: 1024,
1259        }]
1260    }
1261
1262    #[test]
1263    fn test_export_manager_creation() {
1264        let config = ExportConfig::default();
1265        let manager = DataExportManager::new(config);
1266
1267        assert!(manager.get_supported_formats().contains(&ExportFormat::Json));
1268        assert!(manager.get_supported_formats().contains(&ExportFormat::Csv));
1269    }
1270
1271    #[test]
1272    fn test_csv_export() {
1273        let config = ExportConfig::default();
1274        let mut manager = DataExportManager::new(config);
1275        let test_data = create_test_data();
1276
1277        let temp_dir = tempdir().expect("temp file creation failed");
1278        let output_path = temp_dir.path().join("test.csv").to_string_lossy().to_string();
1279
1280        let job_id = manager
1281            .start_export(
1282                "Test CSV Export".to_string(),
1283                test_data,
1284                ExportFormat::Csv,
1285                output_path.clone(),
1286                ExportOptions::default(),
1287            )
1288            .expect("operation failed in test");
1289
1290        // Check job was created
1291        assert!(manager.active_jobs.contains_key(&job_id));
1292
1293        // Check file was created
1294        assert!(std::path::Path::new(&output_path).exists());
1295    }
1296
1297    #[test]
1298    fn test_json_export() {
1299        let config = ExportConfig::default();
1300        let mut manager = DataExportManager::new(config);
1301        let test_data = create_test_data();
1302
1303        let temp_dir = tempdir().expect("temp file creation failed");
1304        let output_path = temp_dir.path().join("test.json").to_string_lossy().to_string();
1305
1306        let job_id = manager
1307            .start_export(
1308                "Test JSON Export".to_string(),
1309                test_data,
1310                ExportFormat::Json,
1311                output_path.clone(),
1312                ExportOptions::default(),
1313            )
1314            .expect("operation failed in test");
1315
1316        assert!(manager.active_jobs.contains_key(&job_id));
1317        assert!(std::path::Path::new(&output_path).exists());
1318    }
1319
1320    #[test]
1321    fn test_export_template() {
1322        let config = ExportConfig::default();
1323        let mut manager = DataExportManager::new(config);
1324
1325        let template_id = manager.create_template(
1326            "CSV Template".to_string(),
1327            "Standard CSV export".to_string(),
1328            ExportFormat::Csv,
1329            ExportOptions::default(),
1330            DataFilters::default(),
1331            vec!["csv".to_string(), "standard".to_string()],
1332        );
1333
1334        let (format, options, _filters) =
1335            manager.apply_template(&template_id).expect("temp file creation failed");
1336        assert_eq!(*format, ExportFormat::Csv);
1337        assert!(options.include_headers);
1338    }
1339
1340    #[test]
1341    fn test_export_statistics() {
1342        let config = ExportConfig::default();
1343        let mut manager = DataExportManager::new(config);
1344
1345        // Add some mock export records
1346        manager.export_history.push(ExportRecord {
1347            id: Uuid::new_v4(),
1348            job_id: Uuid::new_v4(),
1349            timestamp: Utc::now(),
1350            file_path: "test1.csv".to_string(),
1351            file_size: 1024,
1352            format: ExportFormat::Csv,
1353            success: true,
1354            duration: 2.5,
1355        });
1356
1357        manager.export_history.push(ExportRecord {
1358            id: Uuid::new_v4(),
1359            job_id: Uuid::new_v4(),
1360            timestamp: Utc::now(),
1361            file_path: "test2.json".to_string(),
1362            file_size: 2048,
1363            format: ExportFormat::Json,
1364            success: true,
1365            duration: 1.8,
1366        });
1367
1368        let stats = manager.get_export_statistics();
1369        assert_eq!(stats.total_exports, 2);
1370        assert_eq!(stats.successful_exports, 2);
1371        assert_eq!(stats.total_size_bytes, 3072);
1372    }
1373
1374    #[test]
1375    fn test_excel_export_roundtrip() {
1376        use oxiarc_archive::zip::ZipReader;
1377        use std::io::Cursor;
1378
1379        let config = ExportConfig::default();
1380        let mut manager = DataExportManager::new(config);
1381        let test_data = create_test_data();
1382
1383        let file_name = format!("trustformers_xlsx_test_{}.xlsx", Uuid::new_v4());
1384        let output_path = std::env::temp_dir().join(file_name).to_string_lossy().to_string();
1385
1386        manager
1387            .start_export(
1388                "Test Excel Export".to_string(),
1389                test_data,
1390                ExportFormat::Excel,
1391                output_path.clone(),
1392                ExportOptions::default(),
1393            )
1394            .expect("excel export should succeed");
1395
1396        assert!(std::path::Path::new(&output_path).exists());
1397
1398        let bytes = std::fs::read(&output_path).expect("read xlsx bytes");
1399        let mut reader = ZipReader::new(Cursor::new(bytes)).expect("open xlsx as zip");
1400        let entries = reader.entries().to_vec();
1401        let names: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
1402
1403        assert!(
1404            names.iter().any(|n| n == "[Content_Types].xml"),
1405            "missing [Content_Types].xml; entries = {names:?}"
1406        );
1407        assert!(
1408            names.iter().any(|n| n == "xl/workbook.xml"),
1409            "missing xl/workbook.xml; entries = {names:?}"
1410        );
1411        assert!(
1412            names.iter().any(|n| n == "xl/worksheets/sheet1.xml"),
1413            "missing xl/worksheets/sheet1.xml; entries = {names:?}"
1414        );
1415
1416        let sheet_entry = entries
1417            .iter()
1418            .find(|e| e.name == "xl/worksheets/sheet1.xml")
1419            .expect("sheet1.xml entry");
1420        let sheet_bytes = reader.extract(sheet_entry).expect("extract sheet1.xml");
1421        let sheet_xml = String::from_utf8(sheet_bytes).expect("sheet1.xml is utf8");
1422
1423        assert!(sheet_xml.contains("<worksheet"));
1424        // Header text from create_test_data(): "id", "value", "timestamp".
1425        assert!(sheet_xml.contains("id"), "sheet missing header text");
1426        assert!(sheet_xml.contains("value"), "sheet missing header text");
1427        assert!(sheet_xml.contains("timestamp"), "sheet missing header text");
1428
1429        let _ = std::fs::remove_file(&output_path);
1430    }
1431
1432    #[test]
1433    // 3.14 is test data from create_test_data(), not an approximation of PI.
1434    #[allow(clippy::approx_constant)]
1435    fn test_sqlite_export_roundtrip() {
1436        let config = ExportConfig::default();
1437        let mut manager = DataExportManager::new(config);
1438        let test_data = create_test_data();
1439
1440        let file_name = format!("trustformers_sqlite_test_{}.sqlite3", Uuid::new_v4());
1441        let output_path = std::env::temp_dir().join(file_name).to_string_lossy().to_string();
1442
1443        let job_id = manager
1444            .start_export(
1445                "Test SQLite Export".to_string(),
1446                test_data,
1447                ExportFormat::Sqlite,
1448                output_path.clone(),
1449                ExportOptions::default(),
1450            )
1451            .expect("sqlite export should succeed");
1452
1453        // The job must have completed (not silently failed back to JSON).
1454        let status = manager.get_job_status(job_id).expect("job should exist");
1455        assert!(
1456            matches!(status.status, ExportStatus::Completed),
1457            "export status = {:?}",
1458            status.status
1459        );
1460        assert!(
1461            std::path::Path::new(&output_path).exists(),
1462            "sqlite file should exist"
1463        );
1464
1465        // Re-open the real SQLite file and read the rows back.
1466        let conn = SqliteConnectionBlocking::open(&output_path).expect("open sqlite file");
1467        let tables = conn.tables().expect("list tables");
1468        assert!(!tables.is_empty(), "expected at least one table");
1469
1470        // create_test_data() names the item "Test Data" -> sanitized "Test_Data".
1471        let rows = conn
1472            .query(
1473                "SELECT \"id\", \"value\", \"timestamp\" FROM \"Test_Data\"",
1474                &[],
1475            )
1476            .expect("query rows back");
1477        assert_eq!(rows.len(), 2, "two rows should persist");
1478
1479        // Collect ids to verify both rows persisted regardless of row order.
1480        let mut ids: Vec<i64> = rows
1481            .iter()
1482            .map(|r| r.try_get::<i64>("id").expect("id column is INTEGER"))
1483            .collect();
1484        ids.sort_unstable();
1485        assert_eq!(ids, vec![1, 2]);
1486
1487        // Find the row with id == 1 and verify the float + string columns.
1488        let first = rows
1489            .iter()
1490            .find(|r| r.try_get::<i64>("id").map(|v| v == 1).unwrap_or(false))
1491            .expect("row with id=1");
1492        let value: f64 = first.try_get("value").expect("value column is REAL");
1493        assert!((value - 3.14).abs() < 1e-9, "value = {value}");
1494        let timestamp: String = first.try_get("timestamp").expect("timestamp column is TEXT");
1495        assert_eq!(timestamp, "2023-01-01T12:00:00Z");
1496
1497        let _ = std::fs::remove_file(&output_path);
1498    }
1499}