Skip to main content

timeseries_table_format/table/
error.rs

1//! Error types and SNAFU context selectors for the table layer.
2//!
3//! This module centralizes the `TableError` enum used by the public API and
4//! exposes context selectors (via `#[snafu(visibility(pub(crate)))]`) so
5//! implementation details in sibling modules can attach error context without
6//! re-exporting everything at the crate root. Keep new variants here to ensure
7//! consistent user-facing messages and to avoid scattering selectors.
8
9use arrow::{datatypes::DataType, error::ArrowError};
10use chrono::{DateTime, Utc};
11use parquet::errors::ParquetError;
12use snafu::prelude::*;
13
14use crate::{
15    coverage::{
16        EntityIdentity,
17        bucket::{BucketError, LogicalBucketRange},
18        io::CoverageError,
19    },
20    formats::parquet::{EntityRewriteError, SegmentCoverageError},
21    metadata::{
22        schema_compat::SchemaCompatibilityError,
23        table_metadata::{IndexKind, IndexSpecError, IndexValueError},
24    },
25    storage::StorageError,
26    transaction_log::{CommitError, TableKind, segments::SegmentError},
27};
28
29/// Errors from high-level time-series table operations.
30///
31/// Each variant carries enough context for callers to surface actionable
32/// messages to users or implement retries where appropriate (for example,
33/// conflicts on optimistic concurrency control).
34#[derive(Debug, Snafu)]
35#[snafu(visibility(pub(crate)))]
36pub enum TableError {
37    /// Any error coming from the transaction log / commit machinery
38    /// (for example, OCC conflicts, storage failures, or corrupt commits).
39    #[snafu(display("Transaction log error: {source}"))]
40    TransactionLog {
41        /// Underlying transaction log / commit error.
42        #[snafu(source, backtrace)]
43        source: CommitError,
44    },
45
46    /// Entity-layout optimization does not apply to a table without entities.
47    #[snafu(display(
48        "Entity-layout optimization is not applicable to table {table_root}: no entity columns are configured"
49    ))]
50    OptimizeNotApplicable {
51        /// User-facing table root.
52        table_root: String,
53    },
54
55    /// Rewriting one mixed source into staged replacements failed.
56    #[snafu(display("Entity-layout optimization rewrite failed: {source}"))]
57    OptimizeRewrite {
58        /// Storage-level mixed segment rewrite failure.
59        #[snafu(source)]
60        source: EntityRewriteError,
61    },
62
63    /// A staged optimization plan violated an atomic publication invariant.
64    #[snafu(display("Invalid entity-layout optimization plan: {reason}"))]
65    OptimizeInvariant {
66        /// Failed plan invariant.
67        reason: String,
68    },
69
70    /// An optimization count could not be represented without wrapping.
71    #[snafu(display("Entity-layout optimization count overflow: {field}"))]
72    OptimizeCountOverflow {
73        /// Report or version field that overflowed.
74        field: &'static str,
75    },
76
77    /// Optimization failed and one or more owned staged objects could not be removed.
78    #[snafu(display(
79        "Entity-layout optimization failed: {source}; staged-object cleanup also failed: {cleanup_errors:?}"
80    ))]
81    OptimizeRollback {
82        /// Original optimization failure.
83        #[snafu(source)]
84        source: Box<TableError>,
85        /// Every private path whose cleanup failed.
86        cleanup_errors: Vec<String>,
87    },
88
89    /// Append failed and one or more owned coverage sidecars could not be removed.
90    #[snafu(display(
91        "Append failed: {source}; coverage sidecar rollback also failed: {cleanup_errors:?}"
92    ))]
93    AppendRollback {
94        /// Original append failure that triggered rollback.
95        #[snafu(source)]
96        source: Box<TableError>,
97        /// Cleanup failures, including each affected sidecar path.
98        cleanup_errors: Vec<String>,
99    },
100
101    /// Append failed and its provisional external Parquet copy could not be removed.
102    #[snafu(display(
103        "Append failed: {source}; failed to remove provisional Parquet copy at {path}: {cleanup_error}"
104    ))]
105    ExternalParquetRollback {
106        /// Table-relative path of the provisional copy that may remain.
107        path: String,
108        /// Original append failure that triggered rollback.
109        #[snafu(source)]
110        source: Box<TableError>,
111        /// Storage failure encountered while removing the provisional copy.
112        cleanup_error: StorageError,
113    },
114
115    /// Attempting to open a table that has no commits at all (CURRENT == 0).
116    #[snafu(display("Cannot open table with no commits (CURRENT version is 0)"))]
117    EmptyTable,
118
119    /// The underlying table is not a time-series table (TableKind mismatch).
120    #[snafu(display("Table kind is {kind:?}, expected TableKind::TimeSeries"))]
121    NotTimeSeries {
122        /// The actual kind of the underlying table that was discovered.
123        kind: TableKind,
124    },
125
126    /// Attempting to create a table with an unsupported metadata format version.
127    #[snafu(display("Unsupported table format version: expected {expected}, found {found}"))]
128    UnsupportedFormatVersion {
129        /// Format version supported by this writer.
130        expected: u32,
131        /// Format version supplied by the caller.
132        found: u32,
133    },
134
135    /// Attempt to create a table where commits already exist (idempotency guard for create).
136    #[snafu(display("Table already exists; current transaction log version is {current_version}"))]
137    AlreadyExists {
138        /// Current transaction log version that indicates the table already exists.
139        current_version: u64,
140    },
141
142    /// The ordered-index specification is structurally invalid.
143    #[snafu(display("Invalid ordered index specification: {source}"))]
144    IndexSpec {
145        /// Structural or bucket configuration failure.
146        source: IndexSpecError,
147    },
148
149    /// An ordered value could not be mapped to its coverage bucket.
150    #[snafu(display("Coverage bucket mapping failed: {source}"))]
151    CoverageBucket {
152        /// Domain, range, or bucket configuration failure.
153        source: BucketError,
154    },
155
156    /// Segment bounds cannot be ordered in one native index domain.
157    #[snafu(display("Invalid segment ordered-index bounds: {source}"))]
158    InvalidSegmentBounds {
159        /// Domain or bound-order failure.
160        source: IndexValueError,
161    },
162
163    /// Segment-level metadata / Parquet error during append (for example, missing time column, unsupported type, corrupt stats).
164    #[snafu(display("Segment metadata error while appending: {source}"))]
165    SegmentMeta {
166        /// Underlying segment metadata error.
167        #[snafu(source, backtrace)]
168        source: SegmentError,
169    },
170
171    /// Schema compatibility error when appending a segment with incompatible schema (no evolution allowed in v0.1).
172    #[snafu(display("Schema compatibility error: {source}"))]
173    SchemaCompatibility {
174        /// Underlying schema compatibility error.
175        #[snafu(source)]
176        source: SchemaCompatibilityError,
177    },
178
179    /// A segment's schema is incompatible with the table or index specification.
180    #[snafu(display("Schema compatibility error for segment {path}: {source}"))]
181    SegmentSchemaCompatibility {
182        /// Table-relative segment path.
183        path: String,
184        /// Underlying schema compatibility error.
185        #[snafu(source)]
186        source: SchemaCompatibilityError,
187    },
188
189    /// Table has progressed past the initial metadata commit but still lacks
190    /// a canonical logical schema (invariant violation for v0.1).
191    #[snafu(display("Table has no logical_schema at version {version}; cannot append in v0.1"))]
192    MissingCanonicalSchema {
193        /// The transaction log version missing a canonical logical schema.
194        version: u64,
195    },
196
197    /// Storage error while accessing table data (read/write failure at the storage layer).
198    #[snafu(display("Storage error while accessing table data: {source}"))]
199    Storage {
200        /// Underlying storage error while reading or writing table data.
201        source: StorageError,
202    },
203
204    /// Ordered-index range validation failed.
205    #[snafu(display("Ordered-index range validation failed: {source}"))]
206    InvalidRange {
207        /// Domain, kind, or bound-order failure.
208        source: IndexValueError,
209    },
210
211    /// An identity-free coverage query was used on an entity-aware table.
212    #[snafu(display(
213        "Entity identity is required for coverage queries; configured entity columns: {entity_columns:?}"
214    ))]
215    EntityIdentityRequired {
216        /// Entity columns that require values from the caller.
217        entity_columns: Vec<String>,
218    },
219
220    /// An entity-aware coverage query was used on a table with global coverage.
221    #[snafu(display("Table has no configured entity columns"))]
222    EntityIdentityNotConfigured,
223
224    /// A required entity column has no caller-provided value.
225    #[snafu(display("Missing entity identity component for column {column}"))]
226    MissingEntityIdentityColumn {
227        /// Configured entity column missing from the caller input.
228        column: String,
229    },
230
231    /// Caller input repeats one entity column.
232    #[snafu(display("Duplicate entity identity component for column {column}"))]
233    DuplicateEntityIdentityColumn {
234        /// Repeated entity column name.
235        column: String,
236    },
237
238    /// Caller input contains a column that is not part of the entity identity.
239    #[snafu(display("Unexpected entity identity component for column {column}"))]
240    UnexpectedEntityIdentityColumn {
241        /// Unknown entity column name.
242        column: String,
243    },
244
245    /// Parquet read/IO error during scanning or schema extraction.
246    #[snafu(display("Parquet read error for segment {path}: {source}"))]
247    ParquetRead {
248        /// Normalized table-relative path of the segment being scanned.
249        path: String,
250        /// Underlying Parquet error raised during read or schema extraction.
251        source: ParquetError,
252    },
253
254    /// Arrow compute or conversion error while materializing or filtering batches.
255    #[snafu(display("Arrow error while filtering column {column} in segment {path}: {source}"))]
256    Arrow {
257        /// Normalized table-relative path of the segment being scanned.
258        path: String,
259        /// Configured time column being filtered.
260        column: String,
261        /// Underlying Arrow error raised during batch conversion or filtering.
262        source: ArrowError,
263    },
264
265    /// Segment is missing the configured ordered-index column required for scans.
266    #[snafu(display("Missing ordered-index column {column} in segment {path}"))]
267    MissingIndexColumn {
268        /// Normalized table-relative path of the segment being scanned.
269        path: String,
270        /// Name of the expected ordered-index column that was not found.
271        column: String,
272    },
273
274    /// Ordered-index column has an Arrow type that disagrees with the table index.
275    #[snafu(display(
276        "Ordered-index column {column} in segment {path} has Arrow type {datatype:?}, expected {expected}"
277    ))]
278    IndexColumnTypeMismatch {
279        /// Normalized table-relative path of the segment being scanned.
280        path: String,
281        /// Name of the ordered-index column with the mismatched type.
282        column: String,
283        /// Registered ordered-index domain.
284        expected: &'static str,
285        /// Arrow data type encountered for the ordered-index column.
286        datatype: DataType,
287    },
288
289    /// Converting a timestamp to the requested unit would overflow `i64`.
290    #[snafu(display(
291        "Timestamp conversion overflow for column {column} in segment {path} (value: {timestamp})"
292    ))]
293    TimeConversionOverflow {
294        /// Normalized table-relative path of the segment being scanned.
295        path: String,
296        /// Name of the time column being converted.
297        column: String,
298        /// The timestamp value that could not be represented as i64 nanos.
299        timestamp: DateTime<Utc>,
300    },
301
302    /// Segment Coverage error.
303    #[snafu(display("Segment coverage error: {source}"))]
304    SegmentCoverage {
305        /// Underlying coverage error.
306        #[snafu(source, backtrace)]
307        source: SegmentCoverageError,
308    },
309
310    /// Table coverage pointer uses a different ordered-index descriptor.
311    #[snafu(display(
312        "Table coverage index kind mismatch: expected {expected:?}, found {actual:?} (from coverage version {pointer_version})"
313    ))]
314    TableCoverageIndexKindMismatch {
315        /// Index descriptor defined by table metadata.
316        expected: IndexKind,
317        /// Index descriptor recorded in the table coverage pointer.
318        actual: IndexKind,
319        /// Log version where the mismatching coverage pointer was recorded.
320        pointer_version: u64,
321    },
322
323    /// Coverage sidecar read/write or computation error.
324    #[snafu(display("Coverage sidecar error: {source}"))]
325    CoverageSidecar {
326        /// Underlying Coverage error.
327        #[snafu(source, backtrace)]
328        source: CoverageError,
329    },
330
331    /// Appending would overlap existing table coverage.
332    #[snafu(display(
333        "Coverage overlap while appending {segment_path}: {overlap_count} overlapping buckets (example_bucket_range={example_bucket_range})"
334    ))]
335    CoverageOverlap {
336        /// Relative path of the segment being appended.
337        segment_path: String,
338        /// Number of overlapping buckets detected.
339        overlap_count: u64,
340        /// Internal example bucket retained for programmatic compatibility.
341        example_bucket: Option<u64>,
342        /// Logical ordered-index range covered by the example bucket.
343        example_bucket_range: LogicalBucketRange,
344    },
345
346    /// Appending would overlap entity-scoped table coverage.
347    #[snafu(display(
348        "Entity coverage overlap while appending {segment_path}: {overlap_count} overlapping identity/bucket pairs (example_identity={example_identity:?}, example_bucket_range={example_bucket_range})"
349    ))]
350    EntityCoverageOverlap {
351        /// Relative path of the segment being appended.
352        segment_path: String,
353        /// Number of overlapping `(entity identity, bucket)` pairs.
354        overlap_count: u128,
355        /// First overlapping identity in canonical order.
356        example_identity: EntityIdentity,
357        /// Smallest overlapping bucket for `example_identity`.
358        example_bucket: u64,
359        /// Logical ordered-index range covered by the example bucket.
360        example_bucket_range: LogicalBucketRange,
361    },
362
363    /// Entity-aware append produced no entity coverage.
364    #[snafu(display("No entity coverage derived while appending segment {segment_path}"))]
365    EmptySegmentEntityCoverage {
366        /// Relative path of the segment being appended.
367        segment_path: String,
368    },
369
370    /// One entity has rows but no usable ordered-index coverage.
371    #[snafu(display(
372        "Entity {identity:?} in segment {segment_path} has no non-null ordered-index values"
373    ))]
374    EntityWithoutIndexCoverage {
375        /// Relative path of the segment being appended.
376        segment_path: String,
377        /// Complete identity whose rows all have null ordered-index values.
378        identity: EntityIdentity,
379    },
380
381    /// A live segment already uses the normalized path supplied for append.
382    #[snafu(display("Segment path is already live: {path}"))]
383    DuplicateSegmentPath {
384        /// Canonical table-relative path that is already registered.
385        path: String,
386    },
387
388    /// Existing segment lacks a coverage_path when coverage is required.
389    #[snafu(display(
390        "Cannot append because existing segment {path} is missing coverage_path (required for coverage tracking)"
391    ))]
392    ExistingSegmentMissingCoverage {
393        /// Canonical segment path missing a coverage_path entry.
394        path: String,
395    },
396
397    /// Reading the per-segment coverage sidecar failed while rebuilding coverage.
398    #[snafu(display(
399        "Cannot recover table coverage: failed to read segment coverage sidecar for {path} at {coverage_path}: {source}"
400    ))]
401    SegmentCoverageSidecarRead {
402        /// Canonical path of the segment whose coverage sidecar could not be read.
403        path: String,
404        /// Path to the coverage sidecar file that failed to read.
405        coverage_path: String,
406        /// Underlying coverage error (boxed to keep the variant size small).
407        #[snafu(source(from(CoverageError, Box::new)), backtrace)]
408        source: Box<CoverageError>,
409    },
410
411    /// Table state is missing a coverage snapshot pointer when required.
412    #[snafu(display(
413        "Cannot append because table has segments but no table coverage snapshot pointer in state"
414    ))]
415    MissingTableCoveragePointer,
416}