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}