Skip to main content

snowflake_connector_rs/error/
decode.rs

1use std::{
2    borrow::Cow,
3    error::Error as StdError,
4    fmt::{self, Display},
5};
6
7use crate::result_table::ColumnType;
8
9use super::{SchemaError, VALUE_PREVIEW_MAX_CHARS, truncate_preview_chars};
10
11/// Result alias for cell-local decode failures.
12pub type CellDecodeResult<T> = std::result::Result<T, CellConversionError>;
13
14/// Cell-local reason why decoding a value failed.
15///
16/// This describes only the local conversion problem. Row, column, and value context live on [`CellDecodeError`].
17#[derive(Debug)]
18pub struct CellConversionError {
19    reason: Box<str>,
20    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
21}
22
23impl CellConversionError {
24    /// Build a cell error with just a reason, equivalent to `builder(reason).build()`.
25    pub fn new(reason: impl Into<Box<str>>) -> Self {
26        Self::builder(reason).build()
27    }
28
29    pub fn builder(reason: impl Into<Box<str>>) -> CellConversionErrorBuilder {
30        CellConversionErrorBuilder {
31            reason: reason.into(),
32            source: None,
33        }
34    }
35
36    pub fn reason(&self) -> &str {
37        &self.reason
38    }
39
40    pub fn source(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
41        self.source.as_deref()
42    }
43}
44
45impl Display for CellConversionError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.write_str(&self.reason)
48    }
49}
50
51impl StdError for CellConversionError {
52    fn source(&self) -> Option<&(dyn StdError + 'static)> {
53        self.source.as_deref().map(|source| source as _)
54    }
55}
56
57#[derive(Debug)]
58pub struct CellConversionErrorBuilder {
59    reason: Box<str>,
60    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
61}
62
63impl CellConversionErrorBuilder {
64    pub fn source(mut self, source: impl Into<Box<dyn StdError + Send + Sync + 'static>>) -> Self {
65        self.source = Some(source.into());
66        self
67    }
68
69    pub fn build(self) -> CellConversionError {
70        CellConversionError {
71            reason: self.reason,
72            source: self.source,
73        }
74    }
75}
76
77/// Plan-time decode failure raised by a hand-written [`FromCell::build_plan`](crate::FromCell::build_plan)
78/// or [`FromRow::build_plan`](crate::FromRow::build_plan).
79///
80/// Use this for custom validation of column metadata or row shape. The connector's structured schema mismatches still
81/// return [`SchemaError`](crate::error::SchemaError).
82///
83/// Callers provide only a reason and optional source. The connector adds column context for failures returned through
84/// [`CellPlan::new`](crate::CellPlan); row-level plan failures keep no column context.
85#[derive(Debug)]
86pub struct CustomPlanError {
87    reason: Box<str>,
88    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
89    column_index: Option<usize>,
90    column_name: Option<Box<str>>,
91}
92
93impl CustomPlanError {
94    /// Build a plan-time error with just a reason, equivalent to `builder(reason).build()`.
95    pub fn new(reason: impl Into<Box<str>>) -> Self {
96        Self::builder(reason).build()
97    }
98
99    pub fn builder(reason: impl Into<Box<str>>) -> CustomPlanErrorBuilder {
100        CustomPlanErrorBuilder {
101            reason: reason.into(),
102            source: None,
103        }
104    }
105
106    pub fn reason(&self) -> &str {
107        &self.reason
108    }
109
110    pub fn source(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
111        self.source.as_deref()
112    }
113
114    /// The column this plan was being built for, filled in by the connector for
115    /// [`FromCell::build_plan`](crate::FromCell::build_plan) failures.
116    pub fn column_index(&self) -> Option<usize> {
117        self.column_index
118    }
119
120    pub fn column_name(&self) -> Option<&str> {
121        self.column_name.as_deref()
122    }
123
124    /// Fill in the column context if it has not already been set.
125    ///
126    /// Nested plan construction resolves the innermost column first, so later enclosing plans do not overwrite it.
127    pub(crate) fn set_column_context(
128        &mut self,
129        column_index: usize,
130        column_name: impl Into<Box<str>>,
131    ) {
132        if self.column_index.is_some() {
133            return;
134        }
135        self.column_index = Some(column_index);
136        self.column_name = Some(column_name.into());
137    }
138}
139
140impl Display for CustomPlanError {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match (self.column_index, &self.column_name) {
143            (Some(column_index), Some(column_name)) => write!(
144                f,
145                "decode plan error at column_index {column_index} ({column_name}): {}",
146                self.reason
147            ),
148            _ => write!(f, "decode plan error: {}", self.reason),
149        }
150    }
151}
152
153impl StdError for CustomPlanError {
154    fn source(&self) -> Option<&(dyn StdError + 'static)> {
155        self.source.as_deref().map(|source| source as _)
156    }
157}
158
159#[derive(Debug)]
160pub struct CustomPlanErrorBuilder {
161    reason: Box<str>,
162    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
163}
164
165impl CustomPlanErrorBuilder {
166    pub fn source(mut self, source: impl Into<Box<dyn StdError + Send + Sync + 'static>>) -> Self {
167        self.source = Some(source.into());
168        self
169    }
170
171    pub fn build(self) -> CustomPlanError {
172        CustomPlanError {
173            reason: self.reason,
174            source: self.source,
175            column_index: None,
176            column_name: None,
177        }
178    }
179}
180
181/// Row-level conversion failure raised by a hand-written
182/// [`FromRow::from_row_with_plan`](crate::FromRow::from_row_with_plan).
183///
184/// Use this when decoded cells are individually valid but fail a row-level domain rule. Cell-local failures should stay
185/// [`CellConversionError`].
186///
187/// Callers provide only a reason and optional source. The connector adds `row_index` when iteration yields the failure.
188#[derive(Debug)]
189pub struct RowConversionError {
190    reason: Box<str>,
191    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
192    row_index: Option<usize>,
193}
194
195impl RowConversionError {
196    /// Build a row-level error with just a reason, equivalent to `builder(reason).build()`.
197    pub fn new(reason: impl Into<Box<str>>) -> Self {
198        Self::builder(reason).build()
199    }
200
201    pub fn builder(reason: impl Into<Box<str>>) -> RowConversionErrorBuilder {
202        RowConversionErrorBuilder {
203            reason: reason.into(),
204            source: None,
205        }
206    }
207
208    pub fn reason(&self) -> &str {
209        &self.reason
210    }
211
212    pub fn source(&self) -> Option<&(dyn StdError + Send + Sync + 'static)> {
213        self.source.as_deref()
214    }
215
216    /// Zero-based index of the failing row, filled in by the connector's decode loop.
217    pub fn row_index(&self) -> Option<usize> {
218        self.row_index
219    }
220
221    /// Fill in the failing row's index if it has not already been set.
222    pub(crate) fn set_row_index(&mut self, row_index: usize) {
223        if self.row_index.is_none() {
224            self.row_index = Some(row_index);
225        }
226    }
227}
228
229impl Display for RowConversionError {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self.row_index {
232            Some(row_index) => {
233                write!(
234                    f,
235                    "row conversion error at row_index {row_index}: {}",
236                    self.reason
237                )
238            }
239            None => write!(f, "row conversion error: {}", self.reason),
240        }
241    }
242}
243
244impl StdError for RowConversionError {
245    fn source(&self) -> Option<&(dyn StdError + 'static)> {
246        self.source.as_deref().map(|source| source as _)
247    }
248}
249
250#[derive(Debug)]
251pub struct RowConversionErrorBuilder {
252    reason: Box<str>,
253    source: Option<Box<dyn StdError + Send + Sync + 'static>>,
254}
255
256impl RowConversionErrorBuilder {
257    pub fn source(mut self, source: impl Into<Box<dyn StdError + Send + Sync + 'static>>) -> Self {
258        self.source = Some(source.into());
259        self
260    }
261
262    pub fn build(self) -> RowConversionError {
263        RowConversionError {
264            reason: self.reason,
265            source: self.source,
266            row_index: None,
267        }
268    }
269}
270
271/// Cell decode failure with row and column context.
272#[derive(Debug)]
273pub struct CellDecodeError {
274    inner: Box<CellDecodeErrorInner>,
275}
276
277#[derive(Debug)]
278struct CellDecodeErrorInner {
279    row_index: usize,
280    column_index: usize,
281    column_name: Box<str>,
282    target_type_name: Cow<'static, str>,
283    actual_column_type: ColumnType,
284    raw_value_preview: Option<Box<str>>,
285    issue: CellConversionError,
286}
287
288impl CellDecodeError {
289    pub(crate) fn new(
290        row_index: usize,
291        column_index: usize,
292        column_name: impl Into<Box<str>>,
293        target_type_name: impl Into<Cow<'static, str>>,
294        actual_column_type: ColumnType,
295        raw_value_preview: Option<&str>,
296        issue: CellConversionError,
297    ) -> Self {
298        Self {
299            inner: Box::new(CellDecodeErrorInner {
300                row_index,
301                column_index,
302                column_name: column_name.into(),
303                target_type_name: target_type_name.into(),
304                actual_column_type,
305                raw_value_preview: raw_value_preview
306                    .map(|preview| truncate_preview_chars(preview, VALUE_PREVIEW_MAX_CHARS)),
307                issue,
308            }),
309        }
310    }
311
312    pub fn row_index(&self) -> usize {
313        self.inner.row_index
314    }
315
316    pub fn column_index(&self) -> usize {
317        self.inner.column_index
318    }
319
320    pub fn column_name(&self) -> &str {
321        &self.inner.column_name
322    }
323
324    pub fn target_type_name(&self) -> &str {
325        &self.inner.target_type_name
326    }
327
328    pub fn actual_column_type(&self) -> &ColumnType {
329        &self.inner.actual_column_type
330    }
331
332    pub fn raw_value_preview(&self) -> Option<&str> {
333        self.inner.raw_value_preview.as_deref()
334    }
335
336    pub fn conversion_error(&self) -> &CellConversionError {
337        &self.inner.issue
338    }
339}
340
341impl Display for CellDecodeError {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        let inner = &*self.inner;
344        write!(
345            f,
346            "row_index {} column_index {} ({}): target_type {}, found {}",
347            inner.row_index,
348            inner.column_index,
349            inner.column_name,
350            inner.target_type_name,
351            inner.actual_column_type
352        )?;
353        if let Some(preview) = &inner.raw_value_preview {
354            write!(f, ", value: {preview:?}")?;
355        }
356        if !inner.issue.reason().is_empty() {
357            write!(f, " ({})", inner.issue.reason())?;
358        }
359        Ok(())
360    }
361}
362
363impl StdError for CellDecodeError {
364    fn source(&self) -> Option<&(dyn StdError + 'static)> {
365        self.inner
366            .issue
367            .source()
368            .map(|_| &self.inner.issue as &(dyn StdError + 'static))
369    }
370}
371
372/// Error union returned by the plan-time decode hooks ([`FromCell::build_plan`](crate::FromCell::build_plan),
373/// [`FromRow::build_plan`](crate::FromRow::build_plan), and [`CellPlan`](crate::CellPlan) resolution).
374///
375/// This keeps plan hooks limited to schema failures and custom validation failures. At the connector boundary, each
376/// variant maps to [`crate::Error`] with `ErrorKind::Decode`.
377///
378/// Like [`SchemaError`], this enum is non-exhaustive: downstream crates can build variants and convert through the
379/// `From` impls, but cannot match it exhaustively.
380#[derive(Debug)]
381#[non_exhaustive]
382pub enum PlanBuildError {
383    /// A structured schema mismatch, from column resolution or a built-in column-type check.
384    Schema(SchemaError),
385    /// A hand-written plan's free-form validation failure.
386    Custom(CustomPlanError),
387}
388
389/// Error union returned by [`FromRow::from_row_with_plan`](crate::FromRow::from_row_with_plan).
390///
391/// The [`Schema`](RowDecodeError::Schema) variant lets a hand-written row decoder propagate a dynamic
392/// [`RowRef::cell_at`](crate::RowRef::cell_at) lookup failure with `?`.
393#[derive(Debug)]
394#[non_exhaustive]
395pub enum RowDecodeError {
396    /// A dynamic column lookup failed, e.g. from [`RowRef::cell_at`](crate::RowRef::cell_at).
397    Schema(SchemaError),
398    /// A cell failed to decode, surfaced through [`RowRef::get_with_plan`](crate::RowRef::get_with_plan).
399    Cell(CellDecodeError),
400    /// A hand-written row decoder rejected an otherwise-decoded row.
401    Conversion(RowConversionError),
402}
403
404/// Result alias for the plan-time decode hooks.
405pub type PlanBuildResult<T> = std::result::Result<T, PlanBuildError>;
406
407/// Result alias for [`FromRow::from_row_with_plan`](crate::FromRow::from_row_with_plan).
408pub type RowDecodeResult<T> = std::result::Result<T, RowDecodeError>;
409
410impl Display for PlanBuildError {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        match self {
413            Self::Schema(error) => Display::fmt(error, f),
414            Self::Custom(error) => Display::fmt(error, f),
415        }
416    }
417}
418
419impl StdError for PlanBuildError {
420    fn source(&self) -> Option<&(dyn StdError + 'static)> {
421        match self {
422            Self::Schema(error) => StdError::source(error),
423            Self::Custom(error) => StdError::source(error),
424        }
425    }
426}
427
428impl Display for RowDecodeError {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        match self {
431            Self::Schema(error) => Display::fmt(error, f),
432            Self::Cell(error) => Display::fmt(error, f),
433            Self::Conversion(error) => Display::fmt(error, f),
434        }
435    }
436}
437
438impl StdError for RowDecodeError {
439    fn source(&self) -> Option<&(dyn StdError + 'static)> {
440        match self {
441            Self::Schema(error) => StdError::source(error),
442            Self::Cell(error) => StdError::source(error),
443            Self::Conversion(error) => StdError::source(error),
444        }
445    }
446}
447
448impl From<SchemaError> for PlanBuildError {
449    fn from(error: SchemaError) -> Self {
450        Self::Schema(error)
451    }
452}
453
454impl From<CustomPlanError> for PlanBuildError {
455    fn from(error: CustomPlanError) -> Self {
456        Self::Custom(error)
457    }
458}
459
460impl From<SchemaError> for RowDecodeError {
461    fn from(error: SchemaError) -> Self {
462        Self::Schema(error)
463    }
464}
465
466impl From<CellDecodeError> for RowDecodeError {
467    fn from(error: CellDecodeError) -> Self {
468        Self::Cell(error)
469    }
470}
471
472impl From<RowConversionError> for RowDecodeError {
473    fn from(error: RowConversionError) -> Self {
474        Self::Conversion(error)
475    }
476}