Skip to main content

robin_sparkless_core/
error.rs

1//! Engine error type for embedders.
2//!
3//! Use [`EngineError`] when you want to map robin-sparkless and Polars errors
4//! to a single type (e.g. for FFI or CLI) without depending on Polars error types.
5//!
6//! Note: `From<PolarsError>` for `EngineError` is implemented in the main robin-sparkless
7//! crate, which has a Polars dependency.
8
9use std::fmt;
10
11/// Unified error type for robin-sparkless operations.
12///
13/// Embedders (Python, Node, CLI) can map these variants to native errors
14/// without depending on `PolarsError`.
15#[derive(Debug)]
16pub enum EngineError {
17    /// User-facing error (invalid input, unsupported operation).
18    User(String),
19    /// Internal / compute error.
20    Internal(String),
21    /// I/O error (file not found, permission, etc.).
22    Io(String),
23    /// SQL parsing or execution error.
24    Sql(String),
25    /// Resource not found (column, table, file).
26    NotFound(String),
27    /// Other / unclassified.
28    Other(String),
29}
30
31impl fmt::Display for EngineError {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            EngineError::User(s) => write!(f, "user error: {s}"),
35            EngineError::Internal(s) => write!(f, "internal error: {s}"),
36            EngineError::Io(s) => write!(f, "io error: {s}"),
37            EngineError::Sql(s) => write!(f, "sql error: {s}"),
38            EngineError::NotFound(s) => write!(f, "not found: {s}"),
39            EngineError::Other(s) => write!(f, "{s}"),
40        }
41    }
42}
43
44impl std::error::Error for EngineError {}
45
46impl EngineError {
47    /// User-facing message without variant prefixes (for FFI / Python bindings).
48    pub fn user_message(&self) -> &str {
49        match self {
50            EngineError::User(s)
51            | EngineError::Internal(s)
52            | EngineError::Io(s)
53            | EngineError::Sql(s)
54            | EngineError::NotFound(s)
55            | EngineError::Other(s) => s.as_str(),
56        }
57    }
58}
59
60/// Normalize column-not-found text to PySpark-style "cannot be resolved" wording.
61pub fn normalize_unresolved_column_message(msg: &str) -> String {
62    let lower = msg.to_lowercase();
63    if lower.contains("cannot be resolved") {
64        if lower.contains("unresolved_column") {
65            return msg.to_string();
66        }
67        return format!("unresolved_column: {msg}");
68    }
69    if lower.contains("cannot resolve") {
70        let msg = msg.replace("cannot resolve", "cannot be resolved");
71        if lower.contains("unresolved_column") {
72            return msg;
73        }
74        return format!("unresolved_column: {msg}");
75    }
76    if msg.contains("unable to find column")
77        || (msg.contains("not found") && lower.contains("column"))
78        || msg.contains("valid columns")
79    {
80        return format!("unresolved_column: cannot be resolved: {msg}");
81    }
82    msg.to_string()
83}
84
85impl From<serde_json::Error> for EngineError {
86    fn from(e: serde_json::Error) -> Self {
87        EngineError::Internal(e.to_string())
88    }
89}
90
91impl From<std::io::Error> for EngineError {
92    fn from(e: std::io::Error) -> Self {
93        EngineError::Io(e.to_string())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn engine_error_display() {
103        assert_eq!(
104            EngineError::User("bad input".into()).to_string(),
105            "user error: bad input"
106        );
107        assert_eq!(
108            EngineError::Internal("panic".into()).to_string(),
109            "internal error: panic"
110        );
111        assert_eq!(
112            EngineError::Io("file not found".into()).to_string(),
113            "io error: file not found"
114        );
115        assert_eq!(
116            EngineError::Sql("parse error".into()).to_string(),
117            "sql error: parse error"
118        );
119        assert_eq!(
120            EngineError::NotFound("column x".into()).to_string(),
121            "not found: column x"
122        );
123        assert_eq!(EngineError::Other("misc".into()).to_string(), "misc");
124    }
125
126    #[test]
127    fn engine_error_from_io() {
128        let e = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
129        let err: EngineError = e.into();
130        assert!(err.to_string().contains("file missing"));
131        assert!(err.to_string().contains("io error"));
132    }
133
134    #[test]
135    fn engine_error_from_serde_json() {
136        let bad = b"\x80";
137        let e: Result<(), _> = serde_json::from_slice(bad);
138        let err: EngineError = e.unwrap_err().into();
139        assert!(err.to_string().contains("internal error"));
140    }
141}