Skip to main content

timeseries_table_format/table/
error.rs

1//! Top-level error facade for table operations.
2//!
3//! [`TableError`] adds only operation context. Each variant wraps the complete
4//! error owned by that operation and delegates its source and backtrace. Add
5//! detailed failures to the owning operation error rather than copying
6//! subsystem-specific variants into this facade.
7
8use snafu::prelude::*;
9
10use super::operations::{
11    AppendError, CoverageQueryError, CreateTableError, OpenTableError, OptimizeError, ScanError,
12    TableStateAccessError, VacuumError,
13};
14
15/// Errors from high-level time-series table operations.
16///
17/// Each variant carries enough context for callers to surface actionable
18/// messages to users or implement retries where appropriate (for example,
19/// conflicts on optimistic concurrency control).
20#[derive(Debug, Snafu)]
21#[snafu(visibility(pub(crate)))]
22#[non_exhaustive]
23pub enum TableError {
24    /// A table creation operation failed.
25    #[snafu(display("Table creation failed: {source}"))]
26    Create {
27        /// Complete table creation failure.
28        #[snafu(source, backtrace)]
29        source: CreateTableError,
30    },
31
32    /// A table open operation failed.
33    #[snafu(display("Table open failed: {source}"))]
34    Open {
35        /// Complete table open failure.
36        #[snafu(source, backtrace)]
37        source: OpenTableError,
38    },
39
40    /// A table state access or refresh operation failed.
41    #[snafu(display("Table state access failed: {source}"))]
42    StateAccess {
43        /// Complete state-access failure.
44        #[snafu(source, backtrace)]
45        source: TableStateAccessError,
46    },
47
48    /// An append operation failed.
49    #[snafu(display("Append failed: {source}"))]
50    Append {
51        /// Complete append-owned failure.
52        #[snafu(source, backtrace)]
53        source: AppendError,
54    },
55
56    /// A table scan failed during planning or lazy execution.
57    #[snafu(display("Table scan failed: {source}"))]
58    Scan {
59        /// Complete scan operation error.
60        #[snafu(source, backtrace)]
61        source: ScanError,
62    },
63
64    /// A table coverage query failed.
65    #[snafu(display("Table coverage query failed: {source}"))]
66    CoverageQuery {
67        /// Complete coverage query operation error.
68        #[snafu(source, backtrace)]
69        source: CoverageQueryError,
70    },
71
72    /// An entity-layout optimization operation failed.
73    #[snafu(display("Entity-layout optimization failed: {source}"))]
74    Optimize {
75        /// Complete optimization-owned failure.
76        #[snafu(source, backtrace)]
77        source: OptimizeError,
78    },
79
80    /// A vacuum operation failed.
81    #[snafu(display("Vacuum failed: {source}"))]
82    Vacuum {
83        /// Complete vacuum-owned failure.
84        #[snafu(source, backtrace)]
85        source: VacuumError,
86    },
87}
88
89#[cfg(test)]
90mod tests {
91    use std::{error::Error as _, io};
92
93    use arrow::error::ArrowError;
94    use parquet::errors::ParquetError;
95    use snafu::{Backtrace, ErrorCompat, IntoError};
96
97    use crate::coverage::{CoverageCodecError, CoverageSidecarError};
98    use crate::formats::parquet::EntityRewriteError;
99    use crate::metadata::{
100        index::IndexSpecError, logical_schema::LogicalToArrowSchemaError,
101        schema_compat::SchemaCompatibilityError,
102    };
103    use crate::storage::StorageError;
104    use crate::transaction_log::CommitError;
105
106    use super::*;
107
108    #[test]
109    fn append_facade_preserves_arrow_source_and_backtrace() {
110        let append_error = AppendError::ArrowInput {
111            source: ArrowError::ComputeError("input failed".to_string()),
112            backtrace: Backtrace::capture(),
113        };
114        let error = AppendSnafu.into_error(append_error);
115
116        let append_source = error
117            .source()
118            .and_then(|source| source.downcast_ref::<AppendError>())
119            .expect("append source");
120        let arrow_source = append_source
121            .source()
122            .and_then(|source| source.downcast_ref::<ArrowError>())
123            .expect("Arrow source");
124        let append_backtrace = ErrorCompat::backtrace(append_source).expect("append backtrace");
125        let table_backtrace = ErrorCompat::backtrace(&error).expect("table backtrace");
126
127        assert!(
128            matches!(arrow_source, ArrowError::ComputeError(message) if message == "input failed")
129        );
130        assert!(std::ptr::eq(table_backtrace, append_backtrace));
131    }
132
133    #[test]
134    fn append_facade_preserves_parquet_source_and_backtrace() {
135        let error = AppendSnafu.into_error(AppendError::ParquetWrite {
136            source: ParquetError::General("write failed".to_string()),
137            backtrace: Backtrace::capture(),
138        });
139
140        let append_source = error
141            .source()
142            .and_then(|source| source.downcast_ref::<AppendError>())
143            .expect("append source");
144        let parquet_source = append_source
145            .source()
146            .and_then(|source| source.downcast_ref::<ParquetError>())
147            .expect("Parquet source");
148        let append_backtrace = ErrorCompat::backtrace(append_source).expect("append backtrace");
149        let table_backtrace = ErrorCompat::backtrace(&error).expect("table backtrace");
150
151        assert!(
152            matches!(parquet_source, ParquetError::General(message) if message == "write failed")
153        );
154        assert!(std::ptr::eq(table_backtrace, append_backtrace));
155    }
156
157    #[test]
158    fn append_schema_validation_leaf_does_not_manufacture_a_backtrace() {
159        let error = AppendError::from(SchemaCompatibilityError::MissingTableSchema);
160
161        assert!(error.to_string().starts_with("Schema validation failed:"));
162        assert!(ErrorCompat::backtrace(&error).is_none());
163    }
164
165    #[test]
166    fn schema_wrappers_delegate_the_originating_backtrace() {
167        let schema = SchemaCompatibilityError::RegisteredSchemaConversion {
168            source: Box::new(LogicalToArrowSchemaError::Int96Unsupported {
169                column: "time".to_string(),
170                backtrace: Backtrace::capture(),
171            }),
172        };
173        let error = AppendSnafu.into_error(AppendError::from(schema));
174
175        let append = error
176            .source()
177            .and_then(|source| source.downcast_ref::<AppendError>())
178            .expect("append source");
179        let schema = append
180            .source()
181            .and_then(|source| source.downcast_ref::<Box<SchemaCompatibilityError>>())
182            .map(Box::as_ref)
183            .expect("schema compatibility source");
184        let conversion = schema
185            .source()
186            .and_then(|source| source.downcast_ref::<Box<LogicalToArrowSchemaError>>())
187            .map(Box::as_ref)
188            .expect("logical-to-Arrow source");
189        let originating_backtrace = ErrorCompat::backtrace(conversion).expect("source backtrace");
190
191        assert!(std::ptr::eq(
192            ErrorCompat::backtrace(schema).expect("schema backtrace"),
193            originating_backtrace
194        ));
195        assert!(std::ptr::eq(
196            ErrorCompat::backtrace(append).expect("append backtrace"),
197            originating_backtrace
198        ));
199        assert!(std::ptr::eq(
200            ErrorCompat::backtrace(&error).expect("table backtrace"),
201            originating_backtrace
202        ));
203    }
204
205    #[test]
206    fn append_rollback_preserves_primary_chain_cleanup_errors_and_backtrace() {
207        let commit_error = CommitError::Conflict {
208            expected: 1,
209            found: 2,
210            backtrace: Backtrace::capture(),
211        };
212        let cleanup_error = StorageError::OtherIo {
213            path: "data/segment.parquet".to_string(),
214            source: io::Error::other("cleanup failed").into(),
215            backtrace: Backtrace::capture(),
216        };
217        let error = AppendSnafu.into_error(AppendError::Rollback {
218            source: Box::new(AppendError::Commit {
219                source: commit_error,
220            }),
221            cleanup_errors: vec![cleanup_error],
222        });
223
224        let rollback = error
225            .source()
226            .and_then(|source| source.downcast_ref::<AppendError>())
227            .expect("rollback source");
228        let cleanup_errors = match rollback {
229            AppendError::Rollback { cleanup_errors, .. } => cleanup_errors,
230            other => panic!("unexpected append error: {other:?}"),
231        };
232        let primary = rollback
233            .source()
234            .and_then(|source| source.downcast_ref::<Box<AppendError>>())
235            .map(Box::as_ref)
236            .expect("primary append source");
237        let commit = primary
238            .source()
239            .and_then(|source| source.downcast_ref::<CommitError>())
240            .expect("commit source");
241        let commit_backtrace = ErrorCompat::backtrace(commit).expect("commit backtrace");
242
243        assert!(matches!(
244            commit,
245            CommitError::Conflict {
246                expected: 1,
247                found: 2,
248                ..
249            }
250        ));
251        assert!(matches!(
252            cleanup_errors.as_slice(),
253            [StorageError::OtherIo { path, .. }] if path == "data/segment.parquet"
254        ));
255        let message = error.to_string();
256        assert!(message.contains("cleanup failed"));
257        assert!(!message.contains("Backtrace"));
258        assert!(std::ptr::eq(
259            ErrorCompat::backtrace(&error).expect("table backtrace"),
260            commit_backtrace
261        ));
262        assert!(std::ptr::eq(
263            ErrorCompat::backtrace(rollback).expect("rollback backtrace"),
264            commit_backtrace
265        ));
266        assert!(std::ptr::eq(
267            ErrorCompat::backtrace(primary).expect("primary backtrace"),
268            commit_backtrace
269        ));
270    }
271
272    #[test]
273    fn create_facade_preserves_index_source_and_operation_backtrace() {
274        let error = CreateSnafu.into_error(CreateTableError::IndexSpecValidation {
275            source: IndexSpecError::EmptyColumn,
276            backtrace: Backtrace::capture(),
277        });
278
279        let create = error
280            .source()
281            .and_then(|source| source.downcast_ref::<CreateTableError>())
282            .expect("create source");
283        let index = create
284            .source()
285            .and_then(|source| source.downcast_ref::<IndexSpecError>())
286            .expect("index specification source");
287
288        assert!(matches!(index, IndexSpecError::EmptyColumn));
289        assert!(std::ptr::eq(
290            ErrorCompat::backtrace(&error).expect("table backtrace"),
291            ErrorCompat::backtrace(create).expect("create backtrace")
292        ));
293    }
294
295    #[test]
296    fn open_facade_preserves_commit_source_and_backtrace() {
297        let error = OpenSnafu.into_error(OpenTableError::Commit {
298            source: CommitError::Conflict {
299                expected: 8,
300                found: 9,
301                backtrace: Backtrace::capture(),
302            },
303        });
304
305        let open = error
306            .source()
307            .and_then(|source| source.downcast_ref::<OpenTableError>())
308            .expect("open source");
309        let commit = open
310            .source()
311            .and_then(|source| source.downcast_ref::<CommitError>())
312            .expect("commit source");
313        let commit_backtrace = ErrorCompat::backtrace(commit).expect("commit backtrace");
314
315        assert!(matches!(commit, CommitError::Conflict { .. }));
316        assert!(std::ptr::eq(
317            ErrorCompat::backtrace(open).expect("open backtrace"),
318            commit_backtrace
319        ));
320        assert!(std::ptr::eq(
321            ErrorCompat::backtrace(&error).expect("table backtrace"),
322            commit_backtrace
323        ));
324    }
325
326    #[test]
327    fn state_access_facade_preserves_commit_source_and_backtrace() {
328        let error = StateAccessSnafu.into_error(TableStateAccessError::Commit {
329            source: CommitError::MissingTableMetadata {
330                current_version: 7,
331                backtrace: Backtrace::capture(),
332            },
333        });
334
335        let state_access = error
336            .source()
337            .and_then(|source| source.downcast_ref::<TableStateAccessError>())
338            .expect("state access source");
339        let commit = state_access
340            .source()
341            .and_then(|source| source.downcast_ref::<CommitError>())
342            .expect("commit source");
343        let commit_backtrace = ErrorCompat::backtrace(commit).expect("commit backtrace");
344
345        assert!(matches!(commit, CommitError::MissingTableMetadata { .. }));
346        assert!(std::ptr::eq(
347            ErrorCompat::backtrace(state_access).expect("state access backtrace"),
348            commit_backtrace
349        ));
350        assert!(std::ptr::eq(
351            ErrorCompat::backtrace(&error).expect("table backtrace"),
352            commit_backtrace
353        ));
354    }
355
356    #[test]
357    fn scan_facade_preserves_parquet_source_and_backtrace() {
358        let error = ScanSnafu.into_error(ScanError::Parquet {
359            path: "data/segment.parquet".to_string(),
360            operation: "reading metadata",
361            source: Box::new(ParquetError::General("corrupt footer".to_string())),
362            backtrace: Box::new(Backtrace::capture()),
363        });
364
365        let scan = error
366            .source()
367            .and_then(|source| source.downcast_ref::<ScanError>())
368            .expect("scan source");
369        let parquet = scan
370            .source()
371            .and_then(|source| source.downcast_ref::<Box<ParquetError>>())
372            .map(Box::as_ref)
373            .expect("Parquet source");
374        let scan_backtrace = ErrorCompat::backtrace(scan).expect("scan backtrace");
375
376        assert!(matches!(parquet, ParquetError::General(message) if message == "corrupt footer"));
377        assert!(std::ptr::eq(
378            ErrorCompat::backtrace(&error).expect("table backtrace"),
379            scan_backtrace
380        ));
381    }
382
383    #[test]
384    fn coverage_facade_preserves_codec_source_and_backtrace() {
385        let error = CoverageQuerySnafu.into_error(CoverageQueryError::CoverageSnapshotRead {
386            coverage_path: "coverage/table.coverage".to_string(),
387            source: Box::new(CoverageSidecarError::Codec {
388                source: CoverageCodecError::InvalidEntityCoverageMagic {
389                    backtrace: Backtrace::capture(),
390                },
391            }),
392        });
393
394        let coverage_query = error
395            .source()
396            .and_then(|source| source.downcast_ref::<CoverageQueryError>())
397            .expect("coverage query source");
398        let sidecar = coverage_query
399            .source()
400            .and_then(|source| source.downcast_ref::<Box<CoverageSidecarError>>())
401            .map(Box::as_ref)
402            .expect("coverage sidecar source");
403        let codec = sidecar
404            .source()
405            .and_then(|source| source.downcast_ref::<CoverageCodecError>())
406            .expect("coverage codec source");
407        let codec_backtrace = ErrorCompat::backtrace(codec).expect("codec backtrace");
408
409        assert!(matches!(
410            codec,
411            CoverageCodecError::InvalidEntityCoverageMagic { .. }
412        ));
413        assert!(std::ptr::eq(
414            ErrorCompat::backtrace(coverage_query).expect("coverage query backtrace"),
415            codec_backtrace
416        ));
417        assert!(std::ptr::eq(
418            ErrorCompat::backtrace(&error).expect("table backtrace"),
419            codec_backtrace
420        ));
421    }
422
423    #[test]
424    fn optimize_facade_preserves_commit_source_and_backtrace() {
425        let error = OptimizeSnafu.into_error(OptimizeError::Commit {
426            source: CommitError::Conflict {
427                expected: 3,
428                found: 4,
429                backtrace: Backtrace::capture(),
430            },
431        });
432
433        let optimize = error
434            .source()
435            .and_then(|source| source.downcast_ref::<OptimizeError>())
436            .expect("optimize source");
437        let commit = optimize
438            .source()
439            .and_then(|source| source.downcast_ref::<CommitError>())
440            .expect("commit source");
441        let commit_backtrace = ErrorCompat::backtrace(commit).expect("commit backtrace");
442
443        assert!(matches!(
444            commit,
445            CommitError::Conflict {
446                expected: 3,
447                found: 4,
448                ..
449            }
450        ));
451        assert!(std::ptr::eq(
452            ErrorCompat::backtrace(optimize).expect("optimize backtrace"),
453            commit_backtrace
454        ));
455        assert!(std::ptr::eq(
456            ErrorCompat::backtrace(&error).expect("table backtrace"),
457            commit_backtrace
458        ));
459    }
460
461    #[test]
462    fn optimize_facade_preserves_rewrite_storage_source_and_backtrace() {
463        let error = OptimizeSnafu.into_error(OptimizeError::from(EntityRewriteError::Storage {
464            source: StorageError::OtherIo {
465                path: "data/mixed.parquet".to_string(),
466                source: io::Error::other("read failed").into(),
467                backtrace: Backtrace::capture(),
468            },
469        }));
470
471        let optimize = error
472            .source()
473            .and_then(|source| source.downcast_ref::<OptimizeError>())
474            .expect("optimize source");
475        let rewrite = optimize
476            .source()
477            .and_then(|source| source.downcast_ref::<Box<EntityRewriteError>>())
478            .map(Box::as_ref)
479            .expect("rewrite source");
480        let storage = rewrite
481            .source()
482            .and_then(|source| source.downcast_ref::<StorageError>())
483            .expect("storage source");
484        let storage_backtrace = ErrorCompat::backtrace(storage).expect("storage backtrace");
485
486        assert!(matches!(
487            storage,
488            StorageError::OtherIo { path, .. } if path == "data/mixed.parquet"
489        ));
490        assert!(std::ptr::eq(
491            ErrorCompat::backtrace(rewrite).expect("rewrite backtrace"),
492            storage_backtrace
493        ));
494        assert!(std::ptr::eq(
495            ErrorCompat::backtrace(optimize).expect("optimize backtrace"),
496            storage_backtrace
497        ));
498        assert!(std::ptr::eq(
499            ErrorCompat::backtrace(&error).expect("table backtrace"),
500            storage_backtrace
501        ));
502    }
503
504    #[test]
505    fn optimize_rollback_preserves_primary_chain_and_typed_cleanup_errors() {
506        let error = OptimizeSnafu.into_error(OptimizeError::Rollback {
507            source: Box::new(OptimizeError::Commit {
508                source: CommitError::Conflict {
509                    expected: 5,
510                    found: 6,
511                    backtrace: Backtrace::capture(),
512                },
513            }),
514            cleanup_errors: vec![StorageError::OtherIo {
515                path: "data/_staged/segment.parquet".to_string(),
516                source: io::Error::other("cleanup failed").into(),
517                backtrace: Backtrace::capture(),
518            }],
519        });
520
521        let rollback = error
522            .source()
523            .and_then(|source| source.downcast_ref::<OptimizeError>())
524            .expect("optimize source");
525        let cleanup_errors = match rollback {
526            OptimizeError::Rollback { cleanup_errors, .. } => cleanup_errors,
527            other => panic!("unexpected optimize error: {other:?}"),
528        };
529        let primary = rollback
530            .source()
531            .and_then(|source| source.downcast_ref::<Box<OptimizeError>>())
532            .map(Box::as_ref)
533            .expect("primary optimize source");
534        let commit = primary
535            .source()
536            .and_then(|source| source.downcast_ref::<CommitError>())
537            .expect("commit source");
538        let commit_backtrace = ErrorCompat::backtrace(commit).expect("commit backtrace");
539
540        assert!(matches!(
541            cleanup_errors.as_slice(),
542            [StorageError::OtherIo { path, .. }] if path == "data/_staged/segment.parquet"
543        ));
544        assert!(std::ptr::eq(
545            ErrorCompat::backtrace(rollback).expect("rollback backtrace"),
546            commit_backtrace
547        ));
548        assert!(std::ptr::eq(
549            ErrorCompat::backtrace(&error).expect("table backtrace"),
550            commit_backtrace
551        ));
552    }
553}