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            // The payload is embedded as JSON inside a CDATA section rather
575            // than mapped onto XML elements: `item.content` is an arbitrary
576            // `serde_json::Value` whose shape is not known ahead of time, and a
577            // lossless generic JSON-to-XML mapping would have to invent element
578            // names for array members and for keys that are not valid XML
579            // names. CDATA keeps the round-trip exact.
580            let content_json = serde_json::to_string(&item.content)?;
581            writeln!(file, "    <content><![CDATA[{}]]></content>", content_json)?;
582
583            writeln!(file, "  </data_item>")?;
584        }
585
586        writeln!(file, "</export_data>")?;
587        Ok(())
588    }
589
590    /// Export to YAML format
591    fn export_yaml(
592        &mut self,
593        data: &[ExportableData],
594        output_path: &str,
595        _options: &ExportOptions,
596    ) -> Result<()> {
597        use std::fs::File;
598
599        let file = File::create(output_path)?;
600        serde_json::to_writer_pretty(file, data)?;
601        Ok(())
602    }
603
604    /// Export to a real SQLite database file.
605    ///
606    /// Uses the COOLJAPAN Pure-Rust `oxisql-sqlite-compat` backend (a C-free fork
607    /// of Limbo) — never `libsqlite3`/`rusqlite`. Each [`ExportableData`] item is
608    /// written into its own table:
609    ///
610    /// * [`ExportDataContent::Table`] → a table with one column per header,
611    ///   with SQL column types taken from the explicit `column_types` map or
612    ///   inferred from the data (`INTEGER` / `REAL` / `TEXT`).
613    /// * [`ExportDataContent::TimeSeries`] → a table with a `timestamp` column
614    ///   plus one `REAL` column per series.
615    /// * Any other content → a single-column `content TEXT` table holding the
616    ///   JSON serialization so nothing is silently dropped.
617    fn export_sqlite(
618        &mut self,
619        data: &[ExportableData],
620        output_path: &str,
621        options: &ExportOptions,
622    ) -> Result<()> {
623        // Create the database file fresh so the export is deterministic.
624        if std::path::Path::new(output_path).exists() {
625            std::fs::remove_file(output_path).map_err(|e| {
626                anyhow::anyhow!("failed to clear existing SQLite file '{output_path}': {e}")
627            })?;
628        }
629
630        let conn = SqliteConnectionBlocking::open(output_path)
631            .map_err(|e| anyhow::anyhow!("failed to open SQLite database '{output_path}': {e}"))?;
632
633        let mut used_names: HashSet<String> = HashSet::new();
634        for (index, item) in data.iter().enumerate() {
635            let table_name = unique_table_name(&item.name, index, &mut used_names);
636            match &item.content {
637                ExportDataContent::Table(table) => {
638                    write_sqlite_table(
639                        &conn,
640                        &table_name,
641                        &table.headers,
642                        &table.rows,
643                        &table.column_types,
644                    )?;
645                },
646                ExportDataContent::TimeSeries(ts) => {
647                    write_sqlite_timeseries(&conn, &table_name, ts, options)?;
648                },
649                other => {
650                    let json = serde_json::to_string(other)?;
651                    conn.execute(
652                        &format!("CREATE TABLE IF NOT EXISTS \"{table_name}\" (content TEXT)"),
653                        &[],
654                    )
655                    .map_err(|e| anyhow::anyhow!("CREATE TABLE '{table_name}' failed: {e}"))?;
656                    conn.execute(
657                        &format!(
658                            "INSERT INTO \"{table_name}\" (content) VALUES ({})",
659                            quote_sql_string(&json)
660                        ),
661                        &[],
662                    )
663                    .map_err(|e| anyhow::anyhow!("INSERT into '{table_name}' failed: {e}"))?;
664                },
665            }
666        }
667
668        Ok(())
669    }
670
671    /// Helper function to format values for CSV
672    fn format_value_for_csv(&self, value: &serde_json::Value, options: &ExportOptions) -> String {
673        match value {
674            serde_json::Value::Number(n) => {
675                if let Some(f) = n.as_f64() {
676                    format!(
677                        "{:.precision$}",
678                        f,
679                        precision = options.float_precision as usize
680                    )
681                } else {
682                    n.to_string()
683                }
684            },
685            serde_json::Value::String(s) => {
686                // Escape quotes and commas
687                if s.contains(',') || s.contains('"') || s.contains('\n') {
688                    format!("\"{}\"", s.replace('"', "\"\""))
689                } else {
690                    s.clone()
691                }
692            },
693            _ => value.to_string(),
694        }
695    }
696
697    /// Add export record to history
698    fn add_export_record(&mut self, job: &ExportJob) {
699        let record = ExportRecord {
700            id: Uuid::new_v4(),
701            job_id: job.id,
702            timestamp: Utc::now(),
703            file_path: job.output_path.clone(),
704            file_size: job.data_size,
705            format: job.format.clone(),
706            success: matches!(job.status, ExportStatus::Completed),
707            duration: job
708                .completed_at
709                .map(|end| (end - job.started_at).num_milliseconds() as f64 / 1000.0)
710                .unwrap_or(0.0),
711        };
712
713        self.export_history.push(record);
714    }
715
716    /// Get export job status
717    pub fn get_job_status(&self, job_id: Uuid) -> Option<&ExportJob> {
718        self.active_jobs.get(&job_id)
719    }
720
721    /// Get export history
722    pub fn get_export_history(&self) -> &[ExportRecord] {
723        &self.export_history
724    }
725
726    /// Create export template
727    pub fn create_template(
728        &mut self,
729        name: String,
730        description: String,
731        format: ExportFormat,
732        options: ExportOptions,
733        filters: DataFilters,
734        tags: Vec<String>,
735    ) -> String {
736        let template_id = Uuid::new_v4().to_string();
737
738        let template = ExportTemplate {
739            id: template_id.clone(),
740            name,
741            description,
742            format,
743            options,
744            filters,
745            tags,
746        };
747
748        self.config.templates.push(template);
749        template_id
750    }
751
752    /// Apply export template
753    pub fn apply_template(
754        &self,
755        template_id: &str,
756    ) -> Option<(&ExportFormat, &ExportOptions, &DataFilters)> {
757        self.config
758            .templates
759            .iter()
760            .find(|t| t.id == template_id)
761            .map(|t| (&t.format, &t.options, &t.filters))
762    }
763
764    /// Get supported formats
765    pub fn get_supported_formats(&self) -> &[ExportFormat] {
766        &self.supported_formats
767    }
768
769    /// Cancel export job
770    pub fn cancel_job(&mut self, job_id: Uuid) -> Result<()> {
771        if let Some(job) = self.active_jobs.get_mut(&job_id) {
772            if matches!(job.status, ExportStatus::Pending | ExportStatus::InProgress) {
773                job.status = ExportStatus::Cancelled;
774                Ok(())
775            } else {
776                Err(anyhow::anyhow!("Job cannot be cancelled in current status"))
777            }
778        } else {
779            Err(anyhow::anyhow!("Job not found"))
780        }
781    }
782
783    /// Get export statistics
784    pub fn get_export_statistics(&self) -> ExportStatistics {
785        let total_exports = self.export_history.len();
786        let successful_exports = self.export_history.iter().filter(|r| r.success).count();
787        let total_size: u64 = self.export_history.iter().map(|r| r.file_size).sum();
788        let avg_duration = if total_exports > 0 {
789            self.export_history.iter().map(|r| r.duration).sum::<f64>() / total_exports as f64
790        } else {
791            0.0
792        };
793
794        let format_stats: HashMap<ExportFormat, usize> =
795            self.export_history.iter().fold(HashMap::new(), |mut acc, record| {
796                *acc.entry(record.format.clone()).or_insert(0) += 1;
797                acc
798            });
799
800        ExportStatistics {
801            total_exports,
802            successful_exports,
803            failed_exports: total_exports - successful_exports,
804            total_size_bytes: total_size,
805            average_duration_seconds: avg_duration,
806            format_statistics: format_stats,
807            active_jobs: self.active_jobs.len(),
808        }
809    }
810}
811
812/// OOXML `[Content_Types].xml` part declaring the package content types.
813const XLSX_CONTENT_TYPES: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
814<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>"#;
815
816/// OOXML `_rels/.rels` package-level relationships part.
817const XLSX_ROOT_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
818<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>"#;
819
820/// OOXML `xl/workbook.xml` part defining a single worksheet.
821const XLSX_WORKBOOK: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
822<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>"#;
823
824/// OOXML `xl/_rels/workbook.xml.rels` part linking the workbook to its worksheet.
825const XLSX_WORKBOOK_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
826<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>"#;
827
828/// A single spreadsheet cell value to emit into a worksheet.
829enum XlsxCell {
830    /// Inline (shared-string-free) text cell, emitted as `<c t="inlineStr">`.
831    Inline(String),
832    /// Numeric cell, emitted as `<c><v>..</v></c>` with the pre-formatted literal.
833    Number(String),
834    /// Empty placeholder cell.
835    Empty,
836}
837
838/// XML-escape `&`, `<`, `>`, `"`, `'` for safe inclusion in OOXML parts.
839fn xlsx_xml_escape(input: &str) -> String {
840    let mut out = String::with_capacity(input.len());
841    for ch in input.chars() {
842        match ch {
843            '&' => out.push_str("&amp;"),
844            '<' => out.push_str("&lt;"),
845            '>' => out.push_str("&gt;"),
846            '"' => out.push_str("&quot;"),
847            '\'' => out.push_str("&apos;"),
848            _ => out.push(ch),
849        }
850    }
851    out
852}
853
854/// Convert a zero-based column index to an Excel column name (0 -> "A", 26 -> "AA").
855fn xlsx_column_name(index: usize) -> String {
856    let mut n = index + 1;
857    let mut name = String::new();
858    while n > 0 {
859        let rem = (n - 1) % 26;
860        name.insert(0, (b'A' + rem as u8) as char);
861        n = (n - 1) / 26;
862    }
863    name
864}
865
866/// Map a JSON value to a worksheet cell, mirroring `format_value_for_csv` numeric rules.
867fn xlsx_value_to_cell(value: &serde_json::Value, options: &ExportOptions) -> XlsxCell {
868    match value {
869        serde_json::Value::Number(n) => {
870            if let Some(f) = n.as_f64() {
871                XlsxCell::Number(format!(
872                    "{:.precision$}",
873                    f,
874                    precision = options.float_precision as usize
875                ))
876            } else {
877                XlsxCell::Number(n.to_string())
878            }
879        },
880        serde_json::Value::String(s) => XlsxCell::Inline(s.clone()),
881        _ => XlsxCell::Inline(value.to_string()),
882    }
883}
884
885/// Build the worksheet rows from the export data, mirroring `export_csv` layout.
886fn build_xlsx_rows(data: &[ExportableData], options: &ExportOptions) -> Result<Vec<Vec<XlsxCell>>> {
887    let mut rows: Vec<Vec<XlsxCell>> = Vec::new();
888    for item in data {
889        match &item.content {
890            ExportDataContent::Table(table_data) => {
891                if options.include_headers {
892                    rows.push(
893                        table_data.headers.iter().map(|h| XlsxCell::Inline(h.clone())).collect(),
894                    );
895                }
896                for row in &table_data.rows {
897                    rows.push(row.iter().map(|v| xlsx_value_to_cell(v, options)).collect());
898                }
899            },
900            ExportDataContent::TimeSeries(ts_data) => {
901                if options.include_headers {
902                    let mut header = vec![XlsxCell::Inline("timestamp".to_string())];
903                    header.extend(ts_data.series.keys().map(|k| XlsxCell::Inline(k.clone())));
904                    rows.push(header);
905                }
906                for (i, timestamp) in ts_data.timestamps.iter().enumerate() {
907                    let mut cells = vec![XlsxCell::Inline(
908                        timestamp.format(&options.date_format).to_string(),
909                    )];
910                    for series_name in ts_data.series.keys() {
911                        if let Some(series) = ts_data.series.get(series_name) {
912                            if let Some(value) = series.get(i) {
913                                cells.push(XlsxCell::Number(format!(
914                                    "{:.precision$}",
915                                    value,
916                                    precision = options.float_precision as usize
917                                )));
918                            } else {
919                                cells.push(XlsxCell::Empty);
920                            }
921                        }
922                    }
923                    rows.push(cells);
924                }
925            },
926            _ => {
927                let json_str = serde_json::to_string(&item.content)?;
928                rows.push(vec![XlsxCell::Inline(json_str)]);
929            },
930        }
931    }
932    Ok(rows)
933}
934
935/// Render worksheet rows into the `xl/worksheets/sheet1.xml` OOXML part.
936fn build_xlsx_sheet(rows: &[Vec<XlsxCell>]) -> String {
937    let mut sheet = String::new();
938    sheet.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
939    sheet.push_str(
940        "<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">",
941    );
942    sheet.push_str("<sheetData>");
943    for (row_idx, row) in rows.iter().enumerate() {
944        let row_num = row_idx + 1;
945        sheet.push_str(&format!("<row r=\"{row_num}\">"));
946        for (col_idx, cell) in row.iter().enumerate() {
947            let cell_ref = format!("{}{row_num}", xlsx_column_name(col_idx));
948            match cell {
949                XlsxCell::Inline(text) => {
950                    sheet.push_str(&format!(
951                        "<c r=\"{cell_ref}\" t=\"inlineStr\"><is><t xml:space=\"preserve\">{}</t></is></c>",
952                        xlsx_xml_escape(text)
953                    ));
954                },
955                XlsxCell::Number(num) => {
956                    sheet.push_str(&format!("<c r=\"{cell_ref}\"><v>{num}</v></c>"));
957                },
958                XlsxCell::Empty => {
959                    sheet.push_str(&format!("<c r=\"{cell_ref}\"/>"));
960                },
961            }
962        }
963        sheet.push_str("</row>");
964    }
965    sheet.push_str("</sheetData></worksheet>");
966    sheet
967}
968
969// ── SQLite export helpers ───────────────────────────────────────────────────────
970
971/// Sanitize an arbitrary name into a safe, double-quotable SQL identifier.
972///
973/// Non-alphanumeric characters become `_`; a leading non-alphabetic character is
974/// prefixed so the identifier is always valid.
975fn sanitize_sql_identifier(name: &str) -> String {
976    let mut sanitized: String = name
977        .chars()
978        .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
979        .collect();
980    let needs_prefix = sanitized
981        .chars()
982        .next()
983        .map(|c| !(c.is_ascii_alphabetic() || c == '_'))
984        .unwrap_or(true);
985    if needs_prefix {
986        sanitized = format!("t_{sanitized}");
987    }
988    sanitized
989}
990
991/// Produce a collision-free table name for the export, recording it in `used`.
992fn unique_table_name(name: &str, index: usize, used: &mut HashSet<String>) -> String {
993    let base = sanitize_sql_identifier(name);
994    if used.insert(base.clone()) {
995        return base;
996    }
997    let candidate = format!("{base}_{index}");
998    used.insert(candidate.clone());
999    candidate
1000}
1001
1002/// Quote and escape a string for use as a SQL string literal.
1003fn quote_sql_string(value: &str) -> String {
1004    format!("'{}'", value.replace('\'', "''"))
1005}
1006
1007/// Convert a single JSON value into an inline SQL literal.
1008///
1009/// * `null`  → `NULL`
1010/// * `bool`  → `1` / `0` (SQLite has no native boolean)
1011/// * number  → verbatim numeric literal
1012/// * string  → single-quote-escaped string literal
1013/// * array / object → JSON text stored as a string literal
1014fn json_value_to_sql_literal(value: &serde_json::Value) -> String {
1015    match value {
1016        serde_json::Value::Null => "NULL".to_string(),
1017        serde_json::Value::Bool(b) => if *b { "1" } else { "0" }.to_string(),
1018        serde_json::Value::Number(n) => n.to_string(),
1019        serde_json::Value::String(s) => quote_sql_string(s),
1020        other => quote_sql_string(&other.to_string()),
1021    }
1022}
1023
1024/// Infer a SQL column affinity (`INTEGER` / `REAL` / `TEXT`) from the data in a
1025/// column when no explicit [`ColumnType`] is supplied.
1026fn infer_sql_column_type(rows: &[Vec<serde_json::Value>], col: usize) -> &'static str {
1027    let mut integers = 0usize;
1028    let mut floats = 0usize;
1029    let mut bools = 0usize;
1030    let mut others = 0usize;
1031    for row in rows {
1032        match row.get(col) {
1033            None | Some(serde_json::Value::Null) => {},
1034            Some(serde_json::Value::Number(n)) => {
1035                if n.is_i64() || n.is_u64() {
1036                    integers += 1;
1037                } else {
1038                    floats += 1;
1039                }
1040            },
1041            Some(serde_json::Value::Bool(_)) => bools += 1,
1042            Some(_) => others += 1,
1043        }
1044    }
1045    if others > 0 {
1046        "TEXT"
1047    } else if floats > 0 {
1048        "REAL"
1049    } else if integers > 0 || bools > 0 {
1050        "INTEGER"
1051    } else {
1052        "TEXT"
1053    }
1054}
1055
1056/// Map an explicit [`ColumnType`] to its SQL affinity.
1057fn column_type_to_sql(column_type: &ColumnType) -> &'static str {
1058    match column_type {
1059        ColumnType::Integer | ColumnType::Boolean => "INTEGER",
1060        ColumnType::Float => "REAL",
1061        ColumnType::Binary => "BLOB",
1062        ColumnType::String | ColumnType::DateTime => "TEXT",
1063    }
1064}
1065
1066/// Create and populate a SQLite table from [`TableData`].
1067fn write_sqlite_table(
1068    conn: &SqliteConnectionBlocking,
1069    table_name: &str,
1070    headers: &[String],
1071    rows: &[Vec<serde_json::Value>],
1072    column_types: &HashMap<String, ColumnType>,
1073) -> Result<()> {
1074    if headers.is_empty() {
1075        return Ok(());
1076    }
1077
1078    let column_idents: Vec<String> = headers.iter().map(|h| sanitize_sql_identifier(h)).collect();
1079    let column_defs: Vec<String> = headers
1080        .iter()
1081        .enumerate()
1082        .map(|(idx, header)| {
1083            let sql_type = column_types
1084                .get(header)
1085                .map(column_type_to_sql)
1086                .unwrap_or_else(|| infer_sql_column_type(rows, idx));
1087            format!("\"{}\" {}", column_idents[idx], sql_type)
1088        })
1089        .collect();
1090
1091    let create = format!(
1092        "CREATE TABLE IF NOT EXISTS \"{table_name}\" ({})",
1093        column_defs.join(", ")
1094    );
1095    conn.execute(&create, &[])
1096        .map_err(|e| anyhow::anyhow!("CREATE TABLE '{table_name}' failed: {e}"))?;
1097
1098    let column_list =
1099        column_idents.iter().map(|c| format!("\"{c}\"")).collect::<Vec<_>>().join(", ");
1100
1101    for row in rows {
1102        let values: Vec<String> = (0..headers.len())
1103            .map(|idx| match row.get(idx) {
1104                Some(value) => json_value_to_sql_literal(value),
1105                None => "NULL".to_string(),
1106            })
1107            .collect();
1108        let insert = format!(
1109            "INSERT INTO \"{table_name}\" ({column_list}) VALUES ({})",
1110            values.join(", ")
1111        );
1112        conn.execute(&insert, &[])
1113            .map_err(|e| anyhow::anyhow!("INSERT into '{table_name}' failed: {e}"))?;
1114    }
1115
1116    Ok(())
1117}
1118
1119/// Create and populate a SQLite table from [`TimeSeriesData`].
1120fn write_sqlite_timeseries(
1121    conn: &SqliteConnectionBlocking,
1122    table_name: &str,
1123    ts: &TimeSeriesData,
1124    options: &ExportOptions,
1125) -> Result<()> {
1126    // Deterministic series ordering.
1127    let mut series_names: Vec<String> = ts.series.keys().cloned().collect();
1128    series_names.sort();
1129
1130    let mut column_defs = vec!["\"timestamp\" TEXT".to_string()];
1131    for name in &series_names {
1132        column_defs.push(format!("\"{}\" REAL", sanitize_sql_identifier(name)));
1133    }
1134    let create = format!(
1135        "CREATE TABLE IF NOT EXISTS \"{table_name}\" ({})",
1136        column_defs.join(", ")
1137    );
1138    conn.execute(&create, &[])
1139        .map_err(|e| anyhow::anyhow!("CREATE TABLE '{table_name}' failed: {e}"))?;
1140
1141    let mut column_list = vec!["\"timestamp\"".to_string()];
1142    for name in &series_names {
1143        column_list.push(format!("\"{}\"", sanitize_sql_identifier(name)));
1144    }
1145    let column_list = column_list.join(", ");
1146
1147    for (i, timestamp) in ts.timestamps.iter().enumerate() {
1148        let mut values = vec![quote_sql_string(
1149            &timestamp.format(&options.date_format).to_string(),
1150        )];
1151        for name in &series_names {
1152            match ts.series.get(name).and_then(|series| series.get(i)) {
1153                Some(value) => values.push(value.to_string()),
1154                None => values.push("NULL".to_string()),
1155            }
1156        }
1157        let insert = format!(
1158            "INSERT INTO \"{table_name}\" ({column_list}) VALUES ({})",
1159            values.join(", ")
1160        );
1161        conn.execute(&insert, &[])
1162            .map_err(|e| anyhow::anyhow!("INSERT into '{table_name}' failed: {e}"))?;
1163    }
1164
1165    Ok(())
1166}
1167
1168/// Export statistics
1169#[derive(Debug, Clone, Serialize, Deserialize)]
1170pub struct ExportStatistics {
1171    pub total_exports: usize,
1172    pub successful_exports: usize,
1173    pub failed_exports: usize,
1174    pub total_size_bytes: u64,
1175    pub average_duration_seconds: f64,
1176    pub format_statistics: HashMap<ExportFormat, usize>,
1177    pub active_jobs: usize,
1178}
1179
1180impl Default for ExportConfig {
1181    fn default() -> Self {
1182        Self {
1183            default_directory: "./exports".to_string(),
1184            max_file_size: 1024 * 1024 * 1024, // 1GB
1185            enable_compression: true,
1186            default_format: ExportFormat::Json,
1187            include_metadata: true,
1188            templates: Vec::new(),
1189        }
1190    }
1191}
1192
1193impl Default for ExportOptions {
1194    fn default() -> Self {
1195        Self {
1196            include_headers: true,
1197            date_format: "%Y-%m-%d %H:%M:%S UTC".to_string(),
1198            float_precision: 6,
1199            separator: ",".to_string(),
1200            compression_level: 6,
1201            include_metadata: true,
1202            custom_options: HashMap::new(),
1203        }
1204    }
1205}
1206
1207impl Default for DataFilters {
1208    fn default() -> Self {
1209        Self {
1210            date_range: None,
1211            data_types: vec![
1212                DataType::TensorData,
1213                DataType::GradientData,
1214                DataType::PerformanceMetrics,
1215            ],
1216            exclude_fields: Vec::new(),
1217            include_fields: None,
1218            custom_filters: HashMap::new(),
1219        }
1220    }
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225    use super::*;
1226    use tempfile::tempdir;
1227
1228    // These values are test data, not approximations of mathematical constants
1229    #[allow(clippy::approx_constant)]
1230    fn create_test_data() -> Vec<ExportableData> {
1231        let table_data = TableData {
1232            headers: vec![
1233                "id".to_string(),
1234                "value".to_string(),
1235                "timestamp".to_string(),
1236            ],
1237            rows: vec![
1238                vec![
1239                    serde_json::Value::Number(serde_json::Number::from(1)),
1240                    serde_json::Value::Number(
1241                        serde_json::Number::from_f64(3.14).expect("operation failed in test"),
1242                    ),
1243                    serde_json::Value::String("2023-01-01T12:00:00Z".to_string()),
1244                ],
1245                vec![
1246                    serde_json::Value::Number(serde_json::Number::from(2)),
1247                    serde_json::Value::Number(
1248                        serde_json::Number::from_f64(2.71).expect("operation failed in test"),
1249                    ),
1250                    serde_json::Value::String("2023-01-01T12:01:00Z".to_string()),
1251                ],
1252            ],
1253            column_types: HashMap::new(),
1254        };
1255
1256        vec![ExportableData {
1257            id: Uuid::new_v4(),
1258            name: "Test Data".to_string(),
1259            data_type: DataType::TensorData,
1260            timestamp: Utc::now(),
1261            content: ExportDataContent::Table(table_data),
1262            metadata: HashMap::new(),
1263            size: 1024,
1264        }]
1265    }
1266
1267    #[test]
1268    fn test_export_manager_creation() {
1269        let config = ExportConfig::default();
1270        let manager = DataExportManager::new(config);
1271
1272        assert!(manager.get_supported_formats().contains(&ExportFormat::Json));
1273        assert!(manager.get_supported_formats().contains(&ExportFormat::Csv));
1274    }
1275
1276    #[test]
1277    fn test_csv_export() {
1278        let config = ExportConfig::default();
1279        let mut manager = DataExportManager::new(config);
1280        let test_data = create_test_data();
1281
1282        let temp_dir = tempdir().expect("temp file creation failed");
1283        let output_path = temp_dir.path().join("test.csv").to_string_lossy().to_string();
1284
1285        let job_id = manager
1286            .start_export(
1287                "Test CSV Export".to_string(),
1288                test_data,
1289                ExportFormat::Csv,
1290                output_path.clone(),
1291                ExportOptions::default(),
1292            )
1293            .expect("operation failed in test");
1294
1295        // Check job was created
1296        assert!(manager.active_jobs.contains_key(&job_id));
1297
1298        // Check file was created
1299        assert!(std::path::Path::new(&output_path).exists());
1300    }
1301
1302    #[test]
1303    fn test_json_export() {
1304        let config = ExportConfig::default();
1305        let mut manager = DataExportManager::new(config);
1306        let test_data = create_test_data();
1307
1308        let temp_dir = tempdir().expect("temp file creation failed");
1309        let output_path = temp_dir.path().join("test.json").to_string_lossy().to_string();
1310
1311        let job_id = manager
1312            .start_export(
1313                "Test JSON Export".to_string(),
1314                test_data,
1315                ExportFormat::Json,
1316                output_path.clone(),
1317                ExportOptions::default(),
1318            )
1319            .expect("operation failed in test");
1320
1321        assert!(manager.active_jobs.contains_key(&job_id));
1322        assert!(std::path::Path::new(&output_path).exists());
1323    }
1324
1325    #[test]
1326    fn test_export_template() {
1327        let config = ExportConfig::default();
1328        let mut manager = DataExportManager::new(config);
1329
1330        let template_id = manager.create_template(
1331            "CSV Template".to_string(),
1332            "Standard CSV export".to_string(),
1333            ExportFormat::Csv,
1334            ExportOptions::default(),
1335            DataFilters::default(),
1336            vec!["csv".to_string(), "standard".to_string()],
1337        );
1338
1339        let (format, options, _filters) =
1340            manager.apply_template(&template_id).expect("temp file creation failed");
1341        assert_eq!(*format, ExportFormat::Csv);
1342        assert!(options.include_headers);
1343    }
1344
1345    #[test]
1346    fn test_export_statistics() {
1347        let config = ExportConfig::default();
1348        let mut manager = DataExportManager::new(config);
1349
1350        // Add some mock export records
1351        manager.export_history.push(ExportRecord {
1352            id: Uuid::new_v4(),
1353            job_id: Uuid::new_v4(),
1354            timestamp: Utc::now(),
1355            file_path: "test1.csv".to_string(),
1356            file_size: 1024,
1357            format: ExportFormat::Csv,
1358            success: true,
1359            duration: 2.5,
1360        });
1361
1362        manager.export_history.push(ExportRecord {
1363            id: Uuid::new_v4(),
1364            job_id: Uuid::new_v4(),
1365            timestamp: Utc::now(),
1366            file_path: "test2.json".to_string(),
1367            file_size: 2048,
1368            format: ExportFormat::Json,
1369            success: true,
1370            duration: 1.8,
1371        });
1372
1373        let stats = manager.get_export_statistics();
1374        assert_eq!(stats.total_exports, 2);
1375        assert_eq!(stats.successful_exports, 2);
1376        assert_eq!(stats.total_size_bytes, 3072);
1377    }
1378
1379    #[test]
1380    fn test_excel_export_roundtrip() {
1381        use oxiarc_archive::zip::ZipReader;
1382        use std::io::Cursor;
1383
1384        let config = ExportConfig::default();
1385        let mut manager = DataExportManager::new(config);
1386        let test_data = create_test_data();
1387
1388        let file_name = format!("trustformers_xlsx_test_{}.xlsx", Uuid::new_v4());
1389        let output_path = std::env::temp_dir().join(file_name).to_string_lossy().to_string();
1390
1391        manager
1392            .start_export(
1393                "Test Excel Export".to_string(),
1394                test_data,
1395                ExportFormat::Excel,
1396                output_path.clone(),
1397                ExportOptions::default(),
1398            )
1399            .expect("excel export should succeed");
1400
1401        assert!(std::path::Path::new(&output_path).exists());
1402
1403        let bytes = std::fs::read(&output_path).expect("read xlsx bytes");
1404        let mut reader = ZipReader::new(Cursor::new(bytes)).expect("open xlsx as zip");
1405        let entries = reader.entries().to_vec();
1406        let names: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
1407
1408        assert!(
1409            names.iter().any(|n| n == "[Content_Types].xml"),
1410            "missing [Content_Types].xml; entries = {names:?}"
1411        );
1412        assert!(
1413            names.iter().any(|n| n == "xl/workbook.xml"),
1414            "missing xl/workbook.xml; entries = {names:?}"
1415        );
1416        assert!(
1417            names.iter().any(|n| n == "xl/worksheets/sheet1.xml"),
1418            "missing xl/worksheets/sheet1.xml; entries = {names:?}"
1419        );
1420
1421        let sheet_entry = entries
1422            .iter()
1423            .find(|e| e.name == "xl/worksheets/sheet1.xml")
1424            .expect("sheet1.xml entry");
1425        let sheet_bytes = reader.extract(sheet_entry).expect("extract sheet1.xml");
1426        let sheet_xml = String::from_utf8(sheet_bytes).expect("sheet1.xml is utf8");
1427
1428        assert!(sheet_xml.contains("<worksheet"));
1429        // Header text from create_test_data(): "id", "value", "timestamp".
1430        assert!(sheet_xml.contains("id"), "sheet missing header text");
1431        assert!(sheet_xml.contains("value"), "sheet missing header text");
1432        assert!(sheet_xml.contains("timestamp"), "sheet missing header text");
1433
1434        let _ = std::fs::remove_file(&output_path);
1435    }
1436
1437    #[test]
1438    // 3.14 is test data from create_test_data(), not an approximation of PI.
1439    #[allow(clippy::approx_constant)]
1440    fn test_sqlite_export_roundtrip() {
1441        let config = ExportConfig::default();
1442        let mut manager = DataExportManager::new(config);
1443        let test_data = create_test_data();
1444
1445        let file_name = format!("trustformers_sqlite_test_{}.sqlite3", Uuid::new_v4());
1446        let output_path = std::env::temp_dir().join(file_name).to_string_lossy().to_string();
1447
1448        let job_id = manager
1449            .start_export(
1450                "Test SQLite Export".to_string(),
1451                test_data,
1452                ExportFormat::Sqlite,
1453                output_path.clone(),
1454                ExportOptions::default(),
1455            )
1456            .expect("sqlite export should succeed");
1457
1458        // The job must have completed (not silently failed back to JSON).
1459        let status = manager.get_job_status(job_id).expect("job should exist");
1460        assert!(
1461            matches!(status.status, ExportStatus::Completed),
1462            "export status = {:?}",
1463            status.status
1464        );
1465        assert!(
1466            std::path::Path::new(&output_path).exists(),
1467            "sqlite file should exist"
1468        );
1469
1470        // Re-open the real SQLite file and read the rows back.
1471        let conn = SqliteConnectionBlocking::open(&output_path).expect("open sqlite file");
1472        let tables = conn.tables().expect("list tables");
1473        assert!(!tables.is_empty(), "expected at least one table");
1474
1475        // create_test_data() names the item "Test Data" -> sanitized "Test_Data".
1476        let rows = conn
1477            .query(
1478                "SELECT \"id\", \"value\", \"timestamp\" FROM \"Test_Data\"",
1479                &[],
1480            )
1481            .expect("query rows back");
1482        assert_eq!(rows.len(), 2, "two rows should persist");
1483
1484        // Collect ids to verify both rows persisted regardless of row order.
1485        let mut ids: Vec<i64> = rows
1486            .iter()
1487            .map(|r| r.try_get::<i64>("id").expect("id column is INTEGER"))
1488            .collect();
1489        ids.sort_unstable();
1490        assert_eq!(ids, vec![1, 2]);
1491
1492        // Find the row with id == 1 and verify the float + string columns.
1493        let first = rows
1494            .iter()
1495            .find(|r| r.try_get::<i64>("id").map(|v| v == 1).unwrap_or(false))
1496            .expect("row with id=1");
1497        let value: f64 = first.try_get("value").expect("value column is REAL");
1498        assert!((value - 3.14).abs() < 1e-9, "value = {value}");
1499        let timestamp: String = first.try_get("timestamp").expect("timestamp column is TEXT");
1500        assert_eq!(timestamp, "2023-01-01T12:00:00Z");
1501
1502        let _ = std::fs::remove_file(&output_path);
1503    }
1504}