1use 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#[derive(Debug, Clone)]
15pub struct DataExportManager {
16 config: ExportConfig,
18 active_jobs: HashMap<Uuid, ExportJob>,
20 export_history: Vec<ExportRecord>,
22 supported_formats: Vec<ExportFormat>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ExportConfig {
29 pub default_directory: String,
31 pub max_file_size: u64,
33 pub enable_compression: bool,
35 pub default_format: ExportFormat,
37 pub include_metadata: bool,
39 pub templates: Vec<ExportTemplate>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ExportJob {
46 pub id: Uuid,
48 pub name: String,
50 pub format: ExportFormat,
52 pub output_path: String,
54 pub status: ExportStatus,
56 pub progress: f64,
58 pub started_at: DateTime<Utc>,
60 pub completed_at: Option<DateTime<Utc>>,
62 pub data_size: u64,
64 pub error_message: Option<String>,
66 pub options: ExportOptions,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ExportRecord {
73 pub id: Uuid,
75 pub job_id: Uuid,
77 pub timestamp: DateTime<Utc>,
79 pub file_path: String,
81 pub file_size: u64,
83 pub format: ExportFormat,
85 pub success: bool,
87 pub duration: f64,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct ExportTemplate {
94 pub id: String,
96 pub name: String,
98 pub description: String,
100 pub format: ExportFormat,
102 pub options: ExportOptions,
104 pub filters: DataFilters,
106 pub tags: Vec<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ExportOptions {
113 pub include_headers: bool,
115 pub date_format: String,
117 pub float_precision: u32,
119 pub separator: String,
121 pub compression_level: u32,
123 pub include_metadata: bool,
125 pub custom_options: HashMap<String, serde_json::Value>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct DataFilters {
132 pub date_range: Option<DateRange>,
134 pub data_types: Vec<DataType>,
136 pub exclude_fields: Vec<String>,
138 pub include_fields: Option<Vec<String>>,
140 pub custom_filters: HashMap<String, serde_json::Value>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct DateRange {
147 pub start: DateTime<Utc>,
149 pub end: DateTime<Utc>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Hash, Eq)]
155pub enum ExportFormat {
156 Csv,
158 Excel,
160 Json,
162 JsonPretty,
164 Hdf5,
166 Parquet,
168 Xml,
170 Yaml,
172 Sqlite,
174 MessagePack,
176 Arrow,
178 Custom(String),
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub enum ExportStatus {
185 Pending,
186 InProgress,
187 Completed,
188 Failed,
189 Cancelled,
190}
191
192#[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#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct ExportableData {
211 pub id: Uuid,
213 pub name: String,
215 pub data_type: DataType,
217 pub timestamp: DateTime<Utc>,
219 pub content: ExportDataContent,
221 pub metadata: HashMap<String, serde_json::Value>,
223 pub size: u64,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub enum ExportDataContent {
230 Table(TableData),
232 TimeSeries(TimeSeriesData),
234 KeyValue(HashMap<String, serde_json::Value>),
236 Structured(serde_json::Value),
238 Binary(Vec<u8>),
240 Text(String),
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct TableData {
247 pub headers: Vec<String>,
249 pub rows: Vec<Vec<serde_json::Value>>,
251 pub column_types: HashMap<String, ColumnType>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct TimeSeriesData {
258 pub timestamps: Vec<DateTime<Utc>>,
260 pub series: HashMap<String, Vec<f64>>,
262 pub metadata: HashMap<String, String>,
264}
265
266#[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 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 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 let data_size: u64 = data.iter().map(|d| d.size).sum();
311
312 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 self.execute_export(job_id, data, options)?;
335
336 Ok(job_id)
337 }
338
339 fn execute_export(
341 &mut self,
342 job_id: Uuid,
343 data: Vec<ExportableData>,
344 options: ExportOptions,
345 ) -> Result<()> {
346 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 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 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 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 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 if options.include_headers {
425 let header_line = table_data.headers.join(&options.separator);
426 writeln!(file, "{}", header_line)?;
427 }
428
429 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 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 let json_str = serde_json::to_string(&item.content)?;
468 writeln!(file, "{}", json_str)?;
469 },
470 }
471 }
472
473 Ok(())
474 }
475
476 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 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 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 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 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 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 fn export_sqlite(
613 &mut self,
614 data: &[ExportableData],
615 output_path: &str,
616 options: &ExportOptions,
617 ) -> Result<()> {
618 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 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 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 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 pub fn get_job_status(&self, job_id: Uuid) -> Option<&ExportJob> {
713 self.active_jobs.get(&job_id)
714 }
715
716 pub fn get_export_history(&self) -> &[ExportRecord] {
718 &self.export_history
719 }
720
721 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 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 pub fn get_supported_formats(&self) -> &[ExportFormat] {
761 &self.supported_formats
762 }
763
764 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 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
807const 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
811const 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
815const 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
819const 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
823enum XlsxCell {
825 Inline(String),
827 Number(String),
829 Empty,
831}
832
833fn 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("&"),
839 '<' => out.push_str("<"),
840 '>' => out.push_str(">"),
841 '"' => out.push_str("""),
842 '\'' => out.push_str("'"),
843 _ => out.push(ch),
844 }
845 }
846 out
847}
848
849fn 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
861fn 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
880fn 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
930fn 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
964fn 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
986fn 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
997fn quote_sql_string(value: &str) -> String {
999 format!("'{}'", value.replace('\'', "''"))
1000}
1001
1002fn 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
1019fn 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
1051fn 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
1061fn 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
1114fn write_sqlite_timeseries(
1116 conn: &SqliteConnectionBlocking,
1117 table_name: &str,
1118 ts: &TimeSeriesData,
1119 options: &ExportOptions,
1120) -> Result<()> {
1121 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 ×tamp.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#[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, 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 #[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 assert!(manager.active_jobs.contains_key(&job_id));
1292
1293 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 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 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 #[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 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 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 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 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 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}