readstat/lib.rs
1//! Read SAS binary files (`.sas7bdat`) and convert them to other formats.
2//!
3//! This crate provides a library for parsing SAS binary data files using FFI
4//! bindings to the [ReadStat](https://github.com/WizardMac/ReadStat) C library,
5//! then converting the parsed data into Apache Arrow [`RecordBatch`](arrow_array::RecordBatch)
6//! format for output as CSV, Feather (Arrow IPC), NDJSON, or Parquet.
7//!
8//! **Note:** While the underlying [`readstat-sys`](https://docs.rs/readstat-sys) crate
9//! exposes bindings for all formats supported by `ReadStat` (SAS, SPSS, Stata),
10//! this crate currently only implements parsing and conversion for **SAS `.sas7bdat` files**.
11//!
12//! # Data Pipeline
13//!
14//! ```text
15//! .sas7bdat file
16//! → ReadStat C library (FFI parsing via callbacks)
17//! → Typed Arrow builders (StringBuilder, Float64Builder, etc.)
18//! → Arrow RecordBatch
19//! → Output format (CSV / Feather / NDJSON / Parquet)
20//! ```
21//!
22//! # Examples
23//!
24//! ## Inspect file metadata
25//!
26//! Read metadata without loading any row data. Useful for discovering
27//! schema, row counts, variable types, and SAS format classifications.
28//!
29//! ```no_run
30//! use readstat::{ReadStatPath, ReadStatMetadata};
31//!
32//! # fn main() -> Result<(), readstat::ReadStatError> {
33//! let rsp = ReadStatPath::new("data.sas7bdat".into())?;
34//!
35//! let mut md = ReadStatMetadata::new();
36//! md.read_metadata(&rsp, false)?;
37//!
38//! println!("Rows: {}, Variables: {}", md.row_count, md.var_count);
39//! println!("Encoding: {}", md.file_encoding);
40//! println!("Compression: {:?}", md.compression);
41//!
42//! // Iterate over variable metadata
43//! for (idx, var) in &md.vars {
44//! println!(
45//! " [{idx}] {} ({:?}, format: {})",
46//! var.var_name, var.var_type_class, var.var_format
47//! );
48//! }
49//!
50//! // The Arrow schema is also available
51//! println!("Schema: {:?}", md.schema);
52//! # Ok(())
53//! # }
54//! ```
55//!
56//! ## Read all data into Arrow `RecordBatch`
57//!
58//! Parse the entire file into a single Arrow [`RecordBatch`](arrow_array::RecordBatch).
59//! Best for smaller files that fit comfortably in memory.
60//!
61//! ```no_run
62//! use readstat::{ReadStatPath, ReadStatMetadata, ReadStatData};
63//!
64//! # fn main() -> Result<(), readstat::ReadStatError> {
65//! let rsp = ReadStatPath::new("data.sas7bdat".into())?;
66//!
67//! // Read metadata first
68//! let mut md = ReadStatMetadata::new();
69//! md.read_metadata(&rsp, false)?;
70//!
71//! // Read all rows into a single chunk
72//! let row_count = md.row_count as u32;
73//! let mut d = ReadStatData::new().init(md, 0, row_count);
74//! d.read_data(&rsp)?;
75//!
76//! // Access the Arrow RecordBatch
77//! if let Some(batch) = &d.batch {
78//! println!("Got {} rows x {} columns", batch.num_rows(), batch.num_columns());
79//! println!("Schema: {:?}", batch.schema());
80//! }
81//! # Ok(())
82//! # }
83//! ```
84//!
85//! ## Stream data in chunks and write to Parquet
86//!
87//! For large files, read in streaming chunks to control memory usage.
88//! Each chunk is parsed and written incrementally.
89//!
90//! ```no_run
91//! use readstat::{
92//! ReadStatPath, ReadStatMetadata, ReadStatData, ReadStatWriter,
93//! WriteConfig, OutFormat, build_offsets,
94//! };
95//!
96//! # fn main() -> Result<(), readstat::ReadStatError> {
97//! let rsp = ReadStatPath::new("data.sas7bdat".into())?;
98//! let wc = WriteConfig::new(
99//! Some("output.parquet".into()),
100//! Some(OutFormat::Parquet),
101//! false, // overwrite
102//! None, // compression (defaults to Snappy for Parquet)
103//! None, // compression_level
104//! )?;
105//!
106//! let mut md = ReadStatMetadata::new();
107//! md.read_metadata(&rsp, false)?;
108//!
109//! // Build chunk offsets: [0, 10000, 20000, ..., row_count]
110//! let offsets = build_offsets(md.row_count as u32, 10_000)?;
111//! let mut wtr = ReadStatWriter::new();
112//! let pairs = offsets.windows(2);
113//! let pairs_cnt = pairs.len();
114//!
115//! for (i, w) in pairs.enumerate() {
116//! let mut d = ReadStatData::new().init(md.clone(), w[0], w[1]);
117//! d.read_data(&rsp)?;
118//! wtr.write(&d, &wc)?;
119//! if i == pairs_cnt - 1 {
120//! wtr.finish(&d, &wc, &rsp.path)?;
121//! }
122//! }
123//! # Ok(())
124//! # }
125//! ```
126//!
127//! ## Read from in-memory bytes
128//!
129//! Parse a `.sas7bdat` file from a byte slice instead of the filesystem.
130//! Useful for cloud storage, HTTP uploads, WASM targets, and testing.
131//!
132//! ```no_run
133//! use readstat::{ReadStatMetadata, ReadStatData};
134//!
135//! # fn main() -> Result<(), readstat::ReadStatError> {
136//! # let sas_bytes: &[u8] = &[];
137//! // sas_bytes: &[u8] — obtained from S3, HTTP, etc.
138//! let mut md = ReadStatMetadata::new();
139//! md.read_metadata_from_bytes(sas_bytes, false)?;
140//!
141//! let row_count = md.row_count as u32;
142//! let mut d = ReadStatData::new().init(md, 0, row_count);
143//! d.read_data_from_bytes(sas_bytes)?;
144//!
145//! if let Some(batch) = &d.batch {
146//! println!("Parsed {} rows from bytes", batch.num_rows());
147//! }
148//! # Ok(())
149//! # }
150//! ```
151//!
152//! ## Filter to specific columns
153//!
154//! Select only specific columns before reading data. Unselected columns
155//! are skipped during parsing, reducing both memory and CPU usage.
156//!
157//! ```no_run
158//! use readstat::{ReadStatPath, ReadStatMetadata, ReadStatData};
159//! use std::sync::Arc;
160//!
161//! # fn main() -> Result<(), readstat::ReadStatError> {
162//! let rsp = ReadStatPath::new("data.sas7bdat".into())?;
163//!
164//! let mut md = ReadStatMetadata::new();
165//! md.read_metadata(&rsp, false)?;
166//!
167//! // Select only these columns
168//! let columns = vec!["name".to_string(), "age".to_string()];
169//! let filter = md.resolve_selected_columns(Some(columns))?;
170//!
171//! if let Some(ref mapping) = filter {
172//! // Apply filter to metadata (updates schema and vars)
173//! let original_var_count = md.var_count;
174//! md = md.filter_to_selected_columns(mapping);
175//!
176//! let row_count = md.row_count as u32;
177//! let mut d = ReadStatData::new()
178//! .set_column_filter(Some(Arc::new(mapping.clone())), original_var_count)
179//! .init(md, 0, row_count);
180//! d.read_data(&rsp)?;
181//!
182//! if let Some(batch) = &d.batch {
183//! // batch only contains "name" and "age" columns
184//! println!(
185//! "Columns: {:?}",
186//! batch.schema().fields().iter().map(|f| f.name()).collect::<Vec<_>>()
187//! );
188//! }
189//! }
190//! # Ok(())
191//! # }
192//! ```
193//!
194//! ## Convert `RecordBatch` to in-memory bytes
195//!
196//! Serialize a parsed [`RecordBatch`](arrow_array::RecordBatch) directly to
197//! in-memory bytes without writing to a file. Useful for HTTP responses,
198//! message queues, or piping to other Arrow-aware tools.
199//!
200//! ```no_run
201//! use readstat::{ReadStatPath, ReadStatMetadata, ReadStatData};
202//! # #[cfg(feature = "parquet")]
203//! use readstat::write_batch_to_parquet_bytes;
204//! # #[cfg(feature = "csv")]
205//! use readstat::write_batch_to_csv_bytes;
206//!
207//! # fn main() -> Result<(), readstat::ReadStatError> {
208//! let rsp = ReadStatPath::new("data.sas7bdat".into())?;
209//!
210//! let mut md = ReadStatMetadata::new();
211//! md.read_metadata(&rsp, false)?;
212//!
213//! let row_count = md.row_count as u32;
214//! let mut d = ReadStatData::new().init(md, 0, row_count);
215//! d.read_data(&rsp)?;
216//!
217//! if let Some(batch) = &d.batch {
218//! // Get Parquet bytes (e.g. for an HTTP response)
219//! # #[cfg(feature = "parquet")]
220//! let parquet_bytes = write_batch_to_parquet_bytes(batch)?;
221//!
222//! // Or CSV bytes
223//! # #[cfg(feature = "csv")]
224//! let csv_bytes = write_batch_to_csv_bytes(batch)?;
225//! }
226//! # Ok(())
227//! # }
228//! ```
229//!
230//! # Key Types
231//!
232//! - [`ReadStatPath`] — Validated input file path for SAS files
233//! - [`WriteConfig`] — Output configuration (path, format, compression)
234//! - [`ReadStatMetadata`] — File-level metadata (row/var counts, encoding, Arrow schema)
235//! - [`ReadStatData`] — Parsed row data, convertible to Arrow [`RecordBatch`](arrow_array::RecordBatch)
236//! - [`ReadStatVarFormatClass`] — SAS format classification (Date, `DateTime`, Time variants)
237//! - [`ReadStatWriter`] — Writes Arrow batches to the configured output format
238//!
239//! # Features
240//!
241//! Output format writers are feature-gated (all enabled by default):
242//!
243//! | Feature | Format | Notes |
244//! |---------|--------|-------|
245//! | `csv` | CSV | Comma-separated values via `arrow-csv` |
246//! | `parquet` | Parquet | Columnar format via `parquet` crate, 5 compression codecs |
247//! | `feather` | Feather | Arrow IPC format via `arrow-ipc` |
248//! | `ndjson` | NDJSON | Newline-delimited JSON via `arrow-json` |
249//! | `sql` | SQL | Query data with SQL via DataFusion (not enabled by default) |
250
251#![warn(missing_docs)]
252#![allow(clippy::module_name_repetitions)]
253#![allow(clippy::must_use_candidate)]
254#![allow(clippy::return_self_not_must_use)]
255
256pub use common::build_offsets;
257pub use err::{ReadStatCError, ReadStatError};
258pub use progress::ProgressCallback;
259pub use rs_data::ReadStatData;
260pub use rs_metadata::{ReadStatCompress, ReadStatEndian, ReadStatMetadata, ReadStatVarMetadata};
261pub use rs_path::ReadStatPath;
262#[cfg(feature = "sql")]
263pub use rs_query::{
264 execute_sql, execute_sql_and_write_stream, execute_sql_stream, read_sql_file, write_sql_results,
265};
266pub use rs_var::{ReadStatVarFormatClass, ReadStatVarType, ReadStatVarTypeClass};
267pub use rs_write::ReadStatWriter;
268#[cfg(feature = "csv")]
269pub use rs_write::write_batch_to_csv_bytes;
270#[cfg(feature = "feather")]
271pub use rs_write::write_batch_to_feather_bytes;
272#[cfg(feature = "ndjson")]
273pub use rs_write::write_batch_to_ndjson_bytes;
274#[cfg(feature = "parquet")]
275pub use rs_write::write_batch_to_parquet_bytes;
276pub use rs_write_config::{OutFormat, ParquetCompression, WriteConfig};
277
278mod cb;
279mod common;
280mod err;
281mod formats;
282mod progress;
283mod rs_buffer_io;
284mod rs_data;
285mod rs_metadata;
286mod rs_parser;
287mod rs_path;
288#[cfg(feature = "sql")]
289mod rs_query;
290mod rs_var;
291mod rs_write;
292mod rs_write_config;