1use std::path::PathBuf;
8
9use num_derive::FromPrimitive;
10
11#[non_exhaustive]
21#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive)]
22#[allow(non_camel_case_types)]
23pub enum ReadStatCError {
24 READSTAT_OK = 0,
26 READSTAT_ERROR_OPEN = 1,
28 READSTAT_ERROR_READ = 2,
30 READSTAT_ERROR_MALLOC = 3,
32 READSTAT_ERROR_USER_ABORT = 4,
34 READSTAT_ERROR_PARSE = 5,
36 READSTAT_ERROR_UNSUPPORTED_COMPRESSION = 6,
38 READSTAT_ERROR_UNSUPPORTED_CHARSET = 7,
40 READSTAT_ERROR_COLUMN_COUNT_MISMATCH = 8,
42 READSTAT_ERROR_ROW_COUNT_MISMATCH = 9,
44 READSTAT_ERROR_ROW_WIDTH_MISMATCH = 10,
46 READSTAT_ERROR_BAD_FORMAT_STRING = 11,
48 READSTAT_ERROR_VALUE_TYPE_MISMATCH = 12,
50 READSTAT_ERROR_WRITE = 13,
52 READSTAT_ERROR_WRITER_NOT_INITIALIZED = 14,
54 READSTAT_ERROR_SEEK = 15,
56 READSTAT_ERROR_CONVERT = 16,
58 READSTAT_ERROR_CONVERT_BAD_STRING = 17,
60 READSTAT_ERROR_CONVERT_SHORT_STRING = 18,
62 READSTAT_ERROR_CONVERT_LONG_STRING = 19,
64 READSTAT_ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE = 20,
66 READSTAT_ERROR_TAGGED_VALUE_IS_OUT_OF_RANGE = 21,
68 READSTAT_ERROR_STRING_VALUE_IS_TOO_LONG = 22,
70 READSTAT_ERROR_TAGGED_VALUES_NOT_SUPPORTED = 23,
72 READSTAT_ERROR_UNSUPPORTED_FILE_FORMAT_VERSION = 24,
74 READSTAT_ERROR_NAME_BEGINS_WITH_ILLEGAL_CHARACTER = 25,
76 READSTAT_ERROR_NAME_CONTAINS_ILLEGAL_CHARACTER = 26,
78 READSTAT_ERROR_NAME_IS_RESERVED_WORD = 27,
80 READSTAT_ERROR_NAME_IS_TOO_LONG = 28,
82 READSTAT_ERROR_BAD_TIMESTAMP_STRING = 29,
84 READSTAT_ERROR_BAD_FREQUENCY_WEIGHT = 30,
86 READSTAT_ERROR_TOO_MANY_MISSING_VALUE_DEFINITIONS = 31,
88 READSTAT_ERROR_NOTE_IS_TOO_LONG = 32,
90 READSTAT_ERROR_STRING_REFS_NOT_SUPPORTED = 33,
92 READSTAT_ERROR_STRING_REF_IS_REQUIRED = 34,
94 READSTAT_ERROR_ROW_IS_TOO_WIDE_FOR_PAGE = 35,
96 READSTAT_ERROR_TOO_FEW_COLUMNS = 36,
98 READSTAT_ERROR_TOO_MANY_COLUMNS = 37,
100 READSTAT_ERROR_NAME_IS_ZERO_LENGTH = 38,
102 READSTAT_ERROR_BAD_TIMESTAMP_VALUE = 39,
104 READSTAT_ERROR_BAD_MR_STRING = 40,
106}
107
108impl std::fmt::Display for ReadStatCError {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 let msg = match self {
111 Self::READSTAT_OK => "operation completed successfully",
112 Self::READSTAT_ERROR_OPEN => "failed to open the file",
113 Self::READSTAT_ERROR_READ => "failed to read from the file",
114 Self::READSTAT_ERROR_MALLOC => "memory allocation failure",
115 Self::READSTAT_ERROR_USER_ABORT => "user-initiated abort via callback return value",
116 Self::READSTAT_ERROR_PARSE => "general parse error in the file structure",
117 Self::READSTAT_ERROR_UNSUPPORTED_COMPRESSION => {
118 "file uses an unsupported compression method"
119 }
120 Self::READSTAT_ERROR_UNSUPPORTED_CHARSET => "file uses an unsupported character set",
121 Self::READSTAT_ERROR_COLUMN_COUNT_MISMATCH => {
122 "column count in header does not match actual columns"
123 }
124 Self::READSTAT_ERROR_ROW_COUNT_MISMATCH => {
125 "row count in header does not match actual rows"
126 }
127 Self::READSTAT_ERROR_ROW_WIDTH_MISMATCH => {
128 "row width in header does not match actual width"
129 }
130 Self::READSTAT_ERROR_BAD_FORMAT_STRING => "invalid or unrecognized format string",
131 Self::READSTAT_ERROR_VALUE_TYPE_MISMATCH => "value type does not match expected type",
132 Self::READSTAT_ERROR_WRITE => "failed to write output",
133 Self::READSTAT_ERROR_WRITER_NOT_INITIALIZED => {
134 "writer was not properly initialized before use"
135 }
136 Self::READSTAT_ERROR_SEEK => "failed to seek within the file",
137 Self::READSTAT_ERROR_CONVERT => "character encoding conversion failed",
138 Self::READSTAT_ERROR_CONVERT_BAD_STRING => {
139 "conversion failed due to invalid string data"
140 }
141 Self::READSTAT_ERROR_CONVERT_SHORT_STRING => "string is too short for conversion",
142 Self::READSTAT_ERROR_CONVERT_LONG_STRING => "string is too long for conversion",
143 Self::READSTAT_ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE => {
144 "numeric value is outside the representable range"
145 }
146 Self::READSTAT_ERROR_TAGGED_VALUE_IS_OUT_OF_RANGE => {
147 "tagged missing value is outside the valid range"
148 }
149 Self::READSTAT_ERROR_STRING_VALUE_IS_TOO_LONG => {
150 "string value exceeds the maximum allowed length"
151 }
152 Self::READSTAT_ERROR_TAGGED_VALUES_NOT_SUPPORTED => {
153 "tagged missing values are not supported by this format"
154 }
155 Self::READSTAT_ERROR_UNSUPPORTED_FILE_FORMAT_VERSION => {
156 "file format version is not supported"
157 }
158 Self::READSTAT_ERROR_NAME_BEGINS_WITH_ILLEGAL_CHARACTER => {
159 "variable name begins with an illegal character"
160 }
161 Self::READSTAT_ERROR_NAME_CONTAINS_ILLEGAL_CHARACTER => {
162 "variable name contains an illegal character"
163 }
164 Self::READSTAT_ERROR_NAME_IS_RESERVED_WORD => "variable name is a reserved word",
165 Self::READSTAT_ERROR_NAME_IS_TOO_LONG => {
166 "variable name exceeds the maximum allowed length"
167 }
168 Self::READSTAT_ERROR_BAD_TIMESTAMP_STRING => "timestamp string could not be parsed",
169 Self::READSTAT_ERROR_BAD_FREQUENCY_WEIGHT => "invalid frequency weight specification",
170 Self::READSTAT_ERROR_TOO_MANY_MISSING_VALUE_DEFINITIONS => {
171 "too many missing value definitions for a variable"
172 }
173 Self::READSTAT_ERROR_NOTE_IS_TOO_LONG => "note text exceeds the maximum allowed length",
174 Self::READSTAT_ERROR_STRING_REFS_NOT_SUPPORTED => {
175 "string references are not supported by this format"
176 }
177 Self::READSTAT_ERROR_STRING_REF_IS_REQUIRED => {
178 "a string reference is required but was not provided"
179 }
180 Self::READSTAT_ERROR_ROW_IS_TOO_WIDE_FOR_PAGE => "row is too wide for a single page",
181 Self::READSTAT_ERROR_TOO_FEW_COLUMNS => "file has too few columns",
182 Self::READSTAT_ERROR_TOO_MANY_COLUMNS => "file has too many columns",
183 Self::READSTAT_ERROR_NAME_IS_ZERO_LENGTH => "variable name is empty (zero length)",
184 Self::READSTAT_ERROR_BAD_TIMESTAMP_VALUE => "timestamp value is invalid",
185 Self::READSTAT_ERROR_BAD_MR_STRING => "invalid multiple response (MR) set string",
186 };
187 f.write_str(msg)
188 }
189}
190
191impl std::error::Error for ReadStatCError {}
192
193#[non_exhaustive]
202#[derive(Debug, thiserror::Error)]
203pub enum ReadStatError {
204 #[error("ReadStat C library error: {0}")]
206 CLibrary(ReadStatCError),
207
208 #[error("Unknown C error code: {0}")]
210 UnknownCError(i32),
211
212 #[error("Date arithmetic overflow")]
214 DateOverflow,
215
216 #[error("Row count is unavailable; read metadata without skip_row_count")]
218 RowCountUnavailable,
219
220 #[error("Panic in ReadStat callback")]
222 CallbackPanic,
223
224 #[error("Integer conversion failed: {0}")]
226 IntConversion(#[from] std::num::TryFromIntError),
227
228 #[error("{0}")]
230 Arrow(#[from] arrow::error::ArrowError),
231
232 #[cfg(feature = "parquet")]
234 #[error("{0}")]
235 Parquet(#[from] parquet::errors::ParquetError),
236
237 #[error("{0}")]
239 Io(#[from] std::io::Error),
240
241 #[cfg(not(target_arch = "wasm32"))]
243 #[error("{0}")]
244 PathAbs(#[from] path_abs::Error),
245
246 #[error("{0}")]
248 SerdeJson(#[from] serde_json::Error),
249
250 #[cfg(not(target_arch = "wasm32"))]
252 #[error("{0}")]
253 Rayon(#[from] rayon::ThreadPoolBuildError),
254
255 #[error("{0}")]
257 NulError(#[from] std::ffi::NulError),
258
259 #[error("Column(s) not found: {requested:?}\nAvailable columns: {available:?}")]
261 ColumnsNotFound {
262 requested: Vec<String>,
264 available: Vec<String>,
266 },
267
268 #[error("File {} does not exist!", .0.display())]
270 FileNotFound(PathBuf),
271
272 #[error("File {} does not have the expected .sas7bdat extension!", .0.display())]
276 UnsupportedInputExtension(PathBuf),
277
278 #[error("File {} does not have the expected .{expected} extension!", .path.display())]
281 OutputExtensionMismatch {
282 path: PathBuf,
284 expected: String,
286 },
287
288 #[error("Output file {} already exists! Set overwrite = true to replace it.", .0.display())]
290 OutputFileExists(PathBuf),
291
292 #[error("The parent directory of the output path {} does not exist", .0.display())]
294 OutputParentMissing(PathBuf),
295
296 #[error("chunk size must be greater than zero")]
298 InvalidChunkSize,
299
300 #[error("invalid row range: offset {offset}, limit {limit:?}, row count {row_count}")]
302 InvalidRowRange {
303 offset: u32,
305 limit: Option<u32>,
307 row_count: u32,
309 },
310
311 #[error("record batch schema does not match writer schema")]
313 SchemaMismatch,
314
315 #[error("invalid output configuration: {0}")]
317 InvalidOutputConfig(String),
318
319 #[error("invalid compression configuration: {0}")]
321 InvalidCompressionConfig(String),
322
323 #[error("Unknown format: {0:?}. Expected one of: csv, feather, ndjson, parquet")]
325 UnknownFormat(String),
326
327 #[error("SQL file {} is empty", .0.display())]
329 EmptySqlFile(PathBuf),
330
331 #[error("Columns file {} contains no column names", .0.display())]
333 EmptyColumnsFile(PathBuf),
334
335 #[cfg(feature = "sql")]
337 #[error("{0}")]
338 DataFusion(#[from] datafusion::error::DataFusionError),
339
340 #[cfg(feature = "sql")]
342 #[error(
343 "synchronous SQL APIs cannot run inside a Tokio runtime; use the corresponding *_async API"
344 )]
345 SyncSqlInAsyncRuntime,
346
347 #[error("{0}")]
349 Other(String),
350}
351
352pub(crate) fn check_c_error(code: i32) -> Result<(), ReadStatError> {
355 use num_traits::FromPrimitive;
356 match FromPrimitive::from_i32(code) {
357 Some(ReadStatCError::READSTAT_OK) => Ok(()),
358 Some(e) => Err(ReadStatError::CLibrary(e)),
359 None => Err(ReadStatError::UnknownCError(code)),
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 #[test]
368 fn check_c_error_ok() {
369 assert!(check_c_error(0).is_ok());
370 }
371
372 #[test]
373 fn check_c_error_known_errors() {
374 for code in 1..=40 {
375 let err = check_c_error(code).unwrap_err();
376 match err {
377 ReadStatError::CLibrary(_) => {}
378 other => panic!("Expected CLibrary error for code {code}, got {other:?}"),
379 }
380 }
381 }
382
383 #[test]
384 fn check_c_error_open() {
385 let err = check_c_error(1).unwrap_err();
386 assert!(matches!(
387 err,
388 ReadStatError::CLibrary(ReadStatCError::READSTAT_ERROR_OPEN)
389 ));
390 }
391
392 #[test]
393 fn check_c_error_parse() {
394 let err = check_c_error(5).unwrap_err();
395 assert!(matches!(
396 err,
397 ReadStatError::CLibrary(ReadStatCError::READSTAT_ERROR_PARSE)
398 ));
399 }
400
401 #[test]
402 fn check_c_error_unknown_positive() {
403 let err = check_c_error(999).unwrap_err();
404 assert!(matches!(err, ReadStatError::UnknownCError(999)));
405 }
406
407 #[test]
408 fn check_c_error_unknown_negative() {
409 let err = check_c_error(-1).unwrap_err();
410 assert!(matches!(err, ReadStatError::UnknownCError(-1)));
411 }
412
413 #[test]
414 fn error_display_messages() {
415 let err = ReadStatError::Other("test error".to_string());
416 assert_eq!(format!("{err}"), "test error");
417
418 let err = ReadStatError::DateOverflow;
419 assert_eq!(format!("{err}"), "Date arithmetic overflow");
420
421 let err = ReadStatError::UnknownCError(99);
422 assert_eq!(format!("{err}"), "Unknown C error code: 99");
423 }
424
425 #[test]
426 fn clibrary_display_uses_human_message() {
427 let err = check_c_error(1).unwrap_err(); let msg = format!("{err}");
431 assert_eq!(msg, "ReadStat C library error: failed to open the file");
432 assert!(!msg.contains("READSTAT_ERROR_OPEN"));
433 }
434
435 #[test]
436 fn cerror_display_all_variants_nonempty() {
437 use num_traits::FromPrimitive;
438 for code in 0..=40 {
439 let e: ReadStatCError = FromPrimitive::from_i32(code).unwrap();
440 assert!(!format!("{e}").is_empty(), "empty Display for code {code}");
441 }
442 }
443
444 #[test]
445 fn error_columns_not_found_display() {
446 let err = ReadStatError::ColumnsNotFound {
447 requested: vec!["foo".into(), "bar".into()],
448 available: vec!["a".into(), "b".into(), "c".into()],
449 };
450 let msg = format!("{err}");
451 assert!(msg.contains("foo"));
452 assert!(msg.contains("bar"));
453 assert!(msg.contains("Available columns"));
454 }
455}