timeseries_table_format/coverage/
io.rs1use std::path::Path;
22
23use snafu::{Backtrace, Snafu};
24
25use crate::{
26 coverage::layout::CoverageLayoutError,
27 coverage::{
28 Coverage, EntityCoverage,
29 serde::{CoverageCodecError, coverage_from_bytes, entity_coverage_from_bytes},
30 },
31 metadata::schema_compat::SchemaCompatibilityError,
32 storage::{self, StorageError, TableLocation},
33};
34
35#[cfg(test)]
36use crate::coverage::serde::coverage_to_bytes;
37
38#[derive(Debug, Snafu)]
44#[non_exhaustive]
45pub enum CoverageSidecarError {
46 #[snafu(context(false), display("Coverage sidecar layout error: {source}"))]
48 Layout {
49 source: CoverageLayoutError,
51 backtrace: Backtrace,
53 },
54
55 #[snafu(context(false), display("Coverage sidecar codec error: {source}"))]
57 Codec {
58 #[snafu(source, backtrace)]
60 source: CoverageCodecError,
61 },
62
63 #[snafu(
65 context(false),
66 display("Entity coverage identity does not match the table schema: {source}")
67 )]
68 EntityIdentitySchema {
69 #[snafu(source(from(SchemaCompatibilityError, Box::new)), backtrace)]
71 source: Box<SchemaCompatibilityError>,
72 },
73
74 #[snafu(
76 context(false),
77 display("Storage error while accessing coverage sidecar: {source}")
78 )]
79 Storage {
80 #[snafu(source, backtrace)]
82 source: StorageError,
83 },
84}
85
86impl CoverageSidecarError {
87 pub(crate) fn storage_cleanup_failed(&self) -> bool {
88 matches!(
89 self,
90 Self::Storage {
91 source: StorageError::CleanupFailed { .. }
92 }
93 )
94 }
95}
96
97#[cfg(test)]
121pub(crate) async fn write_coverage_sidecar_atomic(
122 location: &TableLocation,
123 rel_path: &Path,
124 cov: &Coverage,
125) -> Result<(), CoverageSidecarError> {
126 let bytes = coverage_to_bytes(cov)?;
127 storage::write_atomic(location.as_ref(), rel_path, &bytes).await?;
128 Ok(())
129}
130
131#[cfg(test)]
156async fn write_coverage_sidecar_new(
157 location: &TableLocation,
158 rel_path: &Path,
159 cov: &Coverage,
160) -> Result<(), CoverageSidecarError> {
161 let bytes = coverage_to_bytes(cov)?;
162 storage::write_new(location.as_ref(), rel_path, &bytes).await?;
163 Ok(())
164}
165
166pub async fn write_coverage_sidecar_new_bytes(
174 location: &TableLocation,
175 rel_path: &Path,
176 bytes: &[u8],
177) -> Result<(), CoverageSidecarError> {
178 storage::write_new(location.as_ref(), rel_path, bytes).await?;
179 Ok(())
180}
181
182pub async fn read_coverage_sidecar(
205 location: &TableLocation,
206 rel_path: &Path,
207) -> Result<Coverage, CoverageSidecarError> {
208 let bytes = storage::read_all_bytes(location.as_ref(), rel_path).await?;
209 Ok(coverage_from_bytes(&bytes)?)
210}
211
212pub async fn read_entity_coverage_sidecar(
218 location: &TableLocation,
219 rel_path: &Path,
220) -> Result<EntityCoverage, CoverageSidecarError> {
221 let bytes = storage::read_all_bytes(location.as_ref(), rel_path).await?;
222 Ok(entity_coverage_from_bytes(&bytes)?)
223}
224
225#[cfg(test)]
226mod tests {
227 use std::{error::Error as _, io};
228
229 use snafu::ErrorCompat;
230
231 use super::*;
232 use crate::{
233 coverage::{
234 EntityIdentity,
235 serde::{coverage_from_bytes, entity_coverage_to_bytes},
236 },
237 storage::{StorageBackendError, StorageLocation},
238 };
239 use tempfile::TempDir;
240
241 fn temp_location() -> (TempDir, TableLocation) {
242 let tmp = TempDir::new().expect("tempdir");
243 let loc = TableLocation::local(tmp.path());
244 (tmp, loc)
245 }
246
247 fn storage_source(error: &CoverageSidecarError) -> &StorageError {
248 error
249 .source()
250 .and_then(|source| source.downcast_ref::<StorageError>())
251 .expect("storage source")
252 }
253
254 fn filesystem_source(error: &StorageError) -> &io::Error {
255 error
256 .source()
257 .and_then(|source| source.downcast_ref::<StorageBackendError>())
258 .and_then(|source| source.source())
259 .and_then(|source| source.downcast_ref::<io::Error>())
260 .expect("filesystem source")
261 }
262
263 #[tokio::test]
264 async fn write_atomic_overwrites_existing() {
265 let (_tmp, loc) = temp_location();
266 let rel = Path::new("_coverage/table/1.roar");
267
268 let cov1 = Coverage::from_iter(vec![1u64, 2, 3]);
269 write_coverage_sidecar_atomic(&loc, rel, &cov1)
270 .await
271 .expect("first write");
272
273 let cov2 = Coverage::from_iter(vec![10u64, 11]);
275 write_coverage_sidecar_atomic(&loc, rel, &cov2)
276 .await
277 .expect("overwrite");
278
279 let abs = match &loc.as_ref() {
281 StorageLocation::Local(root) => root.join(rel),
282 };
283 let bytes = std::fs::read(abs).expect("read file");
284 let restored = coverage_from_bytes(&bytes).expect("deserialize");
285 assert_eq!(cov2.present(), restored.present());
286 }
287
288 #[tokio::test]
289 async fn write_new_fails_if_exists() {
290 let (_tmp, loc) = temp_location();
291 let rel = Path::new("_coverage/segments/seg-1.roar");
292
293 let cov = Coverage::from_iter(vec![5u64]);
294 write_coverage_sidecar_new(&loc, rel, &cov)
295 .await
296 .expect("first write");
297
298 let err = write_coverage_sidecar_new(&loc, rel, &cov)
299 .await
300 .expect_err("second write should fail");
301
302 match err {
303 CoverageSidecarError::Storage {
304 source: StorageError::AlreadyExists { .. },
305 ..
306 } => {}
307 _ => panic!("expected AlreadyExists storage error"),
308 }
309 }
310
311 #[tokio::test]
312 async fn read_sidecar_round_trip() {
313 let (_tmp, loc) = temp_location();
314 let rel = Path::new("_coverage/table/2.roar");
315
316 let cov = Coverage::from_iter(vec![1u64, 3, 5, 7]);
317 write_coverage_sidecar_atomic(&loc, rel, &cov)
318 .await
319 .expect("write sidecar");
320
321 let restored = read_coverage_sidecar(&loc, rel)
322 .await
323 .expect("read sidecar");
324 assert_eq!(cov.present(), restored.present());
325 }
326
327 #[tokio::test]
328 async fn read_entity_sidecar_round_trip() {
329 let (_tmp, loc) = temp_location();
330 let rel = Path::new("_coverage/table/entity.roar");
331 let mut coverage = EntityCoverage::empty();
332 coverage.union_coverage(
333 EntityIdentity::try_new(vec!["A".into()]).unwrap(),
334 Coverage::from_iter([1, 2]),
335 );
336 let bytes = entity_coverage_to_bytes(&coverage).unwrap();
337 write_coverage_sidecar_new_bytes(&loc, rel, &bytes)
338 .await
339 .expect("write entity sidecar");
340
341 assert_eq!(
342 read_entity_coverage_sidecar(&loc, rel)
343 .await
344 .expect("read entity sidecar"),
345 coverage
346 );
347 }
348
349 #[tokio::test]
350 async fn read_sidecar_missing_preserves_storage_not_found() {
351 let (_tmp, loc) = temp_location();
352 let rel = Path::new("_coverage/table/missing.roar");
353
354 let err = read_coverage_sidecar(&loc, rel)
355 .await
356 .expect_err("should be missing");
357
358 let sidecar_backtrace = ErrorCompat::backtrace(&err).expect("sidecar backtrace");
359 let storage = storage_source(&err);
360 let storage_backtrace = ErrorCompat::backtrace(storage).expect("storage backtrace");
361
362 assert!(matches!(
363 storage,
364 StorageError::NotFound { path, .. } if path.contains("missing.roar")
365 ));
366 assert_eq!(filesystem_source(storage).kind(), io::ErrorKind::NotFound);
367 assert!(std::ptr::eq(sidecar_backtrace, storage_backtrace));
368 }
369
370 #[tokio::test]
371 async fn read_sidecar_corrupt_bytes_returns_codec_error() {
372 let (tmp, loc) = temp_location();
373 let rel = Path::new("_coverage/table/corrupt.roar");
374
375 let abs = match &loc.as_ref() {
377 StorageLocation::Local(root) => root.join(rel),
378 };
379 std::fs::create_dir_all(abs.parent().unwrap()).expect("create dirs");
380 std::fs::write(&abs, b"not a bitmap").expect("write corrupt");
381
382 let err = read_coverage_sidecar(&loc, rel)
383 .await
384 .expect_err("should fail to deserialize");
385
386 let sidecar_backtrace = ErrorCompat::backtrace(&err).expect("sidecar backtrace");
387 let codec = err
388 .source()
389 .and_then(|source| source.downcast_ref::<CoverageCodecError>())
390 .expect("codec source");
391 let codec_backtrace = ErrorCompat::backtrace(codec).expect("codec backtrace");
392
393 assert!(matches!(
394 codec,
395 CoverageCodecError::BitmapDeserialization { .. }
396 ));
397 assert!(codec.source().is_some());
398 assert!(std::ptr::eq(sidecar_backtrace, codec_backtrace));
399
400 drop(tmp); }
402
403 #[cfg(unix)]
404 #[tokio::test]
405 async fn read_sidecar_permission_denied_remains_a_storage_error() {
406 use std::os::unix::fs::PermissionsExt;
407
408 let (_tmp, loc) = temp_location();
409 let rel = Path::new("_coverage/table/denied.roar");
410 let absolute = match loc.as_ref() {
411 StorageLocation::Local(root) => root.join(rel),
412 };
413 std::fs::create_dir_all(absolute.parent().unwrap()).expect("create dirs");
414 std::fs::write(&absolute, coverage_to_bytes(&Coverage::empty()).unwrap())
415 .expect("write sidecar");
416 let original_permissions = std::fs::metadata(&absolute).unwrap().permissions();
417 let mut denied_permissions = original_permissions.clone();
418 denied_permissions.set_mode(0o0);
419 std::fs::set_permissions(&absolute, denied_permissions).expect("deny reads");
420
421 let error = read_coverage_sidecar(&loc, rel)
422 .await
423 .expect_err("read must be denied");
424 std::fs::set_permissions(&absolute, original_permissions).expect("restore permissions");
425
426 let storage = storage_source(&error);
427 assert!(matches!(storage, StorageError::OtherIo { .. }));
428 assert_eq!(
429 filesystem_source(storage).kind(),
430 io::ErrorKind::PermissionDenied
431 );
432 }
433}