Skip to main content

arrow_tiberius/
error.rs

1//! Error types for `arrow-tiberius`.
2
3use std::fmt;
4
5use snafu::Snafu;
6
7use crate::{DiagnosticSet, WriteBackend};
8
9/// Crate-local result type.
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// Stable write-path phase vocabulary for classifying writer failures.
13///
14/// Phase names match the `phase` field emitted by crate-owned tracing
15/// instrumentation. Diagnostics describe what failed; phases describe where the
16/// failure happened.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[non_exhaustive]
19pub enum WritePhase {
20    /// Arrow-to-SQL Server schema planning.
21    SchemaPlanning,
22    /// Bulk writer initialization.
23    WriterInitialization,
24    /// SQL Server target metadata validation before writer startup completes.
25    TargetMetadataValidation,
26    /// Bulk writer batch write entry point.
27    BatchWrite,
28    /// Runtime record batch schema validation before value conversion.
29    BatchSchemaValidation,
30    /// Runtime Arrow value conversion to SQL Server cells.
31    ValueConversion,
32    /// Direct raw TDS encoding.
33    DirectEncoding,
34    /// Packet or row write to the Tiberius bulk load sink.
35    PacketWrite,
36    /// Bulk writer finish entry point.
37    Finish,
38    /// Tiberius bulk load finalization.
39    Finalize,
40}
41
42impl WritePhase {
43    /// Returns the stable tracing-compatible phase name.
44    pub const fn as_str(self) -> &'static str {
45        match self {
46            Self::SchemaPlanning => "schema_planning",
47            Self::WriterInitialization => "writer_initialization",
48            Self::TargetMetadataValidation => "target_metadata_validation",
49            Self::BatchWrite => "batch_write",
50            Self::BatchSchemaValidation => "batch_schema_validation",
51            Self::ValueConversion => "value_conversion",
52            Self::DirectEncoding => "direct_encoding",
53            Self::PacketWrite => "packet_write",
54            Self::Finish => "finish",
55            Self::Finalize => "finalize",
56        }
57    }
58}
59
60impl fmt::Display for WritePhase {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        f.write_str(self.as_str())
63    }
64}
65
66/// Error type for `arrow-tiberius` operations.
67#[derive(Debug, Snafu)]
68#[non_exhaustive]
69pub enum Error {
70    /// A write-path operation failed in a known writer phase.
71    #[snafu(display("write phase {phase} failed"))]
72    WritePhaseContext {
73        /// Stable writer phase where the failure was observed.
74        phase: WritePhase,
75        /// Original error raised by the failing operation.
76        source: Box<Error>,
77    },
78
79    /// A SQL Server database compatibility level is not supported.
80    #[snafu(display("invalid compatibility level {level}"))]
81    InvalidCompatibilityLevel {
82        /// The unsupported compatibility level value.
83        level: u16,
84    },
85
86    /// A SQL Server identifier is invalid for the selected identifier policy.
87    #[snafu(display("invalid identifier: {reason}"))]
88    InvalidIdentifier {
89        /// Human-readable reason the identifier is invalid.
90        reason: String,
91    },
92
93    /// Arrow schema planning failed.
94    #[snafu(display("write planning failed with {} diagnostic(s)", diagnostics.len()))]
95    Planning {
96        /// Structured planning diagnostics.
97        diagnostics: DiagnosticSet,
98    },
99
100    /// Runtime value conversion failed.
101    #[snafu(display(
102        "value conversion failed with {} diagnostic(s)",
103        diagnostics.len()
104    ))]
105    ValueConversion {
106        /// Structured value conversion diagnostics.
107        diagnostics: DiagnosticSet,
108    },
109
110    /// Direct raw TDS encoding failed.
111    #[snafu(display(
112        "direct encoding failed with {} diagnostic(s)",
113        diagnostics.len()
114    ))]
115    DirectEncoding {
116        /// Structured direct encoding diagnostics.
117        diagnostics: DiagnosticSet,
118    },
119
120    /// A requested write backend is not available.
121    #[snafu(display("write backend {backend:?} is unavailable: {reason}"))]
122    BackendUnavailable {
123        /// Requested write backend.
124        backend: WriteBackend,
125        /// Human-readable reason the backend is unavailable.
126        reason: String,
127    },
128
129    /// A SQL Server ADO-style connection string is invalid.
130    #[snafu(display("SQL Server connection string is invalid"))]
131    InvalidConnectionString,
132
133    /// Opening the TCP connection to SQL Server failed.
134    #[snafu(display("TCP connection to SQL Server failed: {source}"))]
135    ConnectionTcpConnect {
136        /// Source I/O error returned by the TCP transport.
137        source: std::io::Error,
138    },
139
140    /// Tiberius failed while establishing the SQL Server client session.
141    #[snafu(display("SQL Server client setup failed: {source}"))]
142    ConnectionClientSetup {
143        /// Source error returned by Tiberius.
144        source: tiberius::error::Error,
145    },
146
147    /// SQL Server table-existence metadata query failed.
148    #[snafu(display("SQL Server table existence query failed: {source}"))]
149    TableExistsQuery {
150        /// Source error returned by Tiberius.
151        source: tiberius::error::Error,
152    },
153
154    /// SQL Server table-existence metadata query returned an unexpected shape.
155    #[snafu(display("SQL Server table existence query returned an unexpected result: {reason}"))]
156    TableExistsUnexpectedResult {
157        /// Human-readable reason the metadata result could not be interpreted.
158        reason: String,
159    },
160
161    /// SQL Server target row-count query failed.
162    #[snafu(display("SQL Server target row count query failed: {source}"))]
163    TargetRowCountQuery {
164        /// Source error returned by Tiberius.
165        source: tiberius::error::Error,
166    },
167
168    /// SQL Server target row-count query returned an unexpected shape.
169    #[snafu(display("SQL Server target row count query returned an unexpected result: {reason}"))]
170    TargetRowCountUnexpectedResult {
171        /// Human-readable reason the target row-count result could not be interpreted.
172        reason: String,
173    },
174
175    /// SQL Server lifecycle statement execution failed.
176    #[snafu(display("SQL Server statement execution failed: {source}"))]
177    SqlExecution {
178        /// Source error returned by Tiberius.
179        source: tiberius::error::Error,
180    },
181
182    /// Tiberius returned an error while executing a SQL Server operation.
183    #[snafu(display("tiberius operation failed: {source}"))]
184    Tiberius {
185        /// Source error returned by Tiberius.
186        source: tiberius::error::Error,
187    },
188}
189
190impl Error {
191    /// Attaches stable writer phase context to an error.
192    #[must_use]
193    pub fn with_write_phase(self, phase: WritePhase) -> Self {
194        Self::WritePhaseContext {
195            phase,
196            source: Box::new(self),
197        }
198    }
199
200    /// Returns the stable writer phase when the error can be classified.
201    pub const fn write_phase(&self) -> Option<WritePhase> {
202        match self {
203            Self::WritePhaseContext { phase, .. } => Some(*phase),
204            Self::Planning { .. } => Some(WritePhase::SchemaPlanning),
205            Self::ValueConversion { .. } => Some(WritePhase::ValueConversion),
206            Self::DirectEncoding { .. } => Some(WritePhase::DirectEncoding),
207            _ => None,
208        }
209    }
210
211    /// Returns the inner error without writer phase context.
212    pub fn without_write_phase(&self) -> &Self {
213        match self {
214            Self::WritePhaseContext { source, .. } => source.without_write_phase(),
215            _ => self,
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use std::{borrow::Cow, error::Error as StdError};
223
224    use crate::{
225        Diagnostic, DiagnosticCode, DiagnosticSet, Error, WritePhase,
226        observability::{
227            BATCH_SCHEMA_VALIDATION_PHASE, BATCH_WRITE_PHASE, DIRECT_ENCODING_PHASE,
228            FINALIZE_PHASE, FINISH_PHASE, PACKET_WRITE_PHASE, SCHEMA_PLANNING_PHASE,
229            TARGET_METADATA_VALIDATION_PHASE, VALUE_CONVERSION_PHASE, WRITER_INITIALIZATION_PHASE,
230        },
231    };
232
233    #[test]
234    fn planning_error_display_includes_diagnostic_count() {
235        let diagnostics = DiagnosticSet::from(vec![Diagnostic::error(
236            DiagnosticCode::UnsupportedArrowType,
237            "unsupported",
238        )]);
239        let err = Error::Planning { diagnostics };
240
241        assert_eq!(
242            err.to_string(),
243            "write planning failed with 1 diagnostic(s)"
244        );
245    }
246
247    #[test]
248    fn value_conversion_error_display_includes_diagnostic_count() {
249        let diagnostics = DiagnosticSet::from(vec![Diagnostic::error(
250            DiagnosticCode::ValueConversionUnsupported,
251            "unsupported conversion",
252        )]);
253        let err = Error::ValueConversion { diagnostics };
254
255        assert_eq!(
256            err.to_string(),
257            "value conversion failed with 1 diagnostic(s)"
258        );
259    }
260
261    #[test]
262    fn direct_encoding_error_display_includes_diagnostic_count() {
263        let diagnostics = DiagnosticSet::from(vec![Diagnostic::error(
264            DiagnosticCode::DirectEncodingInvalidPayload,
265            "invalid payload",
266        )]);
267        let err = Error::DirectEncoding { diagnostics };
268
269        assert_eq!(
270            err.to_string(),
271            "direct encoding failed with 1 diagnostic(s)"
272        );
273    }
274
275    #[test]
276    fn write_phase_names_match_tracing_vocabulary() {
277        assert_eq!(WritePhase::SchemaPlanning.as_str(), SCHEMA_PLANNING_PHASE);
278        assert_eq!(
279            WritePhase::WriterInitialization.as_str(),
280            WRITER_INITIALIZATION_PHASE
281        );
282        assert_eq!(
283            WritePhase::TargetMetadataValidation.as_str(),
284            TARGET_METADATA_VALIDATION_PHASE
285        );
286        assert_eq!(WritePhase::BatchWrite.as_str(), BATCH_WRITE_PHASE);
287        assert_eq!(
288            WritePhase::BatchSchemaValidation.as_str(),
289            BATCH_SCHEMA_VALIDATION_PHASE
290        );
291        assert_eq!(WritePhase::ValueConversion.as_str(), VALUE_CONVERSION_PHASE);
292        assert_eq!(WritePhase::DirectEncoding.as_str(), DIRECT_ENCODING_PHASE);
293        assert_eq!(WritePhase::PacketWrite.as_str(), PACKET_WRITE_PHASE);
294        assert_eq!(WritePhase::Finish.as_str(), FINISH_PHASE);
295        assert_eq!(WritePhase::Finalize.as_str(), FINALIZE_PHASE);
296    }
297
298    #[test]
299    fn write_phase_context_classifies_without_display_parsing() {
300        let err = Error::Tiberius {
301            source: tiberius::error::Error::BulkInput(Cow::Borrowed("packet failed")),
302        }
303        .with_write_phase(WritePhase::PacketWrite);
304
305        assert_eq!(err.write_phase(), Some(WritePhase::PacketWrite));
306        assert_eq!(err.to_string(), "write phase packet_write failed");
307        assert!(matches!(err.without_write_phase(), Error::Tiberius { .. }));
308    }
309
310    #[test]
311    fn write_phase_context_display_does_not_include_source_text() {
312        let err = Error::Tiberius {
313            source: tiberius::error::Error::BulkInput(Cow::Borrowed(
314                "server=tcp:sql.example.com;User ID=sa;password=secret",
315            )),
316        }
317        .with_write_phase(WritePhase::Finalize);
318
319        assert_eq!(err.to_string(), "write phase finalize failed");
320        assert!(!err.to_string().contains("password=secret"));
321        assert!(!err.to_string().contains("User ID=sa"));
322        assert!(!err.to_string().contains("sql.example.com"));
323    }
324
325    #[test]
326    fn unwrapped_diagnostic_errors_keep_default_phase_classification() {
327        let planning = Error::Planning {
328            diagnostics: DiagnosticSet::from(vec![Diagnostic::error(
329                DiagnosticCode::UnsupportedArrowType,
330                "unsupported",
331            )]),
332        };
333        let value_conversion = Error::ValueConversion {
334            diagnostics: DiagnosticSet::from(vec![Diagnostic::error(
335                DiagnosticCode::NonFiniteFloat,
336                "not finite",
337            )]),
338        };
339        let direct_encoding = Error::DirectEncoding {
340            diagnostics: DiagnosticSet::from(vec![Diagnostic::error(
341                DiagnosticCode::DirectEncodingInvalidPayload,
342                "invalid",
343            )]),
344        };
345
346        assert_eq!(planning.write_phase(), Some(WritePhase::SchemaPlanning));
347        assert_eq!(
348            value_conversion.write_phase(),
349            Some(WritePhase::ValueConversion)
350        );
351        assert_eq!(
352            direct_encoding.write_phase(),
353            Some(WritePhase::DirectEncoding)
354        );
355    }
356
357    #[test]
358    fn backend_unavailable_error_display_includes_backend() {
359        let err = Error::BackendUnavailable {
360            backend: crate::WriteBackend::DirectRawBulk,
361            reason: "not implemented".to_owned(),
362        };
363
364        assert_eq!(
365            err.to_string(),
366            "write backend DirectRawBulk is unavailable: not implemented"
367        );
368    }
369
370    #[test]
371    fn invalid_connection_string_error_display_is_redacted() {
372        let err = Error::InvalidConnectionString;
373
374        assert_eq!(err.to_string(), "SQL Server connection string is invalid");
375    }
376
377    #[test]
378    fn tiberius_error_display_includes_source_error() {
379        let err = Error::Tiberius {
380            source: tiberius::error::Error::Protocol(Cow::Borrowed("invalid token")),
381        };
382
383        assert_eq!(
384            err.to_string(),
385            "tiberius operation failed: Protocol error: invalid token"
386        );
387    }
388
389    #[test]
390    fn tiberius_error_preserves_source_error() {
391        let err = Error::Tiberius {
392            source: tiberius::error::Error::BulkInput(Cow::Borrowed("row payload is malformed")),
393        };
394
395        let source = StdError::source(&err).expect("tiberius source should be preserved");
396        assert_eq!(
397            source.to_string(),
398            "BULK UPLOAD input failure: row payload is malformed"
399        );
400    }
401}