1use arrow::datatypes::Schema;
9use arrow_array::{
10 ArrayRef, RecordBatch, RecordBatchOptions,
11 builder::{
12 Date32Builder, Float32Builder, Float64Builder, Int16Builder, Int32Builder, StringBuilder,
13 Time32MillisecondBuilder, Time32SecondBuilder, Time64MicrosecondBuilder,
14 Time64NanosecondBuilder, TimestampMicrosecondBuilder, TimestampMillisecondBuilder,
15 TimestampNanosecondBuilder, TimestampSecondBuilder,
16 },
17};
18use log::debug;
19use std::{
20 collections::BTreeMap,
21 ffi::CString,
22 os::raw::c_void,
23 sync::{Arc, atomic::AtomicUsize},
24};
25
26use crate::{
27 cb,
28 err::{ReadStatError, check_c_error},
29 progress::ProgressCallback,
30 rs_buffer_io::ReadStatBufferCtx,
31 rs_metadata::{ReadStatMetadata, ReadStatVarMetadata},
32 rs_parser::ReadStatParser,
33 rs_path::ReadStatPath,
34 rs_var::{ReadStatVarFormatClass, ReadStatVarType, ReadStatVarTypeClass},
35};
36
37const MAX_PREALLOC_ROWS: usize = 1_000_000;
44const MAX_STRING_PREALLOC_ROWS: usize = 1_000_000;
46const MAX_STRING_PREALLOC_BYTES: usize = 16 * 1024 * 1024;
48const MAX_STRING_COLUMN_PREALLOC_BYTES: usize = 1024 * 1024;
50
51fn string_column_byte_budget(string_columns: usize) -> usize {
52 MAX_STRING_PREALLOC_BYTES
53 .checked_div(string_columns)
54 .unwrap_or(0)
55 .min(MAX_STRING_COLUMN_PREALLOC_BYTES)
56}
57
58fn string_column_row_budget(string_columns: usize) -> usize {
59 MAX_STRING_PREALLOC_ROWS
60 .checked_div(string_columns)
61 .unwrap_or(0)
62}
63
64fn string_value_capacity(capacity: usize, storage_width: usize, byte_budget: usize) -> usize {
65 capacity.saturating_mul(storage_width).min(byte_budget)
66}
67
68pub(crate) enum ColumnBuilder {
74 Str(StringBuilder),
76 Int16(Int16Builder),
78 Int32(Int32Builder),
80 Float32(Float32Builder),
82 Float64(Float64Builder),
84 Date32(Date32Builder),
86 TimestampSecond(TimestampSecondBuilder),
88 TimestampMillisecond(TimestampMillisecondBuilder),
90 TimestampMicrosecond(TimestampMicrosecondBuilder),
92 TimestampNanosecond(TimestampNanosecondBuilder),
94 Time32Second(Time32SecondBuilder),
96 Time32Millisecond(Time32MillisecondBuilder),
98 Time64Microsecond(Time64MicrosecondBuilder),
100 Time64Nanosecond(Time64NanosecondBuilder),
102}
103
104impl ColumnBuilder {
105 pub(crate) fn append_null(&mut self) {
107 match self {
108 Self::Str(b) => b.append_null(),
109 Self::Int16(b) => b.append_null(),
110 Self::Int32(b) => b.append_null(),
111 Self::Float32(b) => b.append_null(),
112 Self::Float64(b) => b.append_null(),
113 Self::Date32(b) => b.append_null(),
114 Self::TimestampSecond(b) => b.append_null(),
115 Self::TimestampMillisecond(b) => b.append_null(),
116 Self::TimestampMicrosecond(b) => b.append_null(),
117 Self::TimestampNanosecond(b) => b.append_null(),
118 Self::Time32Second(b) => b.append_null(),
119 Self::Time32Millisecond(b) => b.append_null(),
120 Self::Time64Microsecond(b) => b.append_null(),
121 Self::Time64Nanosecond(b) => b.append_null(),
122 }
123 }
124
125 pub(crate) fn finish(&mut self) -> ArrayRef {
127 match self {
128 Self::Str(b) => Arc::new(b.finish()),
129 Self::Int16(b) => Arc::new(b.finish()),
130 Self::Int32(b) => Arc::new(b.finish()),
131 Self::Float32(b) => Arc::new(b.finish()),
132 Self::Float64(b) => Arc::new(b.finish()),
133 Self::Date32(b) => Arc::new(b.finish()),
134 Self::TimestampSecond(b) => Arc::new(b.finish()),
135 Self::TimestampMillisecond(b) => Arc::new(b.finish()),
136 Self::TimestampMicrosecond(b) => Arc::new(b.finish()),
137 Self::TimestampNanosecond(b) => Arc::new(b.finish()),
138 Self::Time32Second(b) => Arc::new(b.finish()),
139 Self::Time32Millisecond(b) => Arc::new(b.finish()),
140 Self::Time64Microsecond(b) => Arc::new(b.finish()),
141 Self::Time64Nanosecond(b) => Arc::new(b.finish()),
142 }
143 }
144
145 fn from_metadata(
151 vm: &ReadStatVarMetadata,
152 capacity: usize,
153 string_row_budget: usize,
154 string_byte_budget: usize,
155 ) -> Self {
156 match vm.var_type_class {
157 ReadStatVarTypeClass::String => {
158 let string_capacity = capacity.min(string_row_budget);
159 Self::Str(StringBuilder::with_capacity(
160 string_capacity,
161 string_value_capacity(string_capacity, vm.storage_width, string_byte_budget),
162 ))
163 }
164 ReadStatVarTypeClass::Numeric => {
165 match vm.var_format_class {
166 Some(ReadStatVarFormatClass::Date) => {
167 Self::Date32(Date32Builder::with_capacity(capacity))
168 }
169 Some(ReadStatVarFormatClass::DateTime) => {
170 Self::TimestampSecond(TimestampSecondBuilder::with_capacity(capacity))
171 }
172 Some(ReadStatVarFormatClass::DateTimeWithMilliseconds) => {
173 Self::TimestampMillisecond(TimestampMillisecondBuilder::with_capacity(
174 capacity,
175 ))
176 }
177 Some(ReadStatVarFormatClass::DateTimeWithMicroseconds) => {
178 Self::TimestampMicrosecond(TimestampMicrosecondBuilder::with_capacity(
179 capacity,
180 ))
181 }
182 Some(ReadStatVarFormatClass::DateTimeWithNanoseconds) => {
183 Self::TimestampNanosecond(TimestampNanosecondBuilder::with_capacity(
184 capacity,
185 ))
186 }
187 Some(ReadStatVarFormatClass::Time) => {
188 Self::Time32Second(Time32SecondBuilder::with_capacity(capacity))
189 }
190 Some(ReadStatVarFormatClass::TimeWithMilliseconds) => {
191 Self::Time32Millisecond(Time32MillisecondBuilder::with_capacity(capacity))
192 }
193 Some(ReadStatVarFormatClass::TimeWithMicroseconds) => {
194 Self::Time64Microsecond(Time64MicrosecondBuilder::with_capacity(capacity))
195 }
196 Some(ReadStatVarFormatClass::TimeWithNanoseconds) => {
197 Self::Time64Nanosecond(Time64NanosecondBuilder::with_capacity(capacity))
198 }
199 None => {
200 match vm.var_type {
202 ReadStatVarType::Int8 | ReadStatVarType::Int16 => {
203 Self::Int16(Int16Builder::with_capacity(capacity))
204 }
205 ReadStatVarType::Int32 => {
206 Self::Int32(Int32Builder::with_capacity(capacity))
207 }
208 ReadStatVarType::Float => {
209 Self::Float32(Float32Builder::with_capacity(capacity))
210 }
211 _ => Self::Float64(Float64Builder::with_capacity(capacity)),
212 }
213 }
214 }
215 }
216 }
217 }
218}
219
220pub struct ReadStatData {
227 pub var_count: i32,
229 pub vars: Arc<BTreeMap<i32, ReadStatVarMetadata>>,
232 pub(crate) builders: Vec<ColumnBuilder>,
234 pub schema: Arc<Schema>,
237 pub batch: Option<RecordBatch>,
239 pub chunk_rows_to_process: usize,
241 pub(crate) chunk_row_start: usize,
243 pub(crate) chunk_row_end: usize,
245 pub(crate) chunk_rows_processed: usize,
247 pub(crate) total_rows_processed: Option<Arc<AtomicUsize>>,
249 pub(crate) progress: Option<Arc<dyn ProgressCallback>>,
251 pub(crate) abort_error: Option<ReadStatError>,
257 pub(crate) column_filter: Option<Arc<BTreeMap<i32, i32>>>,
260 pub(crate) total_var_count: i32,
264}
265
266pub(crate) struct StreamingData<'a> {
269 pub(crate) data: ReadStatData,
270 chunk_rows: usize,
271 expected_rows: usize,
272 rows_emitted: usize,
273 sink: &'a mut dyn FnMut(RecordBatch) -> Result<(), ReadStatError>,
274}
275
276impl StreamingData<'_> {
277 pub(crate) fn finish_chunk(&mut self) -> Result<(), ReadStatError> {
278 if self.data.chunk_rows_processed == 0 {
279 return Ok(());
280 }
281 self.data.cols_to_batch()?;
282 let batch = self
283 .data
284 .batch
285 .take()
286 .ok_or_else(|| ReadStatError::Other("no record batch was produced".into()))?;
287 let rows = batch.num_rows();
288 (self.sink)(batch)?;
289 self.rows_emitted += rows;
290 if let Some(progress) = &self.data.progress {
291 progress.inc(rows as u64);
292 }
293 self.data.chunk_rows_processed = 0;
294 if self.rows_emitted < self.expected_rows {
295 let data = std::mem::take(&mut self.data);
296 self.data = data.allocate_builders_with_capacity(self.chunk_rows);
297 }
298 Ok(())
299 }
300
301 pub(crate) fn row_complete(&mut self) -> Result<(), ReadStatError> {
302 if self.data.chunk_rows_processed == self.chunk_rows {
303 self.finish_chunk()?;
304 }
305 Ok(())
306 }
307}
308
309impl Default for ReadStatData {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315impl ReadStatData {
316 pub fn new() -> Self {
318 Self {
319 var_count: 0,
321 vars: Arc::new(BTreeMap::new()),
322 builders: Vec::new(),
324 schema: Arc::new(Schema::empty()),
325 batch: None,
327 chunk_rows_to_process: 0,
328 chunk_rows_processed: 0,
329 chunk_row_start: 0,
330 chunk_row_end: 0,
331 total_rows_processed: None,
333 progress: None,
335 abort_error: None,
337 column_filter: None,
339 total_var_count: 0,
340 }
341 }
342
343 #[must_use]
354 pub fn allocate_builders(self) -> Self {
355 let capacity = self.chunk_rows_to_process;
356 self.allocate_builders_with_capacity(capacity)
357 }
358
359 fn allocate_builders_with_capacity(self, capacity: usize) -> Self {
360 let capacity = capacity
361 .min(self.chunk_rows_to_process)
362 .min(MAX_PREALLOC_ROWS);
363 let string_columns = self
364 .vars
365 .values()
366 .filter(|vm| matches!(vm.var_type_class, ReadStatVarTypeClass::String))
367 .count();
368 let string_byte_budget = string_column_byte_budget(string_columns);
369 let string_row_budget = string_column_row_budget(string_columns);
370 let builders: Vec<ColumnBuilder> = self
371 .vars
372 .values()
373 .map(|vm| {
374 ColumnBuilder::from_metadata(vm, capacity, string_row_budget, string_byte_budget)
375 })
376 .collect();
377 Self { builders, ..self }
378 }
379
380 pub(crate) fn cols_to_batch(&mut self) -> Result<(), ReadStatError> {
386 let arrays: Vec<ArrayRef> = self
387 .builders
388 .iter_mut()
389 .map(ColumnBuilder::finish)
390 .collect();
391
392 self.batch = Some(if arrays.is_empty() {
393 RecordBatch::try_new_with_options(
394 self.schema.clone(),
395 arrays,
396 &RecordBatchOptions::new().with_row_count(Some(self.chunk_rows_processed)),
397 )?
398 } else {
399 RecordBatch::try_new(self.schema.clone(), arrays)?
400 });
401
402 Ok(())
403 }
404
405 pub(crate) fn note_value(&mut self, var_index: i32) {
415 if var_index == self.total_var_count - 1 {
416 self.chunk_rows_processed += 1;
417 if let Some(trp) = &self.total_rows_processed {
418 trp.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
419 }
420 }
421 }
422
423 pub fn read_data(&mut self, rsp: &ReadStatPath) -> Result<(), ReadStatError> {
429 self.parse_data(rsp)?;
431 self.cols_to_batch()?;
432 self.report_progress();
433 Ok(())
434 }
435
436 pub fn read_data_from_bytes(&mut self, bytes: &[u8]) -> Result<(), ReadStatError> {
445 self.parse_data_from_bytes(bytes)?;
446 self.cols_to_batch()?;
447 self.report_progress();
448 Ok(())
449 }
450
451 #[cfg(not(target_arch = "wasm32"))]
466 pub fn read_data_from_mmap(&mut self, path: &std::path::Path) -> Result<(), ReadStatError> {
467 let file = std::fs::File::open(path)?;
468 let mmap = unsafe { memmap2::Mmap::map(&file)? };
469 self.read_data_from_bytes(&mmap)
470 }
471
472 pub(crate) fn visit_data(
473 self,
474 rsp: &ReadStatPath,
475 chunk_rows: usize,
476 sink: &mut dyn FnMut(RecordBatch) -> Result<(), ReadStatError>,
477 ) -> Result<(), ReadStatError> {
478 let ppath = rsp.cstring_path.as_ptr();
479 self.visit_parse(chunk_rows, sink, |mut parser, ctx| {
480 Ok(parser.parse_sas7bdat(ppath, ctx))
481 })
482 }
483
484 pub(crate) fn visit_data_from_bytes(
485 self,
486 bytes: &[u8],
487 chunk_rows: usize,
488 sink: &mut dyn FnMut(RecordBatch) -> Result<(), ReadStatError>,
489 ) -> Result<(), ReadStatError> {
490 let mut buffer_ctx = ReadStatBufferCtx::new(bytes);
491 self.visit_parse(chunk_rows, sink, |parser, ctx| {
492 buffer_ctx
493 .configure_parser(parser)
494 .map(|mut p| p.parse_sas7bdat(std::ptr::null(), ctx))
495 })
496 }
497
498 #[cfg(not(target_arch = "wasm32"))]
499 pub(crate) fn visit_data_from_mmap(
500 self,
501 path: &std::path::Path,
502 chunk_rows: usize,
503 sink: &mut dyn FnMut(RecordBatch) -> Result<(), ReadStatError>,
504 ) -> Result<(), ReadStatError> {
505 let file = std::fs::File::open(path)?;
506 let mmap = unsafe { memmap2::Mmap::map(&file)? };
507 self.visit_data_from_bytes(&mmap, chunk_rows, sink)
508 }
509
510 fn visit_parse(
511 self,
512 chunk_rows: usize,
513 sink: &mut dyn FnMut(RecordBatch) -> Result<(), ReadStatError>,
514 parse: impl FnOnce(
515 ReadStatParser,
516 *mut c_void,
517 ) -> Result<readstat_sys::readstat_error_t, ReadStatError>,
518 ) -> Result<(), ReadStatError> {
519 let offset = self.chunk_row_start.try_into()?;
520 let limit = self.chunk_rows_to_process.try_into()?;
521 let expected_rows = self.chunk_rows_to_process;
522 let mut stream = StreamingData {
523 data: self,
524 chunk_rows,
525 expected_rows,
526 rows_emitted: 0,
527 sink,
528 };
529 let ctx = std::ptr::from_mut(&mut stream).cast::<c_void>();
530 let parser = ReadStatParser::new()?
531 .set_value_handler(Some(cb::handle_streaming_value))?
532 .set_row_limit(Some(limit))?
533 .set_row_offset(Some(offset))?;
534 let result = parse(parser, ctx);
535 if let Some(error) = stream.data.abort_error.take() {
536 return Err(error);
537 }
538 check_c_error(result? as i32)?;
539 stream.finish_chunk()?;
540 if stream.rows_emitted != stream.expected_rows {
541 return Err(ReadStatError::Other(format!(
542 "ReadStat emitted {} rows, expected {}",
543 stream.rows_emitted, stream.expected_rows
544 )));
545 }
546 Ok(())
547 }
548
549 #[allow(clippy::cast_possible_wrap, clippy::ptr_as_ptr)]
551 pub(crate) fn parse_data(&mut self, rsp: &ReadStatPath) -> Result<(), ReadStatError> {
552 debug!("Path as C string is {:?}", rsp.cstring_path);
554 let ppath = rsp.cstring_path.as_ptr();
555
556 let ctx = std::ptr::from_mut::<Self>(self) as *mut c_void;
558
559 let error: readstat_sys::readstat_error_t = readstat_sys::readstat_error_e_READSTAT_OK;
561 debug!("Initially, error ==> {error:#?}");
562
563 let error = ReadStatParser::new()?
566 .set_value_handler(Some(cb::handle_value))?
568 .set_row_limit(Some(self.chunk_rows_to_process.try_into()?))?
569 .set_row_offset(Some(self.chunk_row_start.try_into()?))?
570 .parse_sas7bdat(ppath, ctx);
571
572 if let Some(e) = self.abort_error.take() {
575 return Err(e);
576 }
577 check_c_error(error as i32)?;
578
579 Ok(())
580 }
581
582 fn report_progress(&self) {
583 if let Some(progress) = &self.progress {
584 progress.inc(self.batch.as_ref().map_or(0, RecordBatch::num_rows) as u64);
585 }
586 }
587
588 #[allow(clippy::cast_possible_wrap, clippy::ptr_as_ptr)]
589 fn parse_data_from_bytes(&mut self, bytes: &[u8]) -> Result<(), ReadStatError> {
590 let mut buffer_ctx = ReadStatBufferCtx::new(bytes);
591
592 let ctx = std::ptr::from_mut::<Self>(self) as *mut c_void;
594
595 let error: readstat_sys::readstat_error_t = readstat_sys::readstat_error_e_READSTAT_OK;
597 debug!("Initially, error ==> {error:#?}");
598
599 let dummy_path = CString::new("").expect("empty string is valid C string");
601
602 let error = buffer_ctx
604 .configure_parser(
605 ReadStatParser::new()?
606 .set_value_handler(Some(cb::handle_value))?
607 .set_row_limit(Some(self.chunk_rows_to_process.try_into()?))?
608 .set_row_offset(Some(self.chunk_row_start.try_into()?))?,
609 )?
610 .parse_sas7bdat(dummy_path.as_ptr(), ctx);
611
612 if let Some(e) = self.abort_error.take() {
615 return Err(e);
616 }
617 check_c_error(error as i32)?;
618 Ok(())
619 }
620
621 #[must_use]
627 pub fn init(self, md: ReadStatMetadata, row_start: u32, row_end: u32) -> Self {
628 self.set_metadata(md)
629 .set_chunk_counts(row_start, row_end)
630 .allocate_builders()
631 }
632
633 #[must_use]
661 pub fn init_filtered(
662 self,
663 md: ReadStatMetadata,
664 mapping: &BTreeMap<i32, i32>,
665 row_start: u32,
666 row_end: u32,
667 ) -> Self {
668 let original_var_count = md.var_count;
669 let filtered = md.filter_to_selected_columns(mapping);
670 self.set_column_filter(Some(Arc::new(mapping.clone())), original_var_count)
671 .init(filtered, row_start, row_end)
672 }
673
674 pub(crate) fn init_for_visit(
675 self,
676 md: ReadStatMetadata,
677 mapping: Option<&BTreeMap<i32, i32>>,
678 row_start: u32,
679 row_end: u32,
680 batch_rows: usize,
681 ) -> Self {
682 let data = if let Some(mapping) = mapping {
683 let original_var_count = md.var_count;
684 let filtered = md.filter_to_selected_columns(mapping);
685 self.set_column_filter(Some(Arc::new(mapping.clone())), original_var_count)
686 .set_metadata(filtered)
687 } else {
688 self.set_metadata(md)
689 };
690 data.set_chunk_counts(row_start, row_end)
691 .allocate_builders_with_capacity(batch_rows)
692 }
693
694 #[must_use]
700 pub fn init_shared(
701 self,
702 var_count: i32,
703 vars: Arc<BTreeMap<i32, ReadStatVarMetadata>>,
704 schema: Arc<Schema>,
705 row_start: u32,
706 row_end: u32,
707 ) -> Self {
708 let total_var_count = if self.total_var_count != 0 {
709 self.total_var_count
710 } else {
711 var_count
712 };
713 Self {
714 var_count,
715 vars,
716 schema,
717 total_var_count,
718 ..self
719 }
720 .set_chunk_counts(row_start, row_end)
721 .allocate_builders()
722 }
723
724 #[allow(clippy::cast_possible_truncation)]
725 fn set_chunk_counts(self, row_start: u32, row_end: u32) -> Self {
726 let chunk_rows_to_process = row_end.saturating_sub(row_start) as usize;
730 let chunk_row_start = row_start as usize;
731 let chunk_row_end = row_end as usize;
732 let chunk_rows_processed = 0_usize;
733
734 Self {
735 chunk_rows_to_process,
736 chunk_row_start,
737 chunk_row_end,
738 chunk_rows_processed,
739 ..self
740 }
741 }
742
743 fn set_metadata(self, md: ReadStatMetadata) -> Self {
744 let var_count = md.var_count;
745 let vars = Arc::new(md.vars);
746 let schema = Arc::new(md.schema);
747 let total_var_count = if self.total_var_count != 0 {
749 self.total_var_count
750 } else {
751 var_count
752 };
753 Self {
754 var_count,
755 vars,
756 schema,
757 total_var_count,
758 ..self
759 }
760 }
761
762 #[must_use]
764 pub fn set_total_rows_processed(self, total_rows_processed: Arc<AtomicUsize>) -> Self {
765 Self {
766 total_rows_processed: Some(total_rows_processed),
767 ..self
768 }
769 }
770
771 #[must_use]
777 pub fn set_column_filter(
778 self,
779 filter: Option<Arc<BTreeMap<i32, i32>>>,
780 total_var_count: i32,
781 ) -> Self {
782 Self {
783 column_filter: filter,
784 total_var_count,
785 ..self
786 }
787 }
788
789 #[must_use]
794 pub fn set_progress(self, progress: Arc<dyn ProgressCallback>) -> Self {
795 Self {
796 progress: Some(progress),
797 ..self
798 }
799 }
800}
801
802#[cfg(test)]
803mod allocation_tests {
804 use super::*;
805
806 #[test]
807 fn string_byte_hint_is_normal_for_small_columns() {
808 assert_eq!(string_value_capacity(100, 20, 1024 * 1024), 2_000);
809 }
810
811 #[test]
812 fn string_byte_hint_caps_max_sas_width() {
813 assert_eq!(
814 string_value_capacity(1_000_000, 32_767, MAX_STRING_COLUMN_PREALLOC_BYTES),
815 MAX_STRING_COLUMN_PREALLOC_BYTES
816 );
817 }
818
819 #[test]
820 fn string_byte_hint_caps_hostile_overflow() {
821 assert_eq!(
822 string_value_capacity(usize::MAX, usize::MAX, MAX_STRING_COLUMN_PREALLOC_BYTES),
823 MAX_STRING_COLUMN_PREALLOC_BYTES
824 );
825 }
826
827 #[test]
828 fn string_byte_budget_is_bounded_across_many_columns() {
829 let columns = 32_767;
830 let per_column = string_column_byte_budget(columns);
831 assert!(per_column * columns <= MAX_STRING_PREALLOC_BYTES);
832 }
833
834 #[test]
835 fn string_row_budget_is_bounded_across_many_columns() {
836 let columns = 32_767;
837 let per_column = string_column_row_budget(columns);
838 assert!(per_column * columns <= MAX_STRING_PREALLOC_ROWS);
839 }
840}