1use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
9use log::debug;
10use num_derive::FromPrimitive;
11use serde::Serialize;
12#[cfg(any(not(target_arch = "wasm32"), test))]
13use std::fs::File;
14#[cfg(not(target_arch = "wasm32"))]
15use std::path::Path;
16use std::{
17 collections::{BTreeMap, BTreeSet, HashMap},
18 ffi::{CString, c_void},
19};
20
21use crate::cb::{handle_metadata, handle_variable};
22use crate::err::{ReadStatError, check_c_error};
23use crate::rs_buffer_io::ReadStatBufferCtx;
24use crate::rs_parser::ReadStatParser;
25use crate::rs_path::ReadStatPath;
26use crate::rs_var::{ReadStatVarFormatClass, ReadStatVarType, ReadStatVarTypeClass};
27
28#[derive(Clone, Debug, Serialize)]
34pub struct ReadStatMetadata {
35 pub row_count: Option<i32>,
37 pub var_count: i32,
39 pub table_name: String,
41 pub file_label: String,
43 pub file_encoding: String,
45 pub version: i32,
47 pub is_64bit: bool,
49 pub creation_time: String,
51 pub modified_time: String,
53 pub compression: ReadStatCompress,
55 pub endianness: ReadStatEndian,
57 pub vars: BTreeMap<i32, ReadStatVarMetadata>,
59 #[serde(skip_serializing)]
61 pub schema: Schema,
62}
63
64impl Default for ReadStatMetadata {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl ReadStatMetadata {
71 pub fn to_json(&self) -> Result<String, ReadStatError> {
73 Ok(serde_json::to_string_pretty(self)?)
74 }
75 pub fn new() -> Self {
77 Self {
78 row_count: None,
79 var_count: 0,
80 table_name: String::new(),
81 file_label: String::new(),
82 file_encoding: String::new(),
83 version: 0,
84 is_64bit: false,
85 creation_time: String::new(),
86 modified_time: String::new(),
87 compression: ReadStatCompress::None,
88 endianness: ReadStatEndian::None,
89 vars: BTreeMap::new(),
90 schema: Schema::empty(),
91 }
92 }
93
94 fn initialize_schema(&self) -> Schema {
95 let fields: Vec<Field> = self
97 .vars
98 .values()
99 .map(|vm| {
100 let var_dt = match &vm.var_type {
101 ReadStatVarType::String
102 | ReadStatVarType::StringRef
103 | ReadStatVarType::Unknown => DataType::Utf8,
104 ReadStatVarType::Int8 | ReadStatVarType::Int16 => DataType::Int16,
105 ReadStatVarType::Int32 => DataType::Int32,
106 ReadStatVarType::Float => DataType::Float32,
107 ReadStatVarType::Double => match &vm.var_format_class {
108 Some(ReadStatVarFormatClass::Date) => DataType::Date32,
109 Some(ReadStatVarFormatClass::DateTime) => {
110 DataType::Timestamp(TimeUnit::Second, None)
111 }
112 Some(ReadStatVarFormatClass::DateTimeWithMilliseconds) => {
113 DataType::Timestamp(TimeUnit::Millisecond, None)
114 }
115 Some(ReadStatVarFormatClass::DateTimeWithMicroseconds) => {
116 DataType::Timestamp(TimeUnit::Microsecond, None)
117 }
118 Some(ReadStatVarFormatClass::DateTimeWithNanoseconds) => {
119 DataType::Timestamp(TimeUnit::Nanosecond, None)
120 }
121 Some(ReadStatVarFormatClass::Time) => DataType::Time32(TimeUnit::Second),
122 Some(ReadStatVarFormatClass::TimeWithMilliseconds) => {
123 DataType::Time32(TimeUnit::Millisecond)
124 }
125 Some(ReadStatVarFormatClass::TimeWithMicroseconds) => {
126 DataType::Time64(TimeUnit::Microsecond)
127 }
128 Some(ReadStatVarFormatClass::TimeWithNanoseconds) => {
129 DataType::Time64(TimeUnit::Nanosecond)
130 }
131 None => DataType::Float64,
132 },
133 };
134
135 let mut field = Field::new(&vm.var_name, var_dt, true);
137 let mut metadata = HashMap::new();
138 if !vm.var_label.is_empty() {
139 metadata.insert("label".to_string(), vm.var_label.clone());
140 }
141 if !vm.var_format.is_empty() {
142 metadata.insert("sas_format".to_string(), vm.var_format.clone());
143 }
144 metadata.insert("storage_width".to_string(), vm.storage_width.to_string());
145 if vm.display_width != 0 {
146 metadata.insert("display_width".to_string(), vm.display_width.to_string());
147 }
148 if !metadata.is_empty() {
149 field = field.with_metadata(metadata);
150 }
151 field
152 })
153 .collect();
154
155 if self.file_label.is_empty() {
157 Schema::new(fields)
158 } else {
159 let mut schema_metadata = HashMap::new();
160 schema_metadata.insert("table_label".to_string(), self.file_label.clone());
161 Schema::new_with_metadata(fields, schema_metadata)
162 }
163 }
164
165 #[allow(clippy::cast_possible_wrap, clippy::ptr_as_ptr)]
176 pub fn read_metadata(
177 &mut self,
178 rsp: &ReadStatPath,
179 skip_row_count: bool,
180 ) -> Result<(), ReadStatError> {
181 let mut parsed = Self::new();
182 debug!("Path as C string is {:?}", rsp.cstring_path);
183 let ppath = rsp.cstring_path.as_ptr();
184
185 let ctx = std::ptr::from_mut::<Self>(&mut parsed) as *mut c_void;
186
187 let error: readstat_sys::readstat_error_t = readstat_sys::readstat_error_e_READSTAT_OK;
188 debug!("Initially, error ==> {error}");
189
190 let row_limit = if skip_row_count { Some(1) } else { None };
191
192 let error = ReadStatParser::new()?
193 .set_metadata_handler(Some(handle_metadata))?
194 .set_variable_handler(Some(handle_variable))?
195 .set_row_limit(row_limit)?
196 .parse_sas7bdat(ppath, ctx);
197
198 check_c_error(error as i32)?;
199
200 if skip_row_count {
201 parsed.row_count = None;
202 }
203
204 parsed.schema = parsed.initialize_schema();
206 *self = parsed;
207 Ok(())
208 }
209
210 #[allow(clippy::cast_possible_wrap, clippy::ptr_as_ptr)]
224 pub fn read_metadata_from_bytes(
225 &mut self,
226 bytes: &[u8],
227 skip_row_count: bool,
228 ) -> Result<(), ReadStatError> {
229 let mut parsed = Self::new();
230 let mut buffer_ctx = ReadStatBufferCtx::new(bytes);
231
232 let ctx = std::ptr::from_mut::<Self>(&mut parsed) as *mut c_void;
233
234 let error: readstat_sys::readstat_error_t = readstat_sys::readstat_error_e_READSTAT_OK;
235 debug!("Initially, error ==> {error}");
236
237 let row_limit = if skip_row_count { Some(1) } else { None };
238
239 let dummy_path = CString::new("").expect("empty string is valid C string");
241
242 let error = buffer_ctx
243 .configure_parser(
244 ReadStatParser::new()?
245 .set_metadata_handler(Some(handle_metadata))?
246 .set_variable_handler(Some(handle_variable))?
247 .set_row_limit(row_limit)?,
248 )?
249 .parse_sas7bdat(dummy_path.as_ptr(), ctx);
250
251 check_c_error(error as i32)?;
252
253 if skip_row_count {
254 parsed.row_count = None;
255 }
256
257 parsed.schema = parsed.initialize_schema();
259 *self = parsed;
260 Ok(())
261 }
262
263 #[cfg(not(target_arch = "wasm32"))]
280 pub fn read_metadata_from_mmap(
281 &mut self,
282 path: &Path,
283 skip_row_count: bool,
284 ) -> Result<(), ReadStatError> {
285 let file = File::open(path)?;
286 let mmap = unsafe { memmap2::Mmap::map(&file)? };
287 self.read_metadata_from_bytes(&mmap, skip_row_count)
288 }
289
290 pub fn resolve_selected_columns(
301 &self,
302 columns: Option<Vec<String>>,
303 ) -> Result<Option<BTreeMap<i32, i32>>, ReadStatError> {
304 let Some(columns) = columns else {
305 return Ok(None);
306 };
307
308 let requested: BTreeSet<String> = columns.into_iter().collect();
310
311 let name_to_index: HashMap<&str, i32> = self
313 .vars
314 .iter()
315 .map(|(&idx, vm)| (vm.var_name.as_str(), idx))
316 .collect();
317
318 let not_found: Vec<String> = requested
320 .iter()
321 .filter(|name| !name_to_index.contains_key(name.as_str()))
322 .cloned()
323 .collect();
324
325 if !not_found.is_empty() {
326 let available: Vec<String> = self.vars.values().map(|vm| vm.var_name.clone()).collect();
327 return Err(ReadStatError::ColumnsNotFound {
328 requested: not_found,
329 available,
330 });
331 }
332
333 let mut mapping = BTreeMap::new();
336 let mut new_index = 0i32;
337 for (&orig_index, vm) in &self.vars {
338 if requested.contains(&vm.var_name) {
339 mapping.insert(orig_index, new_index);
340 new_index += 1;
341 }
342 }
343
344 Ok(Some(mapping))
345 }
346
347 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
353 pub fn filter_to_selected_columns(&self, mapping: &BTreeMap<i32, i32>) -> Self {
354 let new_vars: BTreeMap<i32, ReadStatVarMetadata> = mapping
355 .iter()
356 .filter_map(|(&orig_idx, &new_idx)| {
357 self.vars.get(&orig_idx).map(|vm| (new_idx, vm.clone()))
358 })
359 .collect();
360
361 let mut filtered = Self {
362 row_count: self.row_count,
363 var_count: mapping.len() as i32,
364 table_name: self.table_name.clone(),
365 file_label: self.file_label.clone(),
366 file_encoding: self.file_encoding.clone(),
367 version: self.version,
368 is_64bit: self.is_64bit,
369 creation_time: self.creation_time.clone(),
370 modified_time: self.modified_time.clone(),
371 compression: self.compression.clone(),
372 endianness: self.endianness.clone(),
373 vars: new_vars,
374 schema: Schema::empty(),
375 };
376 filtered.schema = filtered.initialize_schema();
377 filtered
378 }
379}
380
381#[non_exhaustive]
386#[derive(Clone, Debug, Default, FromPrimitive, Serialize)]
387#[allow(clippy::cast_possible_wrap)]
388pub enum ReadStatCompress {
389 #[default]
391 None = readstat_sys::readstat_compress_e_READSTAT_COMPRESS_NONE as isize,
392 Rows = readstat_sys::readstat_compress_e_READSTAT_COMPRESS_ROWS as isize,
394 Binary = readstat_sys::readstat_compress_e_READSTAT_COMPRESS_BINARY as isize,
396}
397
398#[non_exhaustive]
403#[derive(Clone, Debug, Default, FromPrimitive, Serialize)]
404#[allow(clippy::cast_possible_wrap)]
405pub enum ReadStatEndian {
406 #[default]
408 None = readstat_sys::readstat_endian_e_READSTAT_ENDIAN_NONE as isize,
409 Little = readstat_sys::readstat_endian_e_READSTAT_ENDIAN_LITTLE as isize,
411 Big = readstat_sys::readstat_endian_e_READSTAT_ENDIAN_BIG as isize,
413}
414
415#[derive(Clone, Debug, Serialize)]
417pub struct ReadStatVarMetadata {
418 pub var_name: String,
420 pub var_type: ReadStatVarType,
422 pub var_type_class: ReadStatVarTypeClass,
424 pub var_label: String,
426 pub var_format: String,
428 pub var_format_class: Option<ReadStatVarFormatClass>,
430 pub storage_width: usize,
433 pub display_width: i32,
435}
436
437impl ReadStatVarMetadata {
438 #[allow(clippy::too_many_arguments)]
440 pub fn new(
441 var_name: String,
442 var_type: ReadStatVarType,
443 var_type_class: ReadStatVarTypeClass,
444 var_label: String,
445 var_format: String,
446 var_format_class: Option<ReadStatVarFormatClass>,
447 storage_width: usize,
448 display_width: i32,
449 ) -> Self {
450 Self {
451 var_name,
452 var_type,
453 var_type_class,
454 var_label,
455 var_format,
456 var_format_class,
457 storage_width,
458 display_width,
459 }
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
469 fn test_metadata(var_names: &[&str]) -> ReadStatMetadata {
470 let mut md = ReadStatMetadata::new();
471 for (i, name) in var_names.iter().enumerate() {
472 md.vars.insert(
473 i as i32,
474 ReadStatVarMetadata::new(
475 name.to_string(),
476 ReadStatVarType::Double,
477 ReadStatVarTypeClass::Numeric,
478 String::new(),
479 "BEST12".to_string(),
480 None,
481 8,
482 0,
483 ),
484 );
485 }
486 md.var_count = var_names.len() as i32;
487 md.schema = md.initialize_schema();
488 md
489 }
490
491 #[test]
494 fn resolve_columns_none_returns_none() {
495 let md = test_metadata(&["a", "b", "c"]);
496 assert!(md.resolve_selected_columns(None).unwrap().is_none());
497 }
498
499 #[test]
500 fn resolve_columns_valid_subset() {
501 let md = test_metadata(&["a", "b", "c"]);
502 let mapping = md
503 .resolve_selected_columns(Some(vec!["a".into(), "c".into()]))
504 .unwrap()
505 .unwrap();
506 assert_eq!(mapping.len(), 2);
507 assert_eq!(mapping[&0], 0);
509 assert_eq!(mapping[&2], 1);
511 }
512
513 #[test]
514 fn resolve_columns_invalid_name_errors() {
515 let md = test_metadata(&["a", "b", "c"]);
516 let err = md
517 .resolve_selected_columns(Some(vec!["a".into(), "nonexistent".into()]))
518 .unwrap_err();
519 match err {
520 ReadStatError::ColumnsNotFound {
521 requested,
522 available,
523 } => {
524 assert_eq!(requested, vec!["nonexistent"]);
525 assert_eq!(available, vec!["a", "b", "c"]);
526 }
527 other => panic!("Expected ColumnsNotFound, got {other:?}"),
528 }
529 }
530
531 #[test]
532 fn resolve_columns_all_columns() {
533 let md = test_metadata(&["x", "y", "z"]);
534 let mapping = md
535 .resolve_selected_columns(Some(vec!["x".into(), "y".into(), "z".into()]))
536 .unwrap()
537 .unwrap();
538 assert_eq!(mapping.len(), 3);
539 assert_eq!(mapping[&0], 0);
540 assert_eq!(mapping[&1], 1);
541 assert_eq!(mapping[&2], 2);
542 }
543
544 #[test]
547 fn filter_produces_contiguous_indices() {
548 let md = test_metadata(&["a", "b", "c", "d"]);
549 let mapping = md
550 .resolve_selected_columns(Some(vec!["b".into(), "d".into()]))
551 .unwrap()
552 .unwrap();
553 let filtered = md.filter_to_selected_columns(&mapping);
554
555 assert_eq!(filtered.var_count, 2);
556 assert_eq!(filtered.vars[&0].var_name, "b");
557 assert_eq!(filtered.vars[&1].var_name, "d");
558 }
559
560 #[test]
561 fn filter_preserves_schema() {
562 let md = test_metadata(&["a", "b", "c"]);
563 let mapping = md
564 .resolve_selected_columns(Some(vec!["b".into()]))
565 .unwrap()
566 .unwrap();
567 let filtered = md.filter_to_selected_columns(&mapping);
568
569 assert_eq!(filtered.schema.fields().len(), 1);
570 assert_eq!(filtered.schema.fields()[0].name(), "b");
571 }
572
573 #[test]
576 fn schema_string_type() {
577 let mut md = ReadStatMetadata::new();
578 md.vars.insert(
579 0,
580 ReadStatVarMetadata::new(
581 "name".into(),
582 ReadStatVarType::String,
583 ReadStatVarTypeClass::String,
584 String::new(),
585 "$30".into(),
586 None,
587 30,
588 0,
589 ),
590 );
591 md.var_count = 1;
592 let schema = md.initialize_schema();
593 assert_eq!(*schema.fields()[0].data_type(), DataType::Utf8);
594 }
595
596 #[test]
597 fn schema_float64_type() {
598 let mut md = ReadStatMetadata::new();
599 md.vars.insert(
600 0,
601 ReadStatVarMetadata::new(
602 "value".into(),
603 ReadStatVarType::Double,
604 ReadStatVarTypeClass::Numeric,
605 String::new(),
606 "BEST12".into(),
607 None,
608 8,
609 0,
610 ),
611 );
612 md.var_count = 1;
613 let schema = md.initialize_schema();
614 assert_eq!(*schema.fields()[0].data_type(), DataType::Float64);
615 }
616
617 #[test]
618 fn schema_date_type() {
619 let mut md = ReadStatMetadata::new();
620 md.vars.insert(
621 0,
622 ReadStatVarMetadata::new(
623 "dt".into(),
624 ReadStatVarType::Double,
625 ReadStatVarTypeClass::Numeric,
626 String::new(),
627 "DATE9".into(),
628 Some(ReadStatVarFormatClass::Date),
629 8,
630 0,
631 ),
632 );
633 md.var_count = 1;
634 let schema = md.initialize_schema();
635 assert_eq!(*schema.fields()[0].data_type(), DataType::Date32);
636 }
637
638 #[test]
639 fn schema_datetime_type() {
640 let mut md = ReadStatMetadata::new();
641 md.vars.insert(
642 0,
643 ReadStatVarMetadata::new(
644 "ts".into(),
645 ReadStatVarType::Double,
646 ReadStatVarTypeClass::Numeric,
647 String::new(),
648 "DATETIME22".into(),
649 Some(ReadStatVarFormatClass::DateTime),
650 8,
651 0,
652 ),
653 );
654 md.var_count = 1;
655 let schema = md.initialize_schema();
656 assert_eq!(
657 *schema.fields()[0].data_type(),
658 DataType::Timestamp(TimeUnit::Second, None)
659 );
660 }
661
662 #[test]
663 fn schema_time_type() {
664 let mut md = ReadStatMetadata::new();
665 md.vars.insert(
666 0,
667 ReadStatVarMetadata::new(
668 "tm".into(),
669 ReadStatVarType::Double,
670 ReadStatVarTypeClass::Numeric,
671 String::new(),
672 "TIME8".into(),
673 Some(ReadStatVarFormatClass::Time),
674 8,
675 0,
676 ),
677 );
678 md.var_count = 1;
679 let schema = md.initialize_schema();
680 assert_eq!(
681 *schema.fields()[0].data_type(),
682 DataType::Time32(TimeUnit::Second)
683 );
684 }
685
686 #[test]
687 fn schema_int32_type() {
688 let mut md = ReadStatMetadata::new();
689 md.vars.insert(
690 0,
691 ReadStatVarMetadata::new(
692 "count".into(),
693 ReadStatVarType::Int32,
694 ReadStatVarTypeClass::Numeric,
695 String::new(),
696 String::new(),
697 None,
698 4,
699 0,
700 ),
701 );
702 md.var_count = 1;
703 let schema = md.initialize_schema();
704 assert_eq!(*schema.fields()[0].data_type(), DataType::Int32);
705 }
706
707 #[test]
708 fn schema_with_labels_metadata() {
709 let mut md = ReadStatMetadata::new();
710 md.vars.insert(
711 0,
712 ReadStatVarMetadata::new(
713 "col".into(),
714 ReadStatVarType::Double,
715 ReadStatVarTypeClass::Numeric,
716 "My Label".into(),
717 "BEST12".into(),
718 None,
719 8,
720 0,
721 ),
722 );
723 md.var_count = 1;
724 md.file_label = "My Table".into();
725 let schema = md.initialize_schema();
726
727 let field_meta = schema.fields()[0].metadata();
729 assert_eq!(field_meta.get("label").unwrap(), "My Label");
730
731 let schema_meta = schema.metadata();
733 assert_eq!(schema_meta.get("table_label").unwrap(), "My Table");
734 }
735
736 #[test]
737 fn schema_no_labels_has_format_and_width_metadata() {
738 let mut md = ReadStatMetadata::new();
739 md.vars.insert(
740 0,
741 ReadStatVarMetadata::new(
742 "col".into(),
743 ReadStatVarType::Double,
744 ReadStatVarTypeClass::Numeric,
745 String::new(),
746 "BEST12".into(),
747 None,
748 8,
749 0,
750 ),
751 );
752 md.var_count = 1;
753 let schema = md.initialize_schema();
754
755 let field_meta = schema.fields()[0].metadata();
756 assert!(!field_meta.contains_key("label"));
757 assert_eq!(field_meta.get("sas_format").unwrap(), "BEST12");
758 assert_eq!(field_meta.get("storage_width").unwrap(), "8");
759 assert!(!field_meta.contains_key("display_width"));
760 assert!(schema.metadata().is_empty());
761 }
762
763 #[test]
766 fn default_metadata() {
767 let md = ReadStatMetadata::new();
768 assert_eq!(md.row_count, None);
769 assert_eq!(md.var_count, 0);
770 assert!(md.table_name.is_empty());
771 assert!(md.vars.is_empty());
772 assert!(md.schema.fields().is_empty());
773 }
774}