Skip to main content

alopex_dataframe/
error.rs

1use std::path::PathBuf;
2
3use crate::physical::budget::{ResourceScope, StreamFailureClass};
4
5/// Errors returned by `alopex-dataframe` operations.
6#[derive(Debug, thiserror::Error)]
7pub enum DataFrameError {
8    /// OS-level I/O error (optionally associated with a path).
9    #[error("I/O error{path}: {source}", path = path_display(.path))]
10    Io {
11        source: std::io::Error,
12        path: Option<PathBuf>,
13    },
14
15    /// Arrow schema-related mismatch (e.g. different schema across batches).
16    #[error("schema mismatch: {message}")]
17    SchemaMismatch { message: String },
18
19    /// Data type mismatch (e.g. non-numeric aggregation or incompatible dtypes).
20    #[error(
21        "type mismatch{column}: expected {expected}, got {actual}",
22        column = column_display(.column)
23    )]
24    TypeMismatch {
25        column: Option<String>,
26        expected: String,
27        actual: String,
28    },
29
30    /// Referenced column does not exist.
31    #[error("column not found: {name}")]
32    ColumnNotFound { name: String },
33
34    /// Operation is not supported or invalid for the current inputs.
35    #[error("invalid operation: {message}")]
36    InvalidOperation { message: String },
37
38    /// Invalid configuration option was provided.
39    #[error("invalid configuration option '{option}': {message}")]
40    Configuration { option: String, message: String },
41
42    /// Error originating from Arrow compute / record batch APIs.
43    #[error("arrow error: {source}")]
44    Arrow { source: arrow::error::ArrowError },
45
46    /// Error originating from Parquet APIs.
47    #[error("parquet error: {source}")]
48    Parquet {
49        source: parquet::errors::ParquetError,
50    },
51
52    /// A bounded execution reservation was rejected before the requested ownership began.
53    #[error(
54        "resource limit exceeded in {scope}: requested total {observed} bytes exceeds {limit} bytes \\
55         (in-flight batches {observed_batches}/{max_in_flight_batches})",
56        scope = scope.as_str()
57    )]
58    ResourceLimitExceeded {
59        /// Configured byte limit.
60        limit: u64,
61        /// Requested byte total at the failed reservation point.
62        observed: u64,
63        /// Configured in-flight batch limit.
64        max_in_flight_batches: usize,
65        /// Requested in-flight batch total at the failed reservation point.
66        observed_batches: usize,
67        /// Allocation domain that requested the reservation.
68        scope: ResourceScope,
69    },
70
71    /// A stream was closed by its consumer before normal completion.
72    #[error("stream is closed")]
73    StreamClosed,
74
75    /// A stream was cancelled by its consumer before normal completion.
76    #[error("stream is cancelled")]
77    StreamCancelled,
78
79    /// A stream failed and must reproduce its initial structured diagnostic.
80    #[error("stream failed: {code} ({classification:?})")]
81    StreamFailed {
82        /// Stable failure code.
83        code: &'static str,
84        /// Stable broad failure classification.
85        classification: StreamFailureClass,
86    },
87
88    /// Streaming was rejected before the source was opened.
89    #[error("streaming unsupported for {subject}: {reason}")]
90    StreamingUnsupported { subject: String, reason: String },
91
92    /// Streaming would require global materialization and has no approved bounded algorithm.
93    #[error("streaming requires materialization for {subject}: {reason}")]
94    StreamingRequiresMaterialization { subject: String, reason: String },
95}
96
97/// Result type used throughout this crate.
98pub type Result<T> = std::result::Result<T, DataFrameError>;
99
100impl DataFrameError {
101    /// Create an I/O error without a path.
102    pub fn io(source: std::io::Error) -> Self {
103        Self::Io { source, path: None }
104    }
105
106    /// Create an I/O error associated with a path.
107    pub fn io_with_path(source: std::io::Error, path: impl Into<PathBuf>) -> Self {
108        Self::Io {
109            source,
110            path: Some(path.into()),
111        }
112    }
113
114    /// Create a schema mismatch error with a message.
115    pub fn schema_mismatch(message: impl Into<String>) -> Self {
116        Self::SchemaMismatch {
117            message: message.into(),
118        }
119    }
120
121    /// Create a type mismatch error with optional column context.
122    pub fn type_mismatch(
123        column: impl Into<Option<String>>,
124        expected: impl Into<String>,
125        actual: impl Into<String>,
126    ) -> Self {
127        Self::TypeMismatch {
128            column: column.into(),
129            expected: expected.into(),
130            actual: actual.into(),
131        }
132    }
133
134    /// Create a missing column error.
135    pub fn column_not_found(name: impl Into<String>) -> Self {
136        Self::ColumnNotFound { name: name.into() }
137    }
138
139    /// Create an invalid operation error.
140    pub fn invalid_operation(message: impl Into<String>) -> Self {
141        Self::InvalidOperation {
142            message: message.into(),
143        }
144    }
145
146    /// Create an invalid configuration error.
147    pub fn configuration(option: impl Into<String>, message: impl Into<String>) -> Self {
148        Self::Configuration {
149            option: option.into(),
150            message: message.into(),
151        }
152    }
153
154    /// Create a structured resource-limit error.
155    pub fn resource_limit_exceeded(
156        limit: u64,
157        observed: u64,
158        max_in_flight_batches: usize,
159        observed_batches: usize,
160        scope: ResourceScope,
161    ) -> Self {
162        Self::ResourceLimitExceeded {
163            limit,
164            observed,
165            max_in_flight_batches,
166            observed_batches,
167            scope,
168        }
169    }
170
171    /// Create the repeatable early-close terminal error.
172    pub const fn stream_closed() -> Self {
173        Self::StreamClosed
174    }
175
176    /// Create the repeatable cancellation terminal error.
177    pub const fn stream_cancelled() -> Self {
178        Self::StreamCancelled
179    }
180
181    /// Create the repeatable structured stream-failure error.
182    pub const fn stream_failed(code: &'static str, classification: StreamFailureClass) -> Self {
183        Self::StreamFailed {
184            code,
185            classification,
186        }
187    }
188
189    /// Create a source-open preflight rejection without falling back to eager execution.
190    pub fn streaming_unsupported(subject: impl Into<String>, reason: impl Into<String>) -> Self {
191        Self::StreamingUnsupported {
192            subject: subject.into(),
193            reason: reason.into(),
194        }
195    }
196
197    /// Create a preflight rejection for a plan requiring unimplemented global materialization.
198    pub fn streaming_requires_materialization(
199        subject: impl Into<String>,
200        reason: impl Into<String>,
201    ) -> Self {
202        Self::StreamingRequiresMaterialization {
203            subject: subject.into(),
204            reason: reason.into(),
205        }
206    }
207}
208
209fn column_display(column: &Option<String>) -> String {
210    column
211        .as_ref()
212        .map(|c| format!(" for column '{c}'"))
213        .unwrap_or_default()
214}
215
216fn path_display(path: &Option<PathBuf>) -> String {
217    path.as_ref()
218        .map(|p| format!(" for path '{}'", p.display()))
219        .unwrap_or_default()
220}