1use std::sync::Arc;
9
10use bytes::Bytes;
11use indexmap::IndexMap;
12use object_store::{ObjectStore, ObjectStoreExt};
13
14use crate::{
15 DType, Error, Result,
16 address::ChunkAddress,
17 array::ArrayElement,
18 codec::CompressionCodec,
19 delta::{
20 Delta, DeltaAllocator, DeltaCache, DeltaImmutable, DeltaMutable, write_file_then_bytes,
21 },
22 footer::{FOOTER_VERSION, Footer},
23 layout::{
24 ArrayLayout, ArrayMeta, AttrIndexKind, AttributeValue, Attributes, ChunkEntry, FillValue,
25 StorageLayout,
26 },
27 stats::{ArrayStats, StatsFile, compute_chunk_partial, merge_partial, read_stats_file},
28 storage::{ObjectStoreBackend, Storage},
29};
30
31pub const DEFAULT_BLOCK_TARGET_SIZE: usize = 8 * 1024 * 1024; pub const DEFAULT_CACHE_CAPACITY: usize = 256 * 1024 * 1024; pub const DEFAULT_IO_CACHE_CAPACITY: usize = 64 * 1024 * 1024; pub struct FileConfig<C: CompressionCodec> {
56 pub codec: C,
58 pub block_target_size: usize,
60 pub cache_capacity: usize,
64 pub io_cache_capacity: usize,
68 pub cache: Option<Arc<DeltaCache>>,
75}
76
77impl<C: CompressionCodec> FileConfig<C> {
78 pub fn new(codec: C) -> Self {
81 Self {
82 codec,
83 block_target_size: DEFAULT_BLOCK_TARGET_SIZE,
84 cache_capacity: DEFAULT_CACHE_CAPACITY,
85 io_cache_capacity: DEFAULT_IO_CACHE_CAPACITY,
86 cache: None,
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
97pub struct MergedArrayMeta {
98 pub name: String,
100 pub dtype: DType,
102 pub shape: Vec<u32>,
104 pub chunk_shape: Vec<u32>,
106 pub dimension_names: Vec<String>,
108 pub fill_value: Option<FillValue>,
110}
111
112struct StoreDir {
116 store: Arc<dyn ObjectStore>,
117 base_path: object_store::path::Path,
118}
119
120pub(crate) struct ChunkedSchema {
122 pub full_shape: Vec<u32>,
123 pub chunk_shape: Vec<u32>,
124 pub dtype: DType,
125 pub all_coords: Vec<Vec<u32>>,
126}
127
128pub struct ArrayFile {
135 deltas: Vec<Delta<DeltaImmutable>>,
136 pending: Option<Delta<DeltaMutable>>,
137 codec: Arc<dyn CompressionCodec>,
138 block_target_size: usize,
139 cache: Option<Arc<DeltaCache>>,
140 store_dir: StoreDir,
143 stats: Option<StatsFile>,
145}
146
147impl ArrayFile {
150 pub async fn create<C: CompressionCodec + 'static>(
157 store: Arc<dyn ObjectStore>,
158 path: object_store::path::Path,
159 config: FileConfig<C>,
160 ) -> Result<Self> {
161 let cache = resolve_cache(&config);
162 let delta_path = Arc::<str>::from(path.as_ref());
163 let storage =
164 Arc::new(ObjectStoreBackend::new(Arc::clone(&store), path.clone())) as Arc<dyn Storage>;
165 write_empty_base(&*storage).await?;
166 let base_delta = Delta::<DeltaImmutable>::open(storage, delta_path, cache.clone()).await?;
167 Ok(ArrayFile {
168 deltas: vec![base_delta],
169 pending: None,
170 codec: Arc::new(config.codec),
171 block_target_size: config.block_target_size,
172 cache,
173 store_dir: StoreDir {
174 store,
175 base_path: path,
176 },
177 stats: None,
178 })
179 }
180
181 pub async fn open<C: CompressionCodec + 'static>(
188 store: Arc<dyn ObjectStore>,
189 path: object_store::path::Path,
190 config: FileConfig<C>,
191 ) -> Result<Self> {
192 let cache = resolve_cache(&config);
193 let delta_path = Arc::<str>::from(path.as_ref());
194 let storage =
195 Arc::new(ObjectStoreBackend::new(Arc::clone(&store), path.clone())) as Arc<dyn Storage>;
196 let base_delta = Delta::<DeltaImmutable>::open(storage, delta_path, cache.clone()).await?;
197 let mut deltas = vec![base_delta];
198
199 let sidecars = discover_sidecars_store(&*store, &path).await?;
200 for (_, scar_path) in sidecars {
201 let scar_delta_path = Arc::<str>::from(scar_path.as_ref());
202 let scar_storage = Arc::new(ObjectStoreBackend::new(Arc::clone(&store), scar_path))
203 as Arc<dyn Storage>;
204 deltas.push(
205 Delta::<DeltaImmutable>::open(scar_storage, scar_delta_path, cache.clone()).await?,
206 );
207 }
208
209 let stats = {
210 let s_storage = ObjectStoreBackend::new(Arc::clone(&store), stats_path(&path));
211 read_stats_file(&s_storage).await.ok()
212 };
213
214 Ok(ArrayFile {
215 deltas,
216 pending: None,
217 codec: Arc::new(config.codec),
218 block_target_size: config.block_target_size,
219 cache,
220 store_dir: StoreDir {
221 store,
222 base_path: path,
223 },
224 stats,
225 })
226 }
227
228 pub async fn create_memory<C: CompressionCodec + 'static>(
234 config: FileConfig<C>,
235 ) -> Result<Self> {
236 let store = Arc::new(object_store::memory::InMemory::new());
237 Self::create(store, object_store::path::Path::from("memory.af"), config).await
238 }
239}
240
241impl ArrayFile {
244 pub fn get_array(&self, name: &str) -> Result<&ArrayMeta> {
247 self.resolve_array_meta(name)
248 .ok_or_else(|| Error::ArrayNotFound {
249 name: name.to_string(),
250 })
251 }
252
253 fn resolve_array_meta(&self, name: &str) -> Option<&ArrayMeta> {
254 if let Some(p) = self.pending.as_ref()
255 && let Some(m) = p.inner.array_meta.get(name)
256 {
257 return if m.deleted { None } else { Some(m) };
258 }
259 for delta in self.deltas.iter().rev() {
260 if let Some(&i) = delta.inner.array_index.get(name) {
261 let m = &delta.inner.footer.arrays[i];
262 return if m.deleted { None } else { Some(m) };
263 }
264 }
265 None
266 }
267
268 fn pending_mut(&mut self) -> &mut Delta<DeltaMutable> {
270 if self.pending.is_none() {
271 let overlay_index = self.deltas.len() as u32;
272 self.pending = Some(Delta::<DeltaMutable>::new(
273 Arc::clone(&self.codec),
274 self.block_target_size,
275 overlay_index,
276 ));
277 }
278 self.pending.as_mut().unwrap()
279 }
280
281 pub(crate) fn get_chunked_schema(&self, name: &str) -> Result<ChunkedSchema> {
283 let meta = self.get_array(name)?;
284 let full_shape = meta.layout.shape.clone();
285 let chunk_shape = meta.layout.storage.chunk_shape.clone();
286 let dtype = meta.dtype.clone();
287 let mut existing: IndexMap<Vec<u32>, ()> = IndexMap::new();
289 for delta in &self.deltas {
290 if let Some(&i) = delta.inner.array_index.get(name) {
291 for e in &delta.inner.footer.arrays[i].layout.storage.chunks {
292 existing.entry(e.coord.clone()).or_default();
293 }
294 }
295 }
296 if let Some(p) = self.pending.as_ref()
297 && let Some(m) = p.inner.array_meta.get(name)
298 {
299 for e in &m.layout.storage.chunks {
300 existing.entry(e.coord.clone()).or_default();
301 }
302 }
303 Ok(ChunkedSchema {
304 full_shape,
305 chunk_shape,
306 dtype,
307 all_coords: existing.into_keys().collect(),
308 })
309 }
310
311 pub fn list_arrays(&self) -> Vec<MergedArrayMeta> {
313 let mut seen: IndexMap<String, MergedArrayMeta> = IndexMap::new();
314
315 for delta in &self.deltas {
317 for a in &delta.inner.footer.arrays {
318 if a.deleted {
319 seen.shift_remove(&a.name);
320 } else {
321 seen.insert(
322 a.name.clone(),
323 MergedArrayMeta {
324 name: a.name.clone(),
325 dtype: a.dtype.clone(),
326 shape: a.layout.shape.clone(),
327 chunk_shape: a.layout.storage.chunk_shape.clone(),
328 dimension_names: a.layout.dimension_names.clone(),
329 fill_value: a.fill_value.clone(),
330 },
331 );
332 }
333 }
334 }
335 if let Some(p) = self.pending.as_ref() {
336 for (name, a) in &p.inner.array_meta {
337 if a.deleted {
338 seen.shift_remove(name);
339 } else {
340 seen.insert(
341 name.clone(),
342 MergedArrayMeta {
343 name: a.name.clone(),
344 dtype: a.dtype.clone(),
345 shape: a.layout.shape.clone(),
346 chunk_shape: a.layout.storage.chunk_shape.clone(),
347 dimension_names: a.layout.dimension_names.clone(),
348 fill_value: a.fill_value.clone(),
349 },
350 );
351 }
352 }
353 }
354 seen.into_values().collect()
355 }
356
357 pub fn array_stats(&self, name: &str) -> Option<&ArrayStats> {
359 self.stats.as_ref()?.get_array(name)
360 }
361
362 pub fn num_layers(&self) -> usize {
364 self.deltas.len()
365 }
366
367 pub fn get_attribute(&self, name: &str, key: &str) -> Result<Option<&AttributeValue>> {
370 let meta = self.get_array(name)?;
371 let key_idx = match self
372 .pending
373 .as_ref()
374 .and_then(|p| p.inner.attr_keys.iter().position(|k| k == key))
375 .or_else(|| {
376 self.deltas
378 .iter()
379 .rev()
380 .find_map(|d| d.inner.footer.attr_keys.iter().position(|k| k == key))
381 }) {
382 Some(i) => i,
383 None => return Ok(None),
384 };
385 let val_idx = match meta.attributes.get(key_idx) {
386 Some(i) => i,
387 None => return Ok(None),
388 };
389 if let Some(p) = self.pending.as_ref()
391 && val_idx < p.inner.attr_values.len()
392 {
393 return Ok(Some(&p.inner.attr_values[val_idx]));
394 }
395 for delta in self.deltas.iter().rev() {
396 if val_idx < delta.inner.footer.attr_values.len() {
397 return Ok(Some(&delta.inner.footer.attr_values[val_idx]));
398 }
399 }
400 Ok(None)
401 }
402
403 pub fn attribute_index(&self, key: &str) -> Vec<(String, Option<&AttributeValue>)> {
411 self.list_arrays()
412 .into_iter()
413 .map(|m| {
414 let value = self.get_attribute(&m.name, key).ok().flatten();
415 (m.name, value)
416 })
417 .collect()
418 }
419
420 pub fn set_attribute(&mut self, name: &str, key: &str, value: AttributeValue) -> Result<()> {
425 let mut existing_meta = self.get_array(name)?.clone();
430 existing_meta.layout.storage.chunks.clear();
431
432 let pending = self.pending_mut();
433 let key_idx = pending.intern_attr_key(key);
434 let val_idx = pending.intern_attr_value(value);
435
436 if pending.array_meta_mut(name).is_none() {
438 pending.upsert_array_meta(existing_meta);
439 }
440 let meta = pending.array_meta_mut(name).unwrap();
441 meta.attributes.upsert(key_idx, val_idx);
442 Ok(())
443 }
444}
445
446impl ArrayFile {
449 pub fn define_array<T: ArrayElement>(
461 &mut self,
462 name: impl Into<String>,
463 dimension_names: Vec<String>,
464 shape: Vec<usize>,
465 chunk_shape: Option<Vec<usize>>,
466 fill_value: Option<FillValue>,
467 ) -> Result<()> {
468 let name = name.into();
469 if self.resolve_array_meta(&name).is_some() {
470 return Err(Error::ArrayAlreadyExists { name });
471 }
472 let shape_u32: Vec<u32> = shape.iter().map(|&s| s as u32).collect();
473 let ndim = shape_u32.len();
474 let chunk_shape_u32: Vec<u32> = chunk_shape
475 .map(|cs| cs.iter().map(|&s| s as u32).collect())
476 .unwrap_or_else(|| shape_u32.clone());
477 let dim_names = if dimension_names.len() == ndim {
478 dimension_names
479 } else {
480 (0..ndim).map(|i| format!("dim{i}")).collect()
481 };
482 let layout = ArrayLayout {
483 shape: shape_u32,
484 dimension_names: dim_names,
485 storage: StorageLayout {
486 chunk_shape: chunk_shape_u32,
487 chunks: vec![],
488 },
489 };
490 self.pending_mut().upsert_array_meta(ArrayMeta {
491 name,
492 dtype: T::DTYPE,
493 layout,
494 fill_value,
495 deleted: false,
496 attributes: Attributes::empty(AttrIndexKind::U16),
497 });
498 Ok(())
499 }
500
501 pub fn delete(&mut self, name: &str) -> Result<()> {
507 let meta = self.get_array(name)?.clone();
508 self.pending_mut().mark_deleted(meta);
509 Ok(())
510 }
511}
512
513impl ArrayFile {
516 pub(crate) async fn read_chunk<T: ArrayElement>(
517 &self,
518 name: &str,
519 coord: &[u32],
520 ) -> Result<Vec<T>> {
521 if let Some(bytes) = self.resolve_raw_chunk(name, coord).await? {
522 return Ok(T::decode_chunk(&bytes));
523 }
524 let meta = self.get_array(name)?;
525 let chunk_elems: usize = meta
526 .layout
527 .storage
528 .chunk_shape
529 .iter()
530 .enumerate()
531 .map(|(i, &cs)| {
532 let axis_len = meta.layout.shape[i] as usize;
533 let start = coord[i] as usize * cs as usize;
534 (cs as usize).min(axis_len.saturating_sub(start))
535 })
536 .product();
537 Ok(vec![T::fill_element(meta.fill_value.as_ref()); chunk_elems])
538 }
539
540 pub(crate) fn write_chunk_raw(
541 &mut self,
542 name: &str,
543 coord: Vec<u32>,
544 bytes: Vec<u8>,
545 ) -> Result<()> {
546 let snapshot = if self
551 .pending
552 .as_ref()
553 .and_then(|p| p.inner.array_meta.get(name))
554 .is_none()
555 {
556 let mut m = self.get_array(name)?.clone();
557 m.layout.storage.chunks.clear();
558 Some(m)
559 } else {
560 None
561 };
562 let pending = self.pending_mut();
563 if let Some(meta) = snapshot {
564 pending.upsert_array_meta(meta);
565 }
566 pending.write_raw_chunk(name, coord, &bytes)
567 }
568
569 async fn resolve_raw_chunk(&self, name: &str, coord: &[u32]) -> Result<Option<Bytes>> {
570 if let Some(p) = self.pending.as_ref()
571 && let Some(bytes) = p.read_raw_chunk(name, coord)
572 {
573 return Ok(Some(bytes));
574 }
575 for delta in self.deltas.iter().rev() {
576 if let Some(bytes) = delta.read_raw_chunk(name, coord).await? {
577 return Ok(Some(bytes));
578 }
579 }
580 Ok(None)
581 }
582}
583
584impl ArrayFile {
587 pub async fn write_array<T: ArrayElement>(
595 &mut self,
596 name: &str,
597 start: Vec<usize>,
598 data: ndarray::ArrayView<'_, T, ndarray::IxDyn>,
599 ) -> Result<()> {
600 crate::ndarray_ext::write_nd(self, name, data, &start).await
601 }
602
603 pub async fn read_array<T: ArrayElement>(
610 &self,
611 name: &str,
612 start: Vec<usize>,
613 shape: Vec<usize>,
614 ) -> Result<ndarray::ArcArray<T, ndarray::IxDyn>> {
615 use std::ops::Range;
616 let slice: Option<Vec<Range<usize>>> = if start.is_empty() && shape.is_empty() {
617 None
618 } else {
619 let meta = self.get_array(name)?;
620 let ndim = meta.layout.shape.len();
621 let effective_start = if start.len() == ndim {
622 start.clone()
623 } else {
624 vec![0; ndim]
625 };
626 let effective_shape: Vec<usize> = if shape.len() == ndim {
627 shape.clone()
628 } else {
629 meta.layout.shape.iter().map(|&s| s as usize).collect()
630 };
631 Some(
632 effective_start
633 .iter()
634 .zip(&effective_shape)
635 .map(|(&s, &sz)| s..s + sz)
636 .collect(),
637 )
638 };
639 crate::ndarray_ext::assemble_nd(self, name, slice.as_deref()).await
640 }
641}
642
643impl ArrayFile {
646 pub async fn flush(&mut self) -> Result<()> {
651 if self.pending.is_none() {
652 return Ok(());
653 }
654 let store = Arc::clone(&self.store_dir.store);
655 let base_path = self.store_dir.base_path.clone();
656 let overlay_index = self.deltas.len() as u32;
657 let scar_path = sidecar_path(&base_path, overlay_index);
658 let delta_path = Arc::<str>::from(scar_path.as_ref());
659 let storage =
660 Arc::new(ObjectStoreBackend::new(Arc::clone(&store), scar_path)) as Arc<dyn Storage>;
661 let hint = base_path.as_ref().to_string();
662 let dirty_names = self.commit_pending(storage, delta_path, hint).await?;
663
664 let merged = self.compute_stats_for(&dirty_names).await?;
665 let s_storage = ObjectStoreBackend::new(Arc::clone(&store), stats_path(&base_path));
666 s_storage
667 .write(bytes::Bytes::from(merged.serialize()?))
668 .await?;
669 self.stats = Some(merged);
670 Ok(())
671 }
672
673 async fn compute_stats_for(&self, dirty_names: &[String]) -> Result<StatsFile> {
674 let mut merged = self.stats.clone().unwrap_or_default();
675 for name in dirty_names {
676 let schema = match self.get_chunked_schema(name) {
677 Ok(s) => s,
678 Err(_) => continue,
679 };
680 let fill_value = self
681 .resolve_array_meta(name)
682 .and_then(|m| m.fill_value.clone());
683 let shape_product: u64 = schema.full_shape.iter().map(|&s| s as u64).product();
684 let mut stats = ArrayStats::new(name.clone());
685 let mut written_non_null: u64 = 0;
686 for coord in &schema.all_coords {
687 if let Some(bytes) = self.resolve_raw_chunk(name, coord).await? {
688 let (min, max, nc, rc) =
689 compute_chunk_partial(&bytes, &schema.dtype, fill_value.as_ref());
690 written_non_null += rc - nc;
691 merge_partial(&mut stats, min, max, nc, rc);
692 }
693 }
694 stats.row_count = shape_product;
695 stats.null_count = shape_product - written_non_null;
696 merged.upsert(stats);
697 }
698 Ok(merged)
699 }
700
701 async fn commit_pending(
705 &mut self,
706 storage: Arc<dyn Storage>,
707 delta_path: Arc<str>,
708 base_file_hint: String,
709 ) -> Result<Vec<String>> {
710 let mutable = self
711 .pending
712 .take()
713 .expect("commit_pending: no pending delta");
714 let dirty_names: Vec<String> = mutable
715 .inner
716 .array_meta
717 .iter()
718 .filter(|(_, m)| !m.layout.storage.chunks.is_empty())
719 .map(|(name, _)| name.clone())
720 .collect();
721 let immutable = mutable
722 .commit(storage, delta_path, self.cache.clone(), base_file_hint)
723 .await?;
724 self.deltas.push(immutable);
725 Ok(dirty_names)
726 }
727}
728
729impl ArrayFile {
732 pub async fn compact(&mut self) -> Result<()> {
738 let merged_names: Vec<String> = self.list_arrays().into_iter().map(|m| m.name).collect();
740
741 let mut allocator = DeltaAllocator::new(Arc::clone(&self.codec), self.block_target_size);
743 let mut arrays: Vec<ArrayMeta> = Vec::new();
744 let mut per_array_stats: Vec<ArrayStats> = Vec::new();
745
746 for name in &merged_names {
747 let meta = self
748 .resolve_array_meta(name)
749 .ok_or_else(|| Error::ArrayNotFound { name: name.clone() })?
750 .clone();
751
752 let mut all_coords: indexmap::IndexSet<Vec<u32>> = indexmap::IndexSet::new();
754 for delta in &self.deltas {
755 if let Some(&i) = delta.inner.array_index.get(name.as_str()) {
756 for e in &delta.inner.footer.arrays[i].layout.storage.chunks {
757 all_coords.insert(e.coord.clone());
758 }
759 }
760 }
761
762 let shape_product: u64 = meta.layout.shape.iter().map(|&s| s as u64).product();
763 let mut new_chunks: Vec<ChunkEntry> = Vec::new();
764 let mut array_stats = ArrayStats::new(name.clone());
765 let mut written_non_null: u64 = 0;
766 for coord in &all_coords {
767 if let Some(raw) = self.resolve_raw_chunk(name, coord).await? {
769 let (min, max, nc, rc) =
770 compute_chunk_partial(&raw, &meta.dtype, meta.fill_value.as_ref());
771 written_non_null += rc - nc;
772 merge_partial(&mut array_stats, min, max, nc, rc);
773 let alloc = allocator.allocate(&raw);
774 new_chunks.push(ChunkEntry {
775 coord: coord.clone(),
776 address: ChunkAddress::from(alloc),
777 });
778 }
779 }
780 array_stats.row_count = shape_product;
781 array_stats.null_count = shape_product - written_non_null;
782 per_array_stats.push(array_stats);
783
784 let mut new_meta = meta;
785 new_meta.layout.storage.chunks = new_chunks;
786 arrays.push(new_meta);
787 }
788
789 let crate::delta::AllocatorOutput {
790 mut file,
791 output_size,
792 blocks,
793 } = allocator.commit().await;
794
795 let mut attr_keys: Vec<String> = Vec::new();
797 let mut attr_values: Vec<crate::layout::AttributeValue> = Vec::new();
798 for delta in &self.deltas {
799 for k in &delta.inner.footer.attr_keys {
800 if !attr_keys.contains(k) {
801 attr_keys.push(k.clone());
802 }
803 }
804 for v in &delta.inner.footer.attr_values {
805 if !attr_values.contains(v) {
806 attr_values.push(v.clone());
807 }
808 }
809 }
810
811 let footer = Footer {
812 version: FOOTER_VERSION,
813 blocks,
814 arrays,
815 attr_keys,
816 attr_values,
817 overlay_index: 0,
818 base_file_hint: String::new(),
819 };
820 let footer_bytes = footer.serialize()?;
821
822 let sd = &self.store_dir;
824 for i in 1..self.deltas.len() {
826 let _ = sd
827 .store
828 .delete(&sidecar_path(&sd.base_path, i as u32))
829 .await;
830 }
831 let base_storage: Arc<dyn Storage> = Arc::new(ObjectStoreBackend::new(
832 Arc::clone(&sd.store),
833 sd.base_path.clone(),
834 ));
835
836 write_file_then_bytes(&mut file, output_size, &footer_bytes, &*base_storage).await?;
837 let base_delta_path: Arc<str> = Arc::from(sd.base_path.as_ref());
838 let new_base =
839 Delta::<DeltaImmutable>::open(base_storage, base_delta_path, self.cache.clone())
840 .await?;
841 self.deltas = vec![new_base];
842
843 let mut new_stats = StatsFile::default();
844 for s in per_array_stats {
845 new_stats.upsert(s);
846 }
847 let s_storage = ObjectStoreBackend::new(
848 Arc::clone(&self.store_dir.store),
849 stats_path(&self.store_dir.base_path),
850 );
851 s_storage
852 .write(bytes::Bytes::from(new_stats.serialize()?))
853 .await?;
854 self.stats = Some(new_stats);
855 Ok(())
856 }
857}
858
859fn resolve_cache<C: CompressionCodec>(config: &FileConfig<C>) -> Option<Arc<DeltaCache>> {
862 if let Some(c) = &config.cache {
863 Some(Arc::clone(c))
864 } else if config.cache_capacity == 0 && config.io_cache_capacity == 0 {
865 None
866 } else {
867 Some(Arc::new(DeltaCache::new(
868 config.cache_capacity as u64,
869 config.io_cache_capacity as u64,
870 )))
871 }
872}
873
874fn sidecar_path(base: &object_store::path::Path, n: u32) -> object_store::path::Path {
875 let s = base.as_ref();
876 let without_af = s.strip_suffix(".af").unwrap_or(s);
877 object_store::path::Path::from(format!("{without_af}.{n}.af").as_str())
878}
879
880fn stats_path(base: &object_store::path::Path) -> object_store::path::Path {
881 let s = base.as_ref();
882 let without_af = s.strip_suffix(".af").unwrap_or(s);
883 object_store::path::Path::from(format!("{without_af}.stats").as_str())
884}
885
886async fn discover_sidecars_store(
887 store: &dyn ObjectStore,
888 base_path: &object_store::path::Path,
889) -> Result<Vec<(u32, object_store::path::Path)>> {
890 use futures::TryStreamExt;
891 let base_str = base_path.as_ref();
892 let stem_prefix = base_str
893 .strip_suffix(".af")
894 .ok_or_else(|| Error::Storage("path must end with .af".into()))?;
895 let list_prefix = base_str
896 .rfind('/')
897 .map(|pos| object_store::path::Path::from(&base_str[..pos]));
898 let objects: Vec<_> = store
899 .list(list_prefix.as_ref())
900 .try_collect()
901 .await
902 .map_err(|e| Error::Storage(e.to_string()))?;
903 let mut sidecars: Vec<(u32, object_store::path::Path)> = objects
904 .into_iter()
905 .filter_map(|meta| {
906 let s = meta.location.as_ref();
907 let rest = s.strip_prefix(stem_prefix)?.strip_prefix('.')?;
908 let (num_str, ext) = rest.rsplit_once('.')?;
909 if ext != "af" {
910 return None;
911 }
912 let n: u32 = num_str.parse().ok()?;
913 if n == 0 {
914 return None;
915 }
916 Some((n, meta.location))
917 })
918 .collect();
919 sidecars.sort_by_key(|(n, _)| *n);
920 Ok(sidecars)
921}
922
923async fn write_empty_base(storage: &dyn Storage) -> Result<()> {
924 let footer = Footer::new();
925 let bytes = footer.serialize()?;
926 storage.write(Bytes::from(bytes)).await
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use crate::codec::NoCompression;
933
934 #[tokio::test]
935 async fn shared_cache_is_reused_across_files() {
936 let shared = Arc::new(DeltaCache::new(1024 * 1024, 0));
937
938 let mut cfg_a = FileConfig::new(NoCompression);
939 cfg_a.cache = Some(Arc::clone(&shared));
940 let file_a = ArrayFile::create_memory(cfg_a).await.unwrap();
941
942 let mut cfg_b = FileConfig::new(NoCompression);
943 cfg_b.cache = Some(Arc::clone(&shared));
944 let file_b = ArrayFile::create_memory(cfg_b).await.unwrap();
945
946 let a = file_a.cache.as_ref().expect("file_a has cache");
947 let b = file_b.cache.as_ref().expect("file_b has cache");
948 assert!(Arc::ptr_eq(a, &shared));
949 assert!(Arc::ptr_eq(b, &shared));
950 }
951
952 fn find<'a>(
954 index: &'a [(String, Option<&AttributeValue>)],
955 name: &str,
956 ) -> Option<&'a Option<&'a AttributeValue>> {
957 index.iter().find(|(n, _)| n == name).map(|(_, v)| v)
958 }
959
960 #[tokio::test]
961 async fn attribute_index_returns_full_column() {
962 let mut file = ArrayFile::create_memory(FileConfig::new(NoCompression))
963 .await
964 .unwrap();
965
966 file.define_array::<f32>("a", vec!["x".into()], vec![4], None, None)
967 .unwrap();
968 file.define_array::<f32>("b", vec!["x".into()], vec![4], None, None)
969 .unwrap();
970 file.define_array::<f32>("c", vec!["x".into()], vec![4], None, None)
971 .unwrap();
972
973 file.set_attribute("a", "units", AttributeValue::String("hPa".into()))
974 .unwrap();
975 file.set_attribute("b", "units", AttributeValue::String("Pa".into()))
976 .unwrap();
977 let index = file.attribute_index("units");
980 assert_eq!(index.len(), 3, "every visible array appears once");
981 assert_eq!(
982 find(&index, "a"),
983 Some(&Some(&AttributeValue::String("hPa".into())))
984 );
985 assert_eq!(
986 find(&index, "b"),
987 Some(&Some(&AttributeValue::String("Pa".into())))
988 );
989 assert_eq!(find(&index, "c"), Some(&None), "absent attribute -> None");
990 }
991
992 #[tokio::test]
993 async fn attribute_index_unknown_key_is_all_none() {
994 let mut file = ArrayFile::create_memory(FileConfig::new(NoCompression))
995 .await
996 .unwrap();
997 file.define_array::<f32>("a", vec!["x".into()], vec![4], None, None)
998 .unwrap();
999 file.set_attribute("a", "units", AttributeValue::String("hPa".into()))
1000 .unwrap();
1001
1002 let index = file.attribute_index("nonexistent");
1003 assert_eq!(index.len(), 1);
1004 assert_eq!(find(&index, "a"), Some(&None));
1005 }
1006
1007 #[tokio::test]
1008 async fn attribute_index_omits_deleted_arrays() {
1009 let mut file = ArrayFile::create_memory(FileConfig::new(NoCompression))
1010 .await
1011 .unwrap();
1012 file.define_array::<f32>("a", vec!["x".into()], vec![4], None, None)
1013 .unwrap();
1014 file.define_array::<f32>("b", vec!["x".into()], vec![4], None, None)
1015 .unwrap();
1016 file.set_attribute("a", "units", AttributeValue::String("hPa".into()))
1017 .unwrap();
1018 file.set_attribute("b", "units", AttributeValue::String("Pa".into()))
1019 .unwrap();
1020
1021 file.delete("b").unwrap();
1022
1023 let index = file.attribute_index("units");
1024 assert_eq!(index.len(), 1, "deleted array dropped from column");
1025 assert_eq!(find(&index, "b"), None);
1026 assert_eq!(
1027 find(&index, "a"),
1028 Some(&Some(&AttributeValue::String("hPa".into())))
1029 );
1030 }
1031
1032 #[tokio::test]
1033 async fn attribute_index_survives_flush() {
1034 let mut file = ArrayFile::create_memory(FileConfig::new(NoCompression))
1035 .await
1036 .unwrap();
1037 file.define_array::<f32>("a", vec!["x".into()], vec![4], None, None)
1038 .unwrap();
1039 file.set_attribute("a", "units", AttributeValue::String("hPa".into()))
1040 .unwrap();
1041
1042 file.flush().await.unwrap();
1045
1046 let index = file.attribute_index("units");
1047 assert_eq!(
1048 find(&index, "a"),
1049 Some(&Some(&AttributeValue::String("hPa".into())))
1050 );
1051 }
1052}