Skip to main content

scirs2_io/
universal_reader.rs

1//! Universal data reader with auto-format detection
2//!
3//! Provides a unified interface for reading data files regardless of format.
4//! The reader auto-detects the file format using [`crate::format_detect`] and
5//! dispatches to the appropriate format-specific reader.
6//!
7//! All data is returned as a [`DataTable`], a format-agnostic columnar representation
8//! with metadata support.
9//!
10//! # Supported Formats
11//!
12//! - CSV (with configurable delimiter/header)
13//! - Arrow IPC (columnar binary)
14//! - NetCDF Classic (via `netcdf_lite`)
15//! - HDF5 (pure Rust, via `oxih5`)
16//! - NPY/NPZ (NumPy binary)
17//! - Matrix Market (sparse/dense)
18//! - JSON (array of objects)
19//! - WAV (audio data as numeric columns)
20//!
21//! # Example
22//!
23//! ```rust,no_run
24//! use scirs2_io::universal_reader::{read_data, ReadOptions, DataTable};
25//!
26//! // Auto-detect and read
27//! let table = read_data("measurements.csv", None)?;
28//! println!("Columns: {:?}", table.column_names());
29//! println!("Rows: {}", table.num_rows());
30//!
31//! // Access column data
32//! if let Some(col) = table.column("temperature") {
33//!     let values = col.as_f64();
34//!     println!("Temperature values: {} entries", values.len());
35//! }
36//! # Ok::<(), scirs2_io::error::IoError>(())
37//! ```
38
39use crate::error::{IoError, Result};
40use crate::format_detect::{detect_format, DataFormat};
41use std::collections::HashMap;
42use std::path::Path;
43
44// =====================================================================
45// Core data types
46// =====================================================================
47
48/// A column of data in the unified table representation
49#[derive(Debug, Clone)]
50pub enum DataColumn {
51    /// 32-bit integer column
52    Int32(Vec<i32>),
53    /// 64-bit integer column
54    Int64(Vec<i64>),
55    /// 32-bit float column
56    Float32(Vec<f32>),
57    /// 64-bit float column
58    Float64(Vec<f64>),
59    /// String column
60    String(Vec<String>),
61    /// Boolean column
62    Boolean(Vec<bool>),
63    /// Raw bytes column (for opaque data)
64    Bytes(Vec<Vec<u8>>),
65}
66
67impl DataColumn {
68    /// Number of elements
69    pub fn len(&self) -> usize {
70        match self {
71            Self::Int32(v) => v.len(),
72            Self::Int64(v) => v.len(),
73            Self::Float32(v) => v.len(),
74            Self::Float64(v) => v.len(),
75            Self::String(v) => v.len(),
76            Self::Boolean(v) => v.len(),
77            Self::Bytes(v) => v.len(),
78        }
79    }
80
81    /// Whether the column is empty
82    pub fn is_empty(&self) -> bool {
83        self.len() == 0
84    }
85
86    /// Convert to f64 values (non-numeric types return empty vec)
87    pub fn as_f64(&self) -> Vec<f64> {
88        match self {
89            Self::Int32(v) => v.iter().map(|x| *x as f64).collect(),
90            Self::Int64(v) => v.iter().map(|x| *x as f64).collect(),
91            Self::Float32(v) => v.iter().map(|x| *x as f64).collect(),
92            Self::Float64(v) => v.clone(),
93            Self::Boolean(v) => v.iter().map(|x| if *x { 1.0 } else { 0.0 }).collect(),
94            _ => Vec::new(),
95        }
96    }
97
98    /// Convert to string values
99    pub fn as_strings(&self) -> Vec<String> {
100        match self {
101            Self::Int32(v) => v.iter().map(|x| x.to_string()).collect(),
102            Self::Int64(v) => v.iter().map(|x| x.to_string()).collect(),
103            Self::Float32(v) => v.iter().map(|x| x.to_string()).collect(),
104            Self::Float64(v) => v.iter().map(|x| x.to_string()).collect(),
105            Self::String(v) => v.clone(),
106            Self::Boolean(v) => v.iter().map(|x| x.to_string()).collect(),
107            Self::Bytes(v) => v.iter().map(|b| format!("<{} bytes>", b.len())).collect(),
108        }
109    }
110
111    /// Type name for display
112    pub fn type_name(&self) -> &'static str {
113        match self {
114            Self::Int32(_) => "int32",
115            Self::Int64(_) => "int64",
116            Self::Float32(_) => "float32",
117            Self::Float64(_) => "float64",
118            Self::String(_) => "string",
119            Self::Boolean(_) => "bool",
120            Self::Bytes(_) => "bytes",
121        }
122    }
123}
124
125/// A unified data table with named columns and metadata
126#[derive(Debug, Clone)]
127pub struct DataTable {
128    /// Column names in order
129    column_names: Vec<String>,
130    /// Column data keyed by name
131    columns: HashMap<String, DataColumn>,
132    /// File-level metadata
133    metadata: HashMap<String, String>,
134    /// Source format that was read
135    source_format: DataFormat,
136}
137
138impl DataTable {
139    /// Create a new empty data table
140    pub fn new(source_format: DataFormat) -> Self {
141        Self {
142            column_names: Vec::new(),
143            columns: HashMap::new(),
144            metadata: HashMap::new(),
145            source_format,
146        }
147    }
148
149    /// Add a column to the table
150    pub fn add_column(&mut self, name: &str, data: DataColumn) {
151        if !self.columns.contains_key(name) {
152            self.column_names.push(name.to_string());
153        }
154        self.columns.insert(name.to_string(), data);
155    }
156
157    /// Add metadata
158    pub fn set_metadata(&mut self, key: &str, value: &str) {
159        self.metadata.insert(key.to_string(), value.to_string());
160    }
161
162    /// Get column names in order
163    pub fn column_names(&self) -> &[String] {
164        &self.column_names
165    }
166
167    /// Get a column by name
168    pub fn column(&self, name: &str) -> Option<&DataColumn> {
169        self.columns.get(name)
170    }
171
172    /// Get a column by index
173    pub fn column_by_index(&self, index: usize) -> Option<&DataColumn> {
174        self.column_names
175            .get(index)
176            .and_then(|name| self.columns.get(name))
177    }
178
179    /// Number of columns
180    pub fn num_columns(&self) -> usize {
181        self.column_names.len()
182    }
183
184    /// Number of rows (from first column, or 0 if empty)
185    pub fn num_rows(&self) -> usize {
186        self.column_names
187            .first()
188            .and_then(|name| self.columns.get(name))
189            .map_or(0, |col| col.len())
190    }
191
192    /// Get metadata value
193    pub fn metadata(&self, key: &str) -> Option<&str> {
194        self.metadata.get(key).map(|s| s.as_str())
195    }
196
197    /// Get all metadata
198    pub fn all_metadata(&self) -> &HashMap<String, String> {
199        &self.metadata
200    }
201
202    /// Source format
203    pub fn source_format(&self) -> DataFormat {
204        self.source_format
205    }
206
207    /// Whether the table is empty
208    pub fn is_empty(&self) -> bool {
209        self.num_rows() == 0
210    }
211
212    /// Get a summary of the table for display
213    pub fn summary(&self) -> String {
214        let mut lines = Vec::new();
215        lines.push(format!(
216            "DataTable: {} columns x {} rows (from {})",
217            self.num_columns(),
218            self.num_rows(),
219            self.source_format
220        ));
221        for name in &self.column_names {
222            if let Some(col) = self.columns.get(name) {
223                lines.push(format!(
224                    "  {}: {} ({} values)",
225                    name,
226                    col.type_name(),
227                    col.len()
228                ));
229            }
230        }
231        lines.join("\n")
232    }
233
234    /// Select a subset of columns
235    pub fn select_columns(&self, names: &[&str]) -> Result<DataTable> {
236        let mut table = DataTable::new(self.source_format);
237        for &name in names {
238            let col = self
239                .column(name)
240                .ok_or_else(|| IoError::NotFound(format!("Column '{name}' not found")))?;
241            table.add_column(name, col.clone());
242        }
243        table.metadata = self.metadata.clone();
244        Ok(table)
245    }
246
247    /// Slice rows
248    pub fn slice_rows(&self, start: usize, end: usize) -> DataTable {
249        let mut table = DataTable::new(self.source_format);
250        for name in &self.column_names {
251            if let Some(col) = self.columns.get(name) {
252                let sliced = slice_column(col, start, end);
253                table.add_column(name, sliced);
254            }
255        }
256        table.metadata = self.metadata.clone();
257        table
258    }
259}
260
261/// Options for reading data
262#[derive(Debug, Clone, Default)]
263pub struct ReadOptions {
264    /// Override auto-detected format
265    pub format: Option<DataFormat>,
266    /// Maximum number of rows to read (None = all)
267    pub max_rows: Option<usize>,
268    /// Specific columns to read (None = all)
269    pub columns: Option<Vec<String>>,
270    /// CSV delimiter override
271    pub csv_delimiter: Option<char>,
272    /// Whether CSV has header row
273    pub csv_has_header: Option<bool>,
274    /// HDF5 dataset path to read
275    pub hdf5_dataset: Option<String>,
276    /// NetCDF variable name to read
277    pub netcdf_variable: Option<String>,
278}
279
280// =====================================================================
281// Main read functions
282// =====================================================================
283
284/// Read a data file, auto-detecting the format.
285///
286/// Returns a [`DataTable`] with columns and metadata regardless of the
287/// source format. The detected format is stored in `table.source_format()`.
288pub fn read_data<P: AsRef<Path>>(path: P, options: Option<ReadOptions>) -> Result<DataTable> {
289    let path = path.as_ref();
290    let opts = options.unwrap_or_default();
291
292    let format = if let Some(fmt) = opts.format {
293        fmt
294    } else {
295        detect_format(path)?
296    };
297
298    match format {
299        DataFormat::Csv => read_csv_to_table(path, &opts),
300        DataFormat::ArrowIpc => read_arrow_to_table(path, &opts),
301        DataFormat::Json => read_json_to_table(path, &opts),
302        DataFormat::Wav => read_wav_to_table(path, &opts),
303        DataFormat::Npy => read_npy_to_table(path, &opts),
304        DataFormat::MatrixMarket => read_mtx_to_table(path, &opts),
305        DataFormat::Hdf5 => read_hdf5_to_table(path, &opts),
306        DataFormat::NetCdf => read_netcdf_to_table(path, &opts),
307        _ => Err(IoError::UnsupportedFormat(format!(
308            "Universal reader does not support {} format yet",
309            format.name()
310        ))),
311    }
312}
313
314// =====================================================================
315// Streaming reader interface
316// =====================================================================
317
318/// A streaming reader that yields chunks of data
319pub struct StreamingReader {
320    /// Chunks of data already read
321    chunks: Vec<DataTable>,
322    /// Current chunk index
323    current: usize,
324}
325
326impl StreamingReader {
327    /// Create a streaming reader for a file
328    pub fn open<P: AsRef<Path>>(
329        path: P,
330        chunk_size: usize,
331        options: Option<ReadOptions>,
332    ) -> Result<Self> {
333        // For now, read the full table and split into chunks
334        let full = read_data(path, options)?;
335        let total_rows = full.num_rows();
336
337        let mut chunks = Vec::new();
338        let mut offset = 0;
339        while offset < total_rows {
340            let end = (offset + chunk_size).min(total_rows);
341            chunks.push(full.slice_rows(offset, end));
342            offset = end;
343        }
344
345        if chunks.is_empty() {
346            chunks.push(full);
347        }
348
349        Ok(Self { chunks, current: 0 })
350    }
351
352    /// Read the next chunk (returns None when exhausted)
353    pub fn next_chunk(&mut self) -> Option<&DataTable> {
354        if self.current < self.chunks.len() {
355            let chunk = &self.chunks[self.current];
356            self.current += 1;
357            Some(chunk)
358        } else {
359            None
360        }
361    }
362
363    /// Reset to the beginning
364    pub fn reset(&mut self) {
365        self.current = 0;
366    }
367
368    /// Total number of chunks
369    pub fn num_chunks(&self) -> usize {
370        self.chunks.len()
371    }
372
373    /// Total rows across all chunks
374    pub fn total_rows(&self) -> usize {
375        self.chunks.iter().map(|c| c.num_rows()).sum()
376    }
377}
378
379// =====================================================================
380// Format-specific readers
381// =====================================================================
382
383/// Read CSV into DataTable
384fn read_csv_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
385    let config = crate::csv::CsvReaderConfig {
386        has_header: opts.csv_has_header.unwrap_or(true),
387        delimiter: opts.csv_delimiter.unwrap_or(','),
388        ..Default::default()
389    };
390
391    let (headers, data) = crate::csv::read_csv(path, Some(config))?;
392
393    let mut table = DataTable::new(DataFormat::Csv);
394    table.set_metadata("source_file", &path.display().to_string());
395
396    if headers.is_empty() {
397        return Ok(table);
398    }
399
400    let nrows = data.nrows();
401    let ncols = data.ncols();
402
403    // Determine column types by trying to parse values
404    for (col_idx, header) in headers.iter().enumerate() {
405        if col_idx >= ncols {
406            break;
407        }
408
409        let col_name = if header.is_empty() {
410            format!("column_{col_idx}")
411        } else {
412            header.clone()
413        };
414
415        // Check if requested columns filter applies
416        if let Some(ref wanted) = opts.columns {
417            if !wanted.iter().any(|w| w == &col_name) {
418                continue;
419            }
420        }
421
422        let max = opts.max_rows.unwrap_or(usize::MAX);
423        let row_count = nrows.min(max);
424
425        let values: Vec<&str> = (0..row_count)
426            .map(|r| data[[r, col_idx]].as_str())
427            .collect();
428
429        // Try int64 first
430        let as_int: std::result::Result<Vec<i64>, _> = values
431            .iter()
432            .map(|s: &&str| s.trim().parse::<i64>())
433            .collect();
434
435        if let Ok(ints) = as_int {
436            table.add_column(&col_name, DataColumn::Int64(ints));
437            continue;
438        }
439
440        // Try f64
441        let as_float: std::result::Result<Vec<f64>, _> = values
442            .iter()
443            .map(|s: &&str| s.trim().parse::<f64>())
444            .collect();
445
446        if let Ok(floats) = as_float {
447            table.add_column(&col_name, DataColumn::Float64(floats));
448            continue;
449        }
450
451        // Try boolean
452        let as_bool: std::result::Result<Vec<bool>, _> = values
453            .iter()
454            .map(|s: &&str| match s.trim().to_lowercase().as_str() {
455                "true" | "1" | "yes" => Ok(true),
456                "false" | "0" | "no" => Ok(false),
457                _ => Err(()),
458            })
459            .collect();
460
461        if let Ok(bools) = as_bool {
462            table.add_column(&col_name, DataColumn::Boolean(bools));
463            continue;
464        }
465
466        // Default to string
467        let strings: Vec<String> = values.iter().map(|s: &&str| s.to_string()).collect();
468        table.add_column(&col_name, DataColumn::String(strings));
469    }
470
471    Ok(table)
472}
473
474/// Read Arrow IPC into DataTable
475fn read_arrow_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
476    let (schema, batches) = crate::arrow_ipc::read_arrow_ipc_file(path)?;
477
478    let mut table = DataTable::new(DataFormat::ArrowIpc);
479    table.set_metadata("source_file", &path.display().to_string());
480
481    for (key, val) in &schema.metadata {
482        table.set_metadata(key, val);
483    }
484
485    // Merge all batches into single columns
486    for (idx, field) in schema.fields.iter().enumerate() {
487        if let Some(ref wanted) = opts.columns {
488            if !wanted.iter().any(|w| w == &field.name) {
489                continue;
490            }
491        }
492
493        let col = merge_arrow_columns(&batches, idx, &field.dtype, opts.max_rows)?;
494        table.add_column(&field.name, col);
495    }
496
497    Ok(table)
498}
499
500/// Merge Arrow columns across batches
501fn merge_arrow_columns(
502    batches: &[crate::arrow_ipc::RecordBatch],
503    col_idx: usize,
504    dtype: &crate::arrow_ipc::ArrowDataType,
505    max_rows: Option<usize>,
506) -> Result<DataColumn> {
507    let max = max_rows.unwrap_or(usize::MAX);
508
509    match dtype {
510        crate::arrow_ipc::ArrowDataType::Int32 => {
511            let mut values = Vec::new();
512            for batch in batches {
513                if values.len() >= max {
514                    break;
515                }
516                if let Some(crate::arrow_ipc::ArrowColumn::Int32(v)) = batch.column(col_idx) {
517                    let remaining = max - values.len();
518                    values.extend_from_slice(&v[..v.len().min(remaining)]);
519                }
520            }
521            Ok(DataColumn::Int32(values))
522        }
523        crate::arrow_ipc::ArrowDataType::Int64 => {
524            let mut values = Vec::new();
525            for batch in batches {
526                if values.len() >= max {
527                    break;
528                }
529                if let Some(crate::arrow_ipc::ArrowColumn::Int64(v)) = batch.column(col_idx) {
530                    let remaining = max - values.len();
531                    values.extend_from_slice(&v[..v.len().min(remaining)]);
532                }
533            }
534            Ok(DataColumn::Int64(values))
535        }
536        crate::arrow_ipc::ArrowDataType::Float32 => {
537            let mut values = Vec::new();
538            for batch in batches {
539                if values.len() >= max {
540                    break;
541                }
542                if let Some(crate::arrow_ipc::ArrowColumn::Float32(v)) = batch.column(col_idx) {
543                    let remaining = max - values.len();
544                    values.extend_from_slice(&v[..v.len().min(remaining)]);
545                }
546            }
547            Ok(DataColumn::Float32(values))
548        }
549        crate::arrow_ipc::ArrowDataType::Float64 => {
550            let mut values = Vec::new();
551            for batch in batches {
552                if values.len() >= max {
553                    break;
554                }
555                if let Some(crate::arrow_ipc::ArrowColumn::Float64(v)) = batch.column(col_idx) {
556                    let remaining = max - values.len();
557                    values.extend_from_slice(&v[..v.len().min(remaining)]);
558                }
559            }
560            Ok(DataColumn::Float64(values))
561        }
562        crate::arrow_ipc::ArrowDataType::Utf8 => {
563            let mut values = Vec::new();
564            for batch in batches {
565                if values.len() >= max {
566                    break;
567                }
568                if let Some(crate::arrow_ipc::ArrowColumn::Utf8(v)) = batch.column(col_idx) {
569                    let remaining = max - values.len();
570                    values.extend_from_slice(&v[..v.len().min(remaining)]);
571                }
572            }
573            Ok(DataColumn::String(values))
574        }
575        crate::arrow_ipc::ArrowDataType::Boolean => {
576            let mut values = Vec::new();
577            for batch in batches {
578                if values.len() >= max {
579                    break;
580                }
581                if let Some(crate::arrow_ipc::ArrowColumn::Boolean(v)) = batch.column(col_idx) {
582                    let remaining = max - values.len();
583                    values.extend_from_slice(&v[..v.len().min(remaining)]);
584                }
585            }
586            Ok(DataColumn::Boolean(values))
587        }
588    }
589}
590
591/// Read JSON (array of objects) into DataTable
592fn read_json_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
593    let content = std::fs::read_to_string(path)
594        .map_err(|e| IoError::FileError(format!("Cannot read '{}': {e}", path.display())))?;
595
596    let parsed: serde_json::Value = serde_json::from_str(&content)
597        .map_err(|e| IoError::FormatError(format!("JSON parse error: {e}")))?;
598
599    let mut table = DataTable::new(DataFormat::Json);
600    table.set_metadata("source_file", &path.display().to_string());
601
602    let arr = match &parsed {
603        serde_json::Value::Array(a) => a,
604        serde_json::Value::Object(_) => {
605            // Single object: wrap in array
606            return read_json_single_object(&parsed, &mut table, opts);
607        }
608        _ => {
609            return Err(IoError::FormatError(
610                "Expected JSON array or object".to_string(),
611            ));
612        }
613    };
614
615    if arr.is_empty() {
616        return Ok(table);
617    }
618
619    // Collect all field names from first object
620    let field_names: Vec<String> = if let Some(serde_json::Value::Object(obj)) = arr.first() {
621        obj.keys().cloned().collect()
622    } else {
623        // Array of scalars: single column
624        let values: Vec<f64> = arr
625            .iter()
626            .take(opts.max_rows.unwrap_or(usize::MAX))
627            .filter_map(|v| v.as_f64())
628            .collect();
629        table.add_column("value", DataColumn::Float64(values));
630        return Ok(table);
631    };
632
633    let max = opts.max_rows.unwrap_or(usize::MAX);
634
635    for field_name in &field_names {
636        if let Some(ref wanted) = opts.columns {
637            if !wanted.iter().any(|w| w == field_name) {
638                continue;
639            }
640        }
641
642        // Collect values for this field
643        let values: Vec<&serde_json::Value> = arr
644            .iter()
645            .take(max)
646            .filter_map(|item| item.as_object().and_then(|obj| obj.get(field_name)))
647            .collect();
648
649        // Determine type from values
650        let all_int = values
651            .iter()
652            .all(|v| v.is_i64() || v.is_u64() || v.is_null());
653        let all_float = values.iter().all(|v| v.is_number() || v.is_null());
654        let all_bool = values.iter().all(|v| v.is_boolean() || v.is_null());
655
656        if all_bool && !values.is_empty() {
657            let bools: Vec<bool> = values
658                .iter()
659                .map(|v| v.as_bool().unwrap_or(false))
660                .collect();
661            table.add_column(field_name, DataColumn::Boolean(bools));
662        } else if all_int && !values.is_empty() {
663            let ints: Vec<i64> = values.iter().map(|v| v.as_i64().unwrap_or(0)).collect();
664            table.add_column(field_name, DataColumn::Int64(ints));
665        } else if all_float && !values.is_empty() {
666            let floats: Vec<f64> = values.iter().map(|v| v.as_f64().unwrap_or(0.0)).collect();
667            table.add_column(field_name, DataColumn::Float64(floats));
668        } else {
669            let strings: Vec<String> = values
670                .iter()
671                .map(|v| match v {
672                    serde_json::Value::String(s) => s.clone(),
673                    other => other.to_string(),
674                })
675                .collect();
676            table.add_column(field_name, DataColumn::String(strings));
677        }
678    }
679
680    Ok(table)
681}
682
683/// Read a single JSON object as a one-row table
684fn read_json_single_object(
685    obj: &serde_json::Value,
686    table: &mut DataTable,
687    _opts: &ReadOptions,
688) -> Result<DataTable> {
689    if let serde_json::Value::Object(map) = obj {
690        for (key, value) in map {
691            match value {
692                serde_json::Value::Number(n) => {
693                    if let Some(i) = n.as_i64() {
694                        table.add_column(key, DataColumn::Int64(vec![i]));
695                    } else if let Some(f) = n.as_f64() {
696                        table.add_column(key, DataColumn::Float64(vec![f]));
697                    }
698                }
699                serde_json::Value::Bool(b) => {
700                    table.add_column(key, DataColumn::Boolean(vec![*b]));
701                }
702                serde_json::Value::String(s) => {
703                    table.add_column(key, DataColumn::String(vec![s.clone()]));
704                }
705                _ => {
706                    table.add_column(key, DataColumn::String(vec![value.to_string()]));
707                }
708            }
709        }
710    }
711    Ok(table.clone())
712}
713
714/// Read WAV into DataTable
715fn read_wav_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
716    let (header, data) = crate::wavfile::read_wav(path)?;
717
718    let mut table = DataTable::new(DataFormat::Wav);
719    table.set_metadata("source_file", &path.display().to_string());
720    table.set_metadata("sample_rate", &header.sample_rate.to_string());
721    table.set_metadata("channels", &header.channels.to_string());
722    table.set_metadata("bits_per_sample", &header.bits_per_sample.to_string());
723
724    let max = opts.max_rows.unwrap_or(usize::MAX);
725
726    // Flatten ndarray to f64 column
727    let flat: Vec<f64> = data.iter().take(max).map(|&s| s as f64).collect();
728    table.add_column("samples", DataColumn::Float64(flat));
729
730    Ok(table)
731}
732
733/// Read NPY into DataTable
734fn read_npy_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
735    let arr = crate::npy::read_npy(path)?;
736
737    let mut table = DataTable::new(DataFormat::Npy);
738    table.set_metadata("source_file", &path.display().to_string());
739    table.set_metadata("dtype", &format!("{:?}", arr.dtype()));
740    table.set_metadata("shape", &format!("{:?}", arr.shape()));
741
742    let max = opts.max_rows.unwrap_or(usize::MAX);
743
744    // Convert NpyArray enum to DataColumn
745    match &arr {
746        crate::npy::NpyArray::Float64 { data, .. } => {
747            let values: Vec<f64> = data.iter().take(max).copied().collect();
748            table.add_column("data", DataColumn::Float64(values));
749        }
750        crate::npy::NpyArray::Float32 { data, .. } => {
751            let values: Vec<f32> = data.iter().take(max).copied().collect();
752            table.add_column("data", DataColumn::Float32(values));
753        }
754        crate::npy::NpyArray::Int32 { data, .. } => {
755            let values: Vec<i32> = data.iter().take(max).copied().collect();
756            table.add_column("data", DataColumn::Int32(values));
757        }
758        crate::npy::NpyArray::Int64 { data, .. } => {
759            let values: Vec<i64> = data.iter().take(max).copied().collect();
760            table.add_column("data", DataColumn::Int64(values));
761        }
762    }
763
764    Ok(table)
765}
766
767/// Read Matrix Market into DataTable
768fn read_mtx_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
769    let mm = crate::matrix_market::read_sparse_matrix(path)?;
770
771    let mut table = DataTable::new(DataFormat::MatrixMarket);
772    table.set_metadata("source_file", &path.display().to_string());
773    table.set_metadata("matrix_rows", &mm.rows.to_string());
774    table.set_metadata("matrix_cols", &mm.cols.to_string());
775    table.set_metadata("nnz", &mm.entries.len().to_string());
776
777    let max = opts.max_rows.unwrap_or(usize::MAX);
778
779    // Store as coordinate format columns
780    let mut row_indices = Vec::new();
781    let mut col_indices = Vec::new();
782    let mut vals = Vec::new();
783
784    for (i, entry) in mm.entries.iter().enumerate() {
785        if i >= max {
786            break;
787        }
788        row_indices.push(entry.row as i64);
789        col_indices.push(entry.col as i64);
790        vals.push(entry.value);
791    }
792
793    table.add_column("row", DataColumn::Int64(row_indices));
794    table.add_column("col", DataColumn::Int64(col_indices));
795    table.add_column("value", DataColumn::Float64(vals));
796
797    Ok(table)
798}
799
800/// Read HDF5 into a [`DataTable`], one column per dataset.
801///
802/// Backed by [`oxih5`], the pure-Rust HDF5 implementation. This reaches strictly
803/// further than the `hdf5_lite` reader it replaces: superblock v2/v3, version-2
804/// B-trees, fractal heaps, new-style link-message groups, extensible/fixed array
805/// chunk indices and szip-compressed datasets all resolve here, where the old
806/// reader reported a format error.
807///
808/// Enumeration errors propagate; a dataset that lists but cannot be decoded is
809/// skipped, so one unreadable dataset does not lose the rest of the file. That
810/// split matches the old `list_all()?` / `if let Ok(dataset)` behaviour.
811fn read_hdf5_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
812    let file = oxih5::File::open(path)
813        .map_err(|e| IoError::FormatError(format!("Failed to open HDF5 file: {e}")))?;
814
815    let mut table = DataTable::new(DataFormat::Hdf5);
816    table.set_metadata("source_file", &path.display().to_string());
817    let info = file
818        .info()
819        .map_err(|e| IoError::FormatError(format!("Failed to read HDF5 superblock: {e}")))?;
820    table.set_metadata("superblock_version", &info.superblock_version.to_string());
821
822    // If a specific dataset is requested, read just that.
823    if let Some(ref ds_path) = opts.hdf5_dataset {
824        let dataset = file.dataset(ds_path).map_err(|e| {
825            IoError::FormatError(format!("Failed to read dataset '{ds_path}': {e}"))
826        })?;
827        // The leaf name, matching what the previous reader put in `Hdf5Dataset::name`.
828        // Empty segments are filtered so a trailing slash does not yield "".
829        let name = ds_path
830            .split('/')
831            .rfind(|s| !s.is_empty())
832            .unwrap_or(ds_path)
833            .to_string();
834        table.add_column(&name, hdf5_dataset_to_column(&file, ds_path, &dataset)?);
835        table.set_metadata("dataset_path", ds_path);
836        table.set_metadata("shape", &format!("{:?}", dataset.shape));
837
838        let views = file.attr_views(ds_path).map_err(|e| {
839            IoError::FormatError(format!(
840                "Failed to read attributes of dataset '{ds_path}': {e}"
841            ))
842        })?;
843        for view in &views {
844            table.set_metadata(
845                &format!("attr_{}", view.name()),
846                &hdf5_attr_to_string(view, ds_path),
847            );
848        }
849        return Ok(table);
850    }
851
852    // Otherwise walk the whole tree and read every dataset.
853    //
854    // Explicit recursion rather than `File::walk`: `walk` swallows the error
855    // from descending into an unreadable sub-group, which would silently yield
856    // a partial table with nothing to say so.
857    let root = file
858        .root()
859        .map_err(|e| IoError::FormatError(format!("Failed to open HDF5 root group: {e}")))?;
860    collect_hdf5_datasets(&file, &root, "", &mut table)?;
861
862    Ok(table)
863}
864
865/// Add every dataset at or below `group` to `table`, keyed by full path.
866fn collect_hdf5_datasets(
867    file: &oxih5::File,
868    group: &oxih5::Group,
869    prefix: &str,
870    table: &mut DataTable,
871) -> Result<()> {
872    let names = group.datasets().map_err(|e| {
873        IoError::FormatError(format!("Failed to list datasets of '{prefix}/': {e}"))
874    })?;
875    for name in names {
876        let full_path = format!("{prefix}/{name}");
877        // A dataset whose datatype has no column representation is skipped
878        // rather than failing the whole read.
879        if let Ok(dataset) = group.dataset(&name) {
880            if let Ok(col) = hdf5_dataset_to_column(file, &full_path, &dataset) {
881                table.add_column(&full_path, col);
882            }
883        }
884    }
885
886    let group_names = group
887        .groups()
888        .map_err(|e| IoError::FormatError(format!("Failed to list groups of '{prefix}/': {e}")))?;
889    for name in group_names {
890        let full_path = format!("{prefix}/{name}");
891        let child = group.group(&name).map_err(|e| {
892            IoError::FormatError(format!("Failed to open group '{full_path}': {e}"))
893        })?;
894        collect_hdf5_datasets(file, &child, &full_path, table)?;
895    }
896
897    Ok(())
898}
899
900/// Convert one oxih5 dataset into a [`DataColumn`].
901///
902/// Numeric payloads route through the widening helpers in [`crate::hdf5::convert`]
903/// rather than oxih5's accessors directly: `Dataset::as_f64` matches only
904/// `Float { size: 8 }`, so calling it here would narrow the universal reader to
905/// f64-only datasets. The column width chosen for each datatype reproduces what
906/// the `hdf5_lite` reader produced, so existing `DataColumn` matches keep working:
907/// 1/2/4-byte signed and 1/2-byte unsigned integers become `Int32`, wider
908/// integers `Int64`, f16/f32 become `Float32` and f64 `Float64`.
909fn hdf5_dataset_to_column(
910    file: &oxih5::File,
911    path: &str,
912    dataset: &oxih5::Dataset,
913) -> Result<DataColumn> {
914    use crate::hdf5::convert::{dataset_to_f64, dataset_to_i64, is_floating, is_integral};
915    use oxih5::Dtype;
916
917    match &dataset.dtype {
918        // `dataset_strings` handles fixed-length *and* variable-length strings;
919        // `Dataset::as_string` returns NotImplemented for vlen, whose elements
920        // are global-heap references that need the file bytes to resolve.
921        Dtype::String { .. } => {
922            let strings = file.dataset_strings(path).map_err(|e| {
923                IoError::FormatError(format!("Failed to read string dataset '{path}': {e}"))
924            })?;
925            Ok(DataColumn::String(strings))
926        }
927        dtype if is_floating(dtype) => {
928            let values = dataset_to_f64(dataset)?;
929            if matches!(dtype, Dtype::Float { size: 8, .. }) {
930                Ok(DataColumn::Float64(values))
931            } else {
932                // f16 and f32 are both exactly representable in f32.
933                Ok(DataColumn::Float32(
934                    values.into_iter().map(|v| v as f32).collect(),
935                ))
936            }
937        }
938        dtype if is_integral(dtype) => {
939            let values = dataset_to_i64(dataset)?;
940            if hdf5_fits_i32(dtype) {
941                // The source width guarantees every value fits, so this narrowing
942                // is lossless.
943                Ok(DataColumn::Int32(
944                    values.into_iter().map(|v| v as i32).collect(),
945                ))
946            } else {
947                Ok(DataColumn::Int64(values))
948            }
949        }
950        // Compounds, arrays, opaque blobs and references have no column
951        // representation; their bytes are kept verbatim, as `Hdf5Value::Raw` was.
952        _ => Ok(DataColumn::Bytes(vec![dataset.data.clone()])),
953    }
954}
955
956/// Whether every value of `dtype` is representable in `i32`.
957///
958/// Mirrors the width mapping the `hdf5_lite` reader used, so column types do not
959/// change under the migration. `Enum` and `Bitfield` follow their integer base.
960fn hdf5_fits_i32(dtype: &oxih5::Dtype) -> bool {
961    use oxih5::Dtype;
962    match dtype {
963        Dtype::Int { size, signed, .. } => {
964            if *signed {
965                matches!(size, 1 | 2 | 4)
966            } else {
967                matches!(size, 1 | 2)
968            }
969        }
970        Dtype::Bitfield { size, .. } => matches!(size, 1 | 2),
971        Dtype::Enum { base, .. } => hdf5_fits_i32(base),
972        _ => false,
973    }
974}
975
976/// Render an attribute as the metadata string the table exposes.
977///
978/// Strings join with ", ", exactly as the `hdf5_lite` path did. Numeric values
979/// previously fell through to a `{:?}` of the typed value enum, which printed
980/// Rust syntax such as `Float64([1.0, 2.0])`; they now render as plain
981/// comma-separated values. Array-valued attributes keep their contents rather
982/// than collapsing to a type name — only datatypes with no numeric or textual
983/// reading at all (compounds, references, opaque blobs) fall back to that.
984fn hdf5_attr_to_string(view: &oxih5::AttrView<'_>, path: &str) -> String {
985    use crate::hdf5::convert::{dataset_to_f64, dataset_to_i64, is_floating, is_integral};
986
987    if let Ok(strings) = view.as_strings() {
988        return strings.join(", ");
989    }
990
991    // Scalars decode straight off the view; arrays need the payload re-presented
992    // as a dataset so the widening decoders apply to every element.
993    let dtype = view.dtype();
994    let decoded = if is_floating(dtype) {
995        crate::hdf5::HDF5File::attribute_as_dataset(view, path)
996            .and_then(|ds| dataset_to_f64(&ds))
997            .map(|v| join_display(&v))
998    } else if is_integral(dtype) {
999        crate::hdf5::HDF5File::attribute_as_dataset(view, path)
1000            .and_then(|ds| dataset_to_i64(&ds))
1001            .map(|v| join_display(&v))
1002    } else {
1003        return format!("<{dtype}>");
1004    };
1005
1006    decoded.unwrap_or_else(|_| format!("<{dtype}>"))
1007}
1008
1009/// Render values as a comma-separated list.
1010fn join_display<T: std::fmt::Display>(values: &[T]) -> String {
1011    values
1012        .iter()
1013        .map(T::to_string)
1014        .collect::<Vec<_>>()
1015        .join(", ")
1016}
1017
1018/// Read NetCDF into DataTable
1019fn read_netcdf_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
1020    let nc = crate::netcdf_lite::NcFile::read_from_file(path)?;
1021
1022    let mut table = DataTable::new(DataFormat::NetCdf);
1023    table.set_metadata("source_file", &path.display().to_string());
1024
1025    let dims = nc.dimensions();
1026    let num_records = nc.num_records();
1027
1028    // If a specific variable is requested
1029    if let Some(ref var_name) = opts.netcdf_variable {
1030        let var = nc.variable(var_name)?;
1031        let values = var.as_f64(dims, num_records)?;
1032        let max = opts.max_rows.unwrap_or(usize::MAX);
1033        let truncated: Vec<f64> = values.into_iter().take(max).collect();
1034        table.add_column(var_name, DataColumn::Float64(truncated));
1035        return Ok(table);
1036    }
1037
1038    // Read all variables
1039    let var_names: Vec<String> = nc.variable_names().iter().map(|s| s.to_string()).collect();
1040    for var_name in &var_names {
1041        if let Some(ref wanted) = opts.columns {
1042            if !wanted.iter().any(|w| w == var_name) {
1043                continue;
1044            }
1045        }
1046
1047        if let Ok(var) = nc.variable(var_name) {
1048            if let Ok(values) = var.as_f64(dims, num_records) {
1049                let max = opts.max_rows.unwrap_or(usize::MAX);
1050                let truncated: Vec<f64> = values.into_iter().take(max).collect();
1051                table.add_column(var_name, DataColumn::Float64(truncated));
1052            }
1053        }
1054    }
1055
1056    Ok(table)
1057}
1058
1059// =====================================================================
1060// Utility functions
1061// =====================================================================
1062
1063/// Slice a column from start to end index
1064fn slice_column(col: &DataColumn, start: usize, end: usize) -> DataColumn {
1065    let s = start;
1066    let e = end;
1067    match col {
1068        DataColumn::Int32(v) => DataColumn::Int32(v[s.min(v.len())..e.min(v.len())].to_vec()),
1069        DataColumn::Int64(v) => DataColumn::Int64(v[s.min(v.len())..e.min(v.len())].to_vec()),
1070        DataColumn::Float32(v) => DataColumn::Float32(v[s.min(v.len())..e.min(v.len())].to_vec()),
1071        DataColumn::Float64(v) => DataColumn::Float64(v[s.min(v.len())..e.min(v.len())].to_vec()),
1072        DataColumn::String(v) => DataColumn::String(v[s.min(v.len())..e.min(v.len())].to_vec()),
1073        DataColumn::Boolean(v) => DataColumn::Boolean(v[s.min(v.len())..e.min(v.len())].to_vec()),
1074        DataColumn::Bytes(v) => DataColumn::Bytes(v[s.min(v.len())..e.min(v.len())].to_vec()),
1075    }
1076}
1077
1078// =====================================================================
1079// Tests
1080// =====================================================================
1081
1082#[cfg(test)]
1083mod tests {
1084    use super::*;
1085
1086    #[test]
1087    fn test_data_column_len() {
1088        assert_eq!(DataColumn::Int32(vec![1, 2, 3]).len(), 3);
1089        assert_eq!(DataColumn::Float64(vec![]).len(), 0);
1090        assert!(DataColumn::String(vec![]).is_empty());
1091    }
1092
1093    #[test]
1094    fn test_data_column_as_f64() {
1095        let col = DataColumn::Int32(vec![1, 2, 3]);
1096        let f = col.as_f64();
1097        assert_eq!(f, vec![1.0, 2.0, 3.0]);
1098
1099        let col = DataColumn::Boolean(vec![true, false]);
1100        let f = col.as_f64();
1101        assert_eq!(f, vec![1.0, 0.0]);
1102
1103        let col = DataColumn::String(vec!["a".to_string()]);
1104        assert!(col.as_f64().is_empty());
1105    }
1106
1107    #[test]
1108    fn test_data_column_as_strings() {
1109        let col = DataColumn::Int32(vec![42]);
1110        assert_eq!(col.as_strings(), vec!["42"]);
1111
1112        let col = DataColumn::Boolean(vec![true]);
1113        assert_eq!(col.as_strings(), vec!["true"]);
1114    }
1115
1116    #[test]
1117    fn test_data_column_type_name() {
1118        assert_eq!(DataColumn::Int32(vec![]).type_name(), "int32");
1119        assert_eq!(DataColumn::Float64(vec![]).type_name(), "float64");
1120        assert_eq!(DataColumn::String(vec![]).type_name(), "string");
1121        assert_eq!(DataColumn::Boolean(vec![]).type_name(), "bool");
1122    }
1123
1124    #[test]
1125    fn test_data_table_basic() {
1126        let mut table = DataTable::new(DataFormat::Csv);
1127        table.add_column("x", DataColumn::Float64(vec![1.0, 2.0, 3.0]));
1128        table.add_column("y", DataColumn::Int32(vec![10, 20, 30]));
1129
1130        assert_eq!(table.num_columns(), 2);
1131        assert_eq!(table.num_rows(), 3);
1132        assert!(!table.is_empty());
1133        assert_eq!(table.source_format(), DataFormat::Csv);
1134        assert_eq!(table.column_names(), &["x", "y"]);
1135    }
1136
1137    #[test]
1138    fn test_data_table_column_access() {
1139        let mut table = DataTable::new(DataFormat::Json);
1140        table.add_column("a", DataColumn::Int64(vec![1, 2]));
1141        table.add_column(
1142            "b",
1143            DataColumn::String(vec!["x".to_string(), "y".to_string()]),
1144        );
1145
1146        assert!(table.column("a").is_some());
1147        assert!(table.column("c").is_none());
1148        assert!(table.column_by_index(0).is_some());
1149        assert!(table.column_by_index(5).is_none());
1150    }
1151
1152    #[test]
1153    fn test_data_table_metadata() {
1154        let mut table = DataTable::new(DataFormat::Hdf5);
1155        table.set_metadata("key1", "value1");
1156        table.set_metadata("key2", "value2");
1157
1158        assert_eq!(table.metadata("key1"), Some("value1"));
1159        assert_eq!(table.metadata("key2"), Some("value2"));
1160        assert_eq!(table.metadata("key3"), None);
1161        assert_eq!(table.all_metadata().len(), 2);
1162    }
1163
1164    #[test]
1165    fn test_data_table_select_columns() {
1166        let mut table = DataTable::new(DataFormat::Csv);
1167        table.add_column("a", DataColumn::Int32(vec![1]));
1168        table.add_column("b", DataColumn::Int32(vec![2]));
1169        table.add_column("c", DataColumn::Int32(vec![3]));
1170
1171        let selected = table.select_columns(&["a", "c"]).expect("select");
1172        assert_eq!(selected.num_columns(), 2);
1173        assert_eq!(selected.column_names(), &["a", "c"]);
1174
1175        let err = table.select_columns(&["nonexistent"]);
1176        assert!(err.is_err());
1177    }
1178
1179    #[test]
1180    fn test_data_table_slice_rows() {
1181        let mut table = DataTable::new(DataFormat::Csv);
1182        table.add_column("x", DataColumn::Float64(vec![1.0, 2.0, 3.0, 4.0, 5.0]));
1183
1184        let sliced = table.slice_rows(1, 4);
1185        assert_eq!(sliced.num_rows(), 3);
1186        if let Some(DataColumn::Float64(v)) = sliced.column("x") {
1187            assert_eq!(v, &[2.0, 3.0, 4.0]);
1188        }
1189    }
1190
1191    #[test]
1192    fn test_data_table_summary() {
1193        let mut table = DataTable::new(DataFormat::ArrowIpc);
1194        table.add_column("temp", DataColumn::Float64(vec![20.5, 21.0]));
1195        let summary = table.summary();
1196        assert!(summary.contains("1 columns"));
1197        assert!(summary.contains("2 rows"));
1198        assert!(summary.contains("Arrow IPC"));
1199    }
1200
1201    #[test]
1202    fn test_data_table_empty() {
1203        let table = DataTable::new(DataFormat::Unknown);
1204        assert!(table.is_empty());
1205        assert_eq!(table.num_rows(), 0);
1206        assert_eq!(table.num_columns(), 0);
1207    }
1208
1209    #[test]
1210    fn test_read_options_default() {
1211        let opts = ReadOptions::default();
1212        assert!(opts.format.is_none());
1213        assert!(opts.max_rows.is_none());
1214        assert!(opts.columns.is_none());
1215        assert!(opts.csv_delimiter.is_none());
1216    }
1217
1218    #[test]
1219    fn test_slice_column_bounds() {
1220        let col = DataColumn::Int32(vec![1, 2, 3]);
1221        let sliced = slice_column(&col, 0, 100);
1222        if let DataColumn::Int32(v) = sliced {
1223            assert_eq!(v, vec![1, 2, 3]);
1224        }
1225    }
1226
1227    #[test]
1228    fn test_csv_roundtrip_via_universal() {
1229        let dir = std::env::temp_dir();
1230        let path = dir.join("universal_test.csv");
1231
1232        // Write a simple CSV
1233        std::fs::write(&path, "x,y,name\n1,1.1,a\n2,2.2,b\n3,3.3,c\n").expect("write csv");
1234
1235        let table = read_data(&path, None).expect("read");
1236        assert_eq!(table.source_format(), DataFormat::Csv);
1237        assert_eq!(table.num_rows(), 3);
1238        assert!(table.column("x").is_some());
1239        assert!(table.column("y").is_some());
1240        assert!(table.column("name").is_some());
1241
1242        let _ = std::fs::remove_file(&path);
1243    }
1244
1245    #[test]
1246    fn test_json_roundtrip_via_universal() {
1247        let dir = std::env::temp_dir();
1248        let path = dir.join("universal_test.json");
1249
1250        std::fs::write(
1251            &path,
1252            r#"[{"id": 1, "val": 3.14, "ok": true}, {"id": 2, "val": 2.72, "ok": false}]"#,
1253        )
1254        .expect("write json");
1255
1256        let table = read_data(&path, None).expect("read");
1257        assert_eq!(table.source_format(), DataFormat::Json);
1258        assert_eq!(table.num_rows(), 2);
1259
1260        let _ = std::fs::remove_file(&path);
1261    }
1262
1263    #[test]
1264    fn test_arrow_roundtrip_via_universal() {
1265        let dir = std::env::temp_dir();
1266        let path = dir.join("universal_test.arrow");
1267
1268        let schema = crate::arrow_ipc::ArrowSchema::new(vec![crate::arrow_ipc::ArrowField::new(
1269            "x",
1270            crate::arrow_ipc::ArrowDataType::Float64,
1271        )]);
1272        let batch = crate::arrow_ipc::RecordBatch::new(
1273            schema.clone(),
1274            vec![crate::arrow_ipc::ArrowColumn::Float64(vec![1.0, 2.0])],
1275        )
1276        .expect("batch");
1277
1278        crate::arrow_ipc::write_arrow_ipc_file(&path, &schema, &[batch]).expect("write");
1279
1280        let table = read_data(&path, None).expect("read");
1281        assert_eq!(table.source_format(), DataFormat::ArrowIpc);
1282        assert_eq!(table.num_rows(), 2);
1283
1284        let _ = std::fs::remove_file(&path);
1285    }
1286
1287    #[test]
1288    fn test_read_with_max_rows() {
1289        let dir = std::env::temp_dir();
1290        let path = dir.join("universal_maxrows.csv");
1291
1292        std::fs::write(&path, "x\n1\n2\n3\n4\n5\n").expect("write");
1293
1294        let opts = ReadOptions {
1295            max_rows: Some(2),
1296            ..Default::default()
1297        };
1298        let table = read_data(&path, Some(opts)).expect("read");
1299        assert_eq!(table.num_rows(), 2);
1300
1301        let _ = std::fs::remove_file(&path);
1302    }
1303
1304    #[test]
1305    fn test_read_unsupported_format() {
1306        let dir = std::env::temp_dir();
1307        let path = dir.join("test_file.unsupported_ext");
1308
1309        std::fs::write(&path, [0x00, 0x01, 0x02]).expect("write");
1310
1311        let opts = ReadOptions {
1312            format: Some(DataFormat::Fits),
1313            ..Default::default()
1314        };
1315        let result = read_data(&path, Some(opts));
1316        assert!(result.is_err());
1317
1318        let _ = std::fs::remove_file(&path);
1319    }
1320
1321    #[test]
1322    fn test_streaming_reader() {
1323        let dir = std::env::temp_dir();
1324        let path = dir.join("streaming_test.csv");
1325
1326        std::fs::write(&path, "x\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n").expect("write");
1327
1328        let mut reader = StreamingReader::open(&path, 3, None).expect("open");
1329        assert_eq!(reader.total_rows(), 10);
1330        assert!(reader.num_chunks() >= 3);
1331
1332        let first_row_count = {
1333            let first = reader.next_chunk().expect("chunk1");
1334            assert!(first.num_rows() <= 3);
1335            first.num_rows()
1336        };
1337
1338        reader.reset();
1339        let first_again = reader.next_chunk().expect("chunk1 again");
1340        assert_eq!(first_again.num_rows(), first_row_count);
1341
1342        let _ = std::fs::remove_file(&path);
1343    }
1344
1345    #[test]
1346    fn test_data_column_bytes() {
1347        let col = DataColumn::Bytes(vec![vec![1, 2, 3], vec![4, 5]]);
1348        assert_eq!(col.len(), 2);
1349        assert_eq!(col.type_name(), "bytes");
1350        assert!(col.as_f64().is_empty());
1351        let s = col.as_strings();
1352        assert!(s[0].contains("3 bytes"));
1353    }
1354}