Skip to main content

snowflake_connector_rs/error/
schema.rs

1use std::{
2    borrow::Cow,
3    error::Error as StdError,
4    fmt::{self, Display, Formatter},
5};
6
7use crate::result_table::ColumnType;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum SchemaError {
12    MissingColumn(MissingColumnError),
13    AmbiguousColumn(AmbiguousColumnError),
14    InvalidColumnIndex(InvalidColumnIndexError),
15    DuplicateColumnName(DuplicateColumnNameError),
16    ColumnCountMismatch(ColumnCountMismatchError),
17    IncompatibleColumnType(IncompatibleColumnTypeError),
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct MissingColumnError {
22    name: Box<str>,
23}
24
25impl MissingColumnError {
26    pub(crate) fn new(name: impl Into<Box<str>>) -> Self {
27        Self { name: name.into() }
28    }
29
30    pub fn name(&self) -> &str {
31        &self.name
32    }
33}
34
35impl Display for MissingColumnError {
36    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
37        write!(f, "missing column: {}", self.name)
38    }
39}
40
41impl StdError for MissingColumnError {}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct AmbiguousColumnError {
45    name: Box<str>,
46    candidates: Box<[usize]>,
47}
48
49impl AmbiguousColumnError {
50    pub(crate) fn new(name: impl Into<Box<str>>, candidates: impl Into<Box<[usize]>>) -> Self {
51        Self {
52            name: name.into(),
53            candidates: candidates.into(),
54        }
55    }
56
57    pub fn name(&self) -> &str {
58        &self.name
59    }
60
61    /// Zero-based indices of the columns sharing the ambiguous name.
62    pub fn candidates(&self) -> &[usize] {
63        &self.candidates
64    }
65}
66
67impl Display for AmbiguousColumnError {
68    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
69        write!(f, "ambiguous column: {}", self.name)
70    }
71}
72
73impl StdError for AmbiguousColumnError {}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct InvalidColumnIndexError {
77    index: usize,
78    column_count: usize,
79}
80
81impl InvalidColumnIndexError {
82    pub(crate) fn new(index: usize, column_count: usize) -> Self {
83        Self {
84            index,
85            column_count,
86        }
87    }
88
89    pub fn index(&self) -> usize {
90        self.index
91    }
92
93    pub fn column_count(&self) -> usize {
94        self.column_count
95    }
96}
97
98impl Display for InvalidColumnIndexError {
99    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
100        write!(
101            f,
102            "invalid column index {} for schema with {} columns",
103            self.index, self.column_count
104        )
105    }
106}
107
108impl StdError for InvalidColumnIndexError {}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct DuplicateColumnNameError {
112    name: Box<str>,
113}
114
115impl DuplicateColumnNameError {
116    pub(crate) fn new(name: impl Into<Box<str>>) -> Self {
117        Self { name: name.into() }
118    }
119
120    pub fn name(&self) -> &str {
121        &self.name
122    }
123}
124
125impl Display for DuplicateColumnNameError {
126    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
127        write!(f, "duplicate column name in result: {}", self.name)
128    }
129}
130
131impl StdError for DuplicateColumnNameError {}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct ColumnCountMismatchError {
135    expected: usize,
136    actual: usize,
137}
138
139impl ColumnCountMismatchError {
140    // Must stay `pub`: the `FromRow` derive emits a call to this constructor in downstream crates.
141    pub fn new(expected: usize, actual: usize) -> Self {
142        Self { expected, actual }
143    }
144
145    pub fn expected(&self) -> usize {
146        self.expected
147    }
148
149    pub fn actual(&self) -> usize {
150        self.actual
151    }
152}
153
154impl Display for ColumnCountMismatchError {
155    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
156        write!(
157            f,
158            "column count mismatch (expected {}, actual {})",
159            self.expected, self.actual
160        )
161    }
162}
163
164impl StdError for ColumnCountMismatchError {}
165
166/// A column's Snowflake type cannot be decoded into the requested Rust type.
167///
168/// Raised while building a row decode plan, before any row is read, so a type mismatch fails the whole
169/// [`rows`](crate::ResultTable::rows) call rather than surfacing per cell.
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct IncompatibleColumnTypeError {
172    column_index: usize,
173    column_name: Box<str>,
174    target_type_name: Cow<'static, str>,
175    actual_column_type: ColumnType,
176    detail: Option<Box<str>>,
177}
178
179impl IncompatibleColumnTypeError {
180    pub(crate) fn new(
181        column_index: usize,
182        column_name: impl Into<Box<str>>,
183        target_type_name: impl Into<Cow<'static, str>>,
184        actual_column_type: ColumnType,
185        detail: Option<Box<str>>,
186    ) -> Self {
187        Self {
188            column_index,
189            column_name: column_name.into(),
190            target_type_name: target_type_name.into(),
191            actual_column_type,
192            detail,
193        }
194    }
195
196    pub fn column_index(&self) -> usize {
197        self.column_index
198    }
199
200    pub fn column_name(&self) -> &str {
201        &self.column_name
202    }
203
204    pub fn target_type_name(&self) -> &str {
205        &self.target_type_name
206    }
207
208    pub fn actual_column_type(&self) -> &ColumnType {
209        &self.actual_column_type
210    }
211
212    /// Extra context beyond the type tags, such as an out-of-range scale.
213    pub fn detail(&self) -> Option<&str> {
214        self.detail.as_deref()
215    }
216}
217
218impl Display for IncompatibleColumnTypeError {
219    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
220        write!(
221            f,
222            "column {} ({}) of type {} cannot be decoded as {}",
223            self.column_index, self.column_name, self.actual_column_type, self.target_type_name
224        )?;
225        if let Some(detail) = &self.detail {
226            write!(f, " ({detail})")?;
227        }
228        Ok(())
229    }
230}
231
232impl StdError for IncompatibleColumnTypeError {}
233
234impl Display for SchemaError {
235    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
236        match self {
237            Self::MissingColumn(error) => error.fmt(f),
238            Self::AmbiguousColumn(error) => error.fmt(f),
239            Self::InvalidColumnIndex(error) => error.fmt(f),
240            Self::DuplicateColumnName(error) => error.fmt(f),
241            Self::ColumnCountMismatch(error) => error.fmt(f),
242            Self::IncompatibleColumnType(error) => error.fmt(f),
243        }
244    }
245}
246
247impl StdError for SchemaError {}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn schema_error_display_formats_missing_and_ambiguous_variants() {
255        assert_eq!(
256            SchemaError::MissingColumn(MissingColumnError::new("value")).to_string(),
257            "missing column: value"
258        );
259        assert_eq!(
260            SchemaError::AmbiguousColumn(AmbiguousColumnError::new(
261                "value",
262                vec![0, 1].into_boxed_slice(),
263            ))
264            .to_string(),
265            "ambiguous column: value"
266        );
267    }
268
269    #[test]
270    fn schema_error_accessors_expose_structured_fields() {
271        let ambiguous = AmbiguousColumnError::new("value", vec![0, 1].into_boxed_slice());
272        assert_eq!(ambiguous.name(), "value");
273        assert_eq!(ambiguous.candidates(), &[0, 1]);
274
275        let invalid = InvalidColumnIndexError::new(7, 3);
276        assert_eq!(invalid.index(), 7);
277        assert_eq!(invalid.column_count(), 3);
278
279        let duplicate = DuplicateColumnNameError::new("id");
280        assert_eq!(duplicate.name(), "id");
281
282        let mismatch = ColumnCountMismatchError::new(4, 2);
283        assert_eq!(mismatch.expected(), 4);
284        assert_eq!(mismatch.actual(), 2);
285    }
286
287    #[test]
288    fn schema_error_display_formats_remaining_variants() {
289        assert_eq!(
290            SchemaError::InvalidColumnIndex(InvalidColumnIndexError::new(7, 3)).to_string(),
291            "invalid column index 7 for schema with 3 columns"
292        );
293        assert_eq!(
294            SchemaError::DuplicateColumnName(DuplicateColumnNameError::new("id")).to_string(),
295            "duplicate column name in result: id"
296        );
297        assert_eq!(
298            SchemaError::ColumnCountMismatch(ColumnCountMismatchError::new(4, 2)).to_string(),
299            "column count mismatch (expected 4, actual 2)"
300        );
301    }
302
303    #[test]
304    fn incompatible_column_type_exposes_fields_and_appends_detail() {
305        let without_detail = IncompatibleColumnTypeError::new(
306            2,
307            "TS",
308            "chrono::NaiveDateTime",
309            ColumnType::Boolean,
310            None,
311        );
312        assert_eq!(without_detail.column_index(), 2);
313        assert_eq!(without_detail.column_name(), "TS");
314        assert_eq!(without_detail.target_type_name(), "chrono::NaiveDateTime");
315        assert_eq!(without_detail.actual_column_type(), &ColumnType::Boolean);
316        assert_eq!(without_detail.detail(), None);
317        assert_eq!(
318            without_detail.to_string(),
319            "column 2 (TS) of type boolean cannot be decoded as chrono::NaiveDateTime"
320        );
321
322        let with_detail = IncompatibleColumnTypeError::new(
323            0,
324            "T",
325            "chrono::NaiveTime",
326            ColumnType::Time { scale: Some(12) },
327            Some(Box::from("invalid time scale: 12")),
328        );
329        assert_eq!(with_detail.detail(), Some("invalid time scale: 12"));
330        assert!(
331            with_detail
332                .to_string()
333                .ends_with("(invalid time scale: 12)"),
334            "actual: {with_detail}"
335        );
336    }
337}