1use std::io;
5use std::io::Write;
6use std::sync::Arc;
7use std::sync::atomic::AtomicU64;
8use std::sync::atomic::Ordering;
9
10use futures::FutureExt;
11use futures::StreamExt;
12use futures::TryStreamExt;
13use futures::future::Fuse;
14use futures::future::LocalBoxFuture;
15use futures::future::ready;
16use futures::pin_mut;
17use futures::select;
18use itertools::Itertools;
19use vortex_array::ArrayContext;
20use vortex_array::ArrayId;
21use vortex_array::ArrayRef;
22use vortex_array::dtype::DType;
23use vortex_array::dtype::FieldPath;
24use vortex_array::expr::stats::Stat;
25use vortex_array::iter::ArrayIterator;
26use vortex_array::iter::ArrayIteratorExt;
27use vortex_array::session::ArraySessionExt;
28use vortex_array::stats::PRUNING_STATS;
29use vortex_array::stream::ArrayStream;
30use vortex_array::stream::ArrayStreamAdapter;
31use vortex_array::stream::ArrayStreamExt;
32use vortex_array::stream::SendableArrayStream;
33use vortex_btrblocks::BtrBlocksCompressorBuilder;
34use vortex_buffer::ByteBuffer;
35use vortex_edition::ComponentKind;
36use vortex_edition::EditionSessionExt;
37use vortex_error::VortexError;
38use vortex_error::VortexExpect;
39use vortex_error::VortexResult;
40use vortex_error::vortex_bail;
41use vortex_error::vortex_err;
42use vortex_io::IoBuf;
43use vortex_io::VortexWrite;
44use vortex_io::kanal_ext::KanalExt;
45use vortex_io::runtime::BlockingRuntime;
46use vortex_io::session::RuntimeSessionExt;
47use vortex_layout::BufferedBytesTracker;
48use vortex_layout::LayoutContext;
49use vortex_layout::LayoutStrategy;
50use vortex_layout::LayoutWriterContext;
51use vortex_layout::layouts::file_stats::accumulate_stats;
52use vortex_layout::sequence::SequenceId;
53use vortex_layout::sequence::SequentialStreamAdapter;
54use vortex_layout::sequence::SequentialStreamExt;
55use vortex_session::SessionExt;
56use vortex_session::VortexSession;
57use vortex_session::registry::Id;
58use vortex_session::registry::ReadContext;
59use vortex_utils::aliases::hash_map::HashMap;
60use vortex_utils::aliases::hash_set::HashSet;
61
62use crate::Footer;
63use crate::MAGIC_BYTES;
64use crate::WriteStrategyBuilder;
65use crate::counting::CountingVortexWrite;
66use crate::footer::FileStatistics;
67use crate::footer::MAX_METADATA_KEY_BYTES;
68use crate::footer::MAX_METADATA_SEGMENTS;
69use crate::segments::writer::BufferedSegmentSink;
70
71pub struct VortexWriteOptions {
81 session: VortexSession,
82 strategy: Option<Arc<dyn LayoutStrategy>>,
83 disable_editions: bool,
84 buffered_bytes: BufferedBytesTracker,
85 exclude_dtype: bool,
86 max_variable_length_statistics_size: usize,
87 file_statistics: Vec<Stat>,
88 metadata: HashMap<String, ByteBuffer>,
89}
90
91pub trait WriteOptionsSessionExt: SessionExt {
93 fn write_options(&self) -> VortexWriteOptions {
95 VortexWriteOptions::new(self.session())
96 }
97}
98impl<S: SessionExt> WriteOptionsSessionExt for S {}
99
100impl VortexWriteOptions {
101 pub fn new(session: VortexSession) -> Self {
103 VortexWriteOptions {
104 strategy: None,
105 disable_editions: false,
106 buffered_bytes: BufferedBytesTracker::new(),
107 session,
108 exclude_dtype: false,
109 file_statistics: PRUNING_STATS.to_vec(),
110 max_variable_length_statistics_size: 64,
111 metadata: HashMap::default(),
112 }
113 }
114
115 pub fn with_strategy(mut self, strategy: Arc<dyn LayoutStrategy>) -> Self {
124 self.strategy = Some(strategy);
125 self
126 }
127
128 pub fn disable_editions(mut self) -> Self {
135 self.disable_editions = true;
136 self
137 }
138
139 pub fn buffered_bytes_tracker(&self) -> BufferedBytesTracker {
146 self.buffered_bytes.clone()
147 }
148
149 pub fn exclude_dtype(mut self) -> Self {
153 self.exclude_dtype = true;
154 self
155 }
156
157 pub fn with_file_statistics(mut self, file_statistics: Vec<Stat>) -> Self {
161 self.file_statistics = file_statistics;
162 self
163 }
164
165 pub fn with_metadata_segment(
168 mut self,
169 key: impl Into<String>,
170 metadata: impl Into<ByteBuffer>,
171 ) -> Self {
172 let key = key.into();
173 let metadata = metadata.into();
174 self.metadata.insert(key, metadata);
175 self
176 }
177
178 pub fn with_metadata_segments<I, K, B>(mut self, metadata: I) -> Self
182 where
183 I: IntoIterator<Item = (K, B)>,
184 K: Into<String>,
185 B: Into<ByteBuffer>,
186 {
187 for (key, metadata) in metadata {
188 self = self.with_metadata_segment(key, metadata);
189 }
190 self
191 }
192
193 pub fn validate_metadata(&self) -> VortexResult<()> {
199 validate_metadata_segments(&self.metadata)
200 }
201}
202
203impl VortexWriteOptions {
204 pub fn blocking<B: BlockingRuntime>(self, runtime: &B) -> BlockingWrite<'_, B> {
209 BlockingWrite {
210 options: self,
211 runtime,
212 }
213 }
214
215 pub async fn write<W: VortexWrite + Unpin, S: ArrayStream + Send + 'static>(
224 self,
225 write: W,
226 stream: S,
227 ) -> VortexResult<WriteSummary> {
228 self.write_internal(write, ArrayStreamExt::boxed(stream))
229 .await
230 }
231
232 async fn write_internal<W: VortexWrite + Unpin>(
233 self,
234 mut write: W,
235 stream: SendableArrayStream,
236 ) -> VortexResult<WriteSummary> {
237 validate_metadata_segments(&self.metadata)?;
238
239 let enforce_editions = !self.disable_editions;
240 let (array_ctx, allowed_array_encodings) =
243 new_array_context(&self.session, enforce_editions);
244 let ctx = LayoutWriterContext::new(array_ctx)
245 .with_buffered_bytes_tracker(self.buffered_bytes.clone());
246 let ctx = if enforce_editions {
247 ctx.with_allowed_aggregates(edition_filter(&self.session, ComponentKind::Aggregate))
248 } else {
249 ctx
250 };
251 let strategy = match self.strategy {
252 Some(strategy) => strategy,
253 None => WriteStrategyBuilder::default()
254 .with_btrblocks_builder(
255 BtrBlocksCompressorBuilder::default()
256 .retain_allowed_encodings(&allowed_array_encodings),
257 )
258 .build(),
259 };
260 let dtype = stream.dtype().clone();
261 if enforce_editions {
262 validate_dtype_editions(&self.session, &dtype)?;
263 }
264
265 let (mut ptr, eof) = SequenceId::root().split();
266
267 let stream = SequentialStreamAdapter::new(
268 dtype.clone(),
269 stream
270 .try_filter(|chunk| ready(!chunk.is_empty()))
271 .map(move |result| result.map(|chunk| (ptr.advance(), chunk))),
272 )
273 .sendable();
274 let (file_stats, stream) = accumulate_stats(
275 stream,
276 self.file_statistics.clone().into(),
277 self.max_variable_length_statistics_size,
278 &self.session,
279 );
280
281 write.write_all(ByteBuffer::copy_from(MAGIC_BYTES)).await?;
283 let mut position = MAGIC_BYTES.len() as u64;
284
285 let (send, recv) = kanal::bounded_async(1);
287
288 let segments = Arc::new(BufferedSegmentSink::new(send, position));
289
290 let ctx2 = ctx.clone();
293 let session = self.session.clone();
294 let layout_fut = self.session.handle().spawn_nested(move |_| async move {
295 let layout = strategy
296 .write_stream(
297 ctx2,
298 Arc::<BufferedSegmentSink>::clone(&segments),
299 stream,
300 eof,
301 &session,
302 )
303 .await?;
304 Ok::<_, VortexError>((layout, segments.segment_specs()))
305 });
306
307 let recv_stream = recv.into_stream();
309 pin_mut!(recv_stream);
310 while let Some(buffer) = recv_stream.next().await {
311 if buffer.is_empty() {
312 continue;
313 }
314 position += buffer.len() as u64;
315 write.write_all(buffer).await?;
316 }
317
318 let (layout, segment_specs) = layout_fut.await?;
319
320 let statistics = if self.file_statistics.is_empty() {
322 None
323 } else {
324 Some(FileStatistics::new_with_dtype(
325 file_stats.stats_sets().into(),
326 &dtype,
327 ))
328 };
329 let mut footer = Footer::new(
330 Arc::clone(&layout),
331 segment_specs,
332 statistics,
333 ReadContext::new(ctx.array_ctx().to_ids()),
334 );
335
336 let (footer_buffers, metadata, approx_byte_size) = footer
338 .clone()
339 .into_serializer()
340 .with_layout_context(new_layout_context(&self.session, enforce_editions))
341 .with_metadata_segments(self.metadata)
342 .with_offset(position)
343 .with_exclude_dtype(self.exclude_dtype)
344 .serialize_with_metadata()?;
345 footer = footer
346 .with_metadata_segments(metadata)
347 .with_approx_byte_size(approx_byte_size);
348
349 for buffer in footer_buffers {
350 position += buffer.len() as u64;
351 write.write_all(buffer).await?;
352 }
353
354 write.flush().await?;
355
356 Ok(WriteSummary {
357 footer,
358 size: position,
359 })
360 }
361
362 pub fn writer<'w, W: VortexWrite + Unpin + 'w>(self, write: W, dtype: DType) -> Writer<'w> {
367 let (arrays_send, arrays_recv) = kanal::bounded_async(1);
369
370 let arrays =
371 ArrayStreamExt::boxed(ArrayStreamAdapter::new(dtype, arrays_recv.into_stream()));
372
373 let write = CountingVortexWrite::new(write);
374 let bytes_written = write.counter();
375 let buffered_bytes = self.buffered_bytes.clone();
376 let future = self.write(write, arrays).boxed_local().fuse();
377
378 Writer {
379 arrays: Some(arrays_send),
380 future,
381 bytes_written,
382 buffered_bytes,
383 }
384 }
385}
386
387fn new_array_context(
388 session: &VortexSession,
389 enforce_editions: bool,
390) -> (ArrayContext, HashSet<ArrayId>) {
391 let arrays = session.arrays();
397 let serialized_ids = if enforce_editions {
398 session.enabled_component_ids(ComponentKind::Array)
399 } else {
400 arrays
401 .registry()
402 .read(|registry| registry.keys().copied().collect())
403 };
404 let allowed_array_encodings = serialized_ids
405 .iter()
406 .filter_map(|serialized_id| arrays.registry().get(serialized_id))
407 .map(|plugin| plugin.id())
408 .collect();
409 let array_ctx = ArrayContext::new(serialized_ids.iter().copied().sorted().collect());
410 let array_ctx = if enforce_editions {
411 array_ctx.with_allowed_ids(serialized_ids.into_iter().collect())
413 } else {
414 array_ctx
415 };
416 (array_ctx, allowed_array_encodings)
417}
418
419fn edition_filter(session: &VortexSession, kind: ComponentKind) -> HashSet<Id> {
421 session.enabled_component_ids(kind).into_iter().collect()
422}
423
424fn validate_dtype_editions(session: &VortexSession, dtype: &DType) -> VortexResult<()> {
426 let allowed = edition_filter(session, ComponentKind::DType);
427
428 fn validate(dtype: &DType, allowed: &HashSet<Id>) -> VortexResult<()> {
429 match dtype {
430 DType::List(element, _) | DType::FixedSizeList(element, ..) => {
431 validate(element, allowed)
432 }
433 DType::Map(map, _) => {
434 validate(&map.key_dtype(), allowed)?;
435 validate(&map.value_dtype(), allowed)
436 }
437 DType::Struct(fields, _) => {
438 for field in fields.fields() {
439 validate(&field, allowed)?;
440 }
441 Ok(())
442 }
443 DType::Union(variants, _) => {
444 for variant in variants.variants() {
445 validate(&variant, allowed)?;
446 }
447 Ok(())
448 }
449 DType::Extension(extension) => {
450 if !allowed.contains(&extension.id()) {
451 vortex_bail!(
452 "Extension DType {} not permitted by enabled editions",
453 extension.id()
454 );
455 }
456 validate(extension.storage_dtype(), allowed)
457 }
458 DType::Null
459 | DType::Bool(_)
460 | DType::Primitive(..)
461 | DType::Decimal(..)
462 | DType::Utf8(_)
463 | DType::Binary(_)
464 | DType::Variant(_) => Ok(()),
465 }
466 }
467
468 validate(dtype, &allowed)
469}
470
471fn new_layout_context(session: &VortexSession, enforce_editions: bool) -> LayoutContext {
473 let context = LayoutContext::default();
474 if enforce_editions {
475 context.with_allowed_ids(edition_filter(session, ComponentKind::Layout))
476 } else {
477 context
478 }
479}
480
481fn validate_metadata_segments(metadata: &HashMap<String, ByteBuffer>) -> VortexResult<()> {
482 if metadata.len() > MAX_METADATA_SEGMENTS {
483 vortex_bail!(
484 "Vortex files may contain at most {} metadata segments; got {} metadata segments. Metadata keys must be non-empty and at most {} bytes",
485 MAX_METADATA_SEGMENTS,
486 metadata.len(),
487 MAX_METADATA_KEY_BYTES
488 );
489 }
490
491 for key in metadata.keys() {
492 if key.is_empty() {
493 vortex_bail!(
494 "Vortex metadata keys must be non-empty and at most {} bytes; files may contain at most {} metadata segments",
495 MAX_METADATA_KEY_BYTES,
496 MAX_METADATA_SEGMENTS
497 );
498 }
499
500 let key_bytes = key.len();
501 if key_bytes > MAX_METADATA_KEY_BYTES {
502 vortex_bail!(
503 "Vortex metadata key {key:?} is {key_bytes} bytes, but keys must be at most {} bytes; files may contain at most {} metadata segments",
504 MAX_METADATA_KEY_BYTES,
505 MAX_METADATA_SEGMENTS
506 );
507 }
508 }
509
510 Ok(())
511}
512
513pub struct Writer<'w> {
515 arrays: Option<kanal::AsyncSender<VortexResult<ArrayRef>>>,
517 future: Fuse<LocalBoxFuture<'w, VortexResult<WriteSummary>>>,
519 bytes_written: Arc<AtomicU64>,
521 buffered_bytes: BufferedBytesTracker,
523}
524
525impl Writer<'_> {
526 pub async fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
528 let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
529 let send_fut = async move { arrays.send(Ok(chunk)).await }.fuse();
530 pin_mut!(send_fut);
531
532 select! {
535 result = send_fut => {
536 if result.is_err() {
538 return Err(self.handle_failed_task().await);
539 }
540 },
541 result = &mut self.future => {
542 match result {
546 Ok(_) => vortex_bail!("Internal error: writer future completed early"),
547 Err(e) => return Err(e),
548 }
549 }
550 }
551
552 Ok(())
553 }
554
555 pub async fn push_stream(&mut self, mut stream: SendableArrayStream) -> VortexResult<()> {
560 let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
561 let stream_fut = async move {
562 while let Some(chunk) = stream.next().await {
563 arrays.send(chunk).await?;
564 }
565 Ok::<_, kanal::SendError>(())
566 }
567 .fuse();
568 pin_mut!(stream_fut);
569
570 select! {
573 result = stream_fut => {
574 if let Err(_send_err) = result {
575 return Err(self.handle_failed_task().await);
577 }
578 }
579
580 result = &mut self.future => {
581 match result {
585 Ok(_) => vortex_bail!("Internal error: writer future completed early"),
586 Err(e) => return Err(e),
587 }
588 }
589 }
590
591 Ok(())
592 }
593
594 pub fn bytes_written(&self) -> u64 {
596 self.bytes_written.load(Ordering::Relaxed)
597 }
598
599 pub fn buffered_bytes(&self) -> u64 {
601 self.buffered_bytes.buffered_bytes()
602 }
603
604 pub async fn finish(mut self) -> VortexResult<WriteSummary> {
607 drop(self.arrays.take());
609
610 self.future.await
612 }
613
614 async fn handle_failed_task(&mut self) -> VortexError {
616 match (&mut self.future).await {
617 Ok(_) => vortex_err!(
618 "Internal error: writer task completed successfully but write future finished early"
619 ),
620 Err(e) => e,
621 }
622 }
623}
624
625pub struct BlockingWrite<'rt, B: BlockingRuntime> {
627 options: VortexWriteOptions,
628 runtime: &'rt B,
629}
630
631impl<'rt, B: BlockingRuntime> BlockingWrite<'rt, B> {
632 pub fn write<W: Write + Unpin + Send>(
637 self,
638 write: W,
639 iter: impl ArrayIterator + Send + 'static,
640 ) -> VortexResult<WriteSummary> {
641 self.runtime.block_on(async move {
642 self.options
643 .write(BlockingWriteAdapter(write), iter.into_array_stream())
644 .await
645 })
646 }
647
648 pub fn writer<'w, W: Write + Unpin + Send + 'w>(
650 self,
651 write: W,
652 dtype: DType,
653 ) -> BlockingWriter<'rt, 'w, B> {
654 BlockingWriter {
655 writer: self.options.writer(BlockingWriteAdapter(write), dtype),
656 runtime: self.runtime,
657 }
658 }
659}
660
661pub struct BlockingWriter<'rt, 'w, B: BlockingRuntime> {
663 runtime: &'rt B,
664 writer: Writer<'w>,
665}
666
667impl<B: BlockingRuntime> BlockingWriter<'_, '_, B> {
668 pub fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
670 self.runtime.block_on(self.writer.push(chunk))
671 }
672
673 pub fn bytes_written(&self) -> u64 {
675 self.writer.bytes_written()
676 }
677
678 pub fn buffered_bytes(&self) -> u64 {
680 self.writer.buffered_bytes()
681 }
682
683 pub fn finish(self) -> VortexResult<WriteSummary> {
685 self.runtime.block_on(self.writer.finish())
686 }
687}
688
689struct BlockingWriteAdapter<W>(W);
691
692impl<W: Write + Unpin + Send> VortexWrite for BlockingWriteAdapter<W> {
693 async fn write_all<B: IoBuf>(&mut self, buffer: B) -> io::Result<B> {
694 self.0.write_all(buffer.as_slice())?;
695 Ok(buffer)
696 }
697
698 fn flush(&mut self) -> impl Future<Output = io::Result<()>> + Send {
699 ready(self.0.flush())
700 }
701
702 fn shutdown(&mut self) -> impl Future<Output = io::Result<()>> + Send {
703 ready(Ok(()))
704 }
705}
706
707pub struct WriteSummary {
709 footer: Footer,
710 size: u64,
711 }
713
714impl WriteSummary {
715 pub fn footer(&self) -> &Footer {
717 &self.footer
718 }
719
720 pub fn size(&self) -> u64 {
722 self.size
723 }
724
725 pub fn row_count(&self) -> u64 {
727 self.footer.row_count()
728 }
729
730 pub fn compressed_column_sizes(&self) -> VortexResult<Vec<u64>> {
740 let sizes = self.footer.compressed_field_sizes()?;
741 let Some(fields) = self.footer.dtype().as_struct_fields_opt() else {
742 return Ok(vec![sizes.total()]);
743 };
744 Ok(fields
745 .names()
746 .iter()
747 .map(|name| {
748 sizes
749 .get(&FieldPath::from_name(name.clone()))
750 .unwrap_or_default()
751 })
752 .collect())
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use rstest::rstest;
759 use vortex_array::VTable;
760 use vortex_array::array_session;
761 use vortex_array::arrays::Bool;
762 use vortex_array::arrays::Primitive;
763 use vortex_buffer::ByteBuffer;
764 use vortex_edition::ComponentKind;
765 use vortex_edition::Edition;
766 use vortex_edition::EditionDeclaration;
767 use vortex_edition::EditionId;
768 use vortex_edition::EditionInclusion;
769 use vortex_edition::EditionMember;
770 use vortex_edition::EditionSession;
771 use vortex_edition::EditionSessionExt;
772
773 use super::*;
774
775 #[test]
776 fn array_context_only_permits_enabled_encodings() -> VortexResult<()> {
777 const EDITION: EditionId = EditionId::new("test", 2026, 7, 0);
778 static DECLARATION: EditionDeclaration = EditionDeclaration {
779 edition: Edition {
780 id: EDITION,
781 min_library_version: None,
782 },
783 added: &[EditionMember::array(&"vortex.primitive")],
784 };
785
786 let session = array_session().with::<EditionSession>();
787 session.register_edition(&DECLARATION)?;
788 session.enable_edition(EDITION)?;
789
790 let (ctx, allowed_array_encodings) = new_array_context(&session, true);
791 assert_eq!(ctx.to_ids(), [Primitive.id()]);
792 assert!(ctx.intern(&Bool.id()).is_none());
793 assert_eq!(allowed_array_encodings, HashSet::from([Primitive.id()]));
794 Ok(())
795 }
796
797 #[test]
798 fn disabling_editions_allows_all_registered_array_ids() {
799 let session = array_session();
800 let (registered_ids, registered_encodings) = session.arrays().registry().read(|registry| {
801 (
802 registry.keys().copied().sorted().collect::<Vec<_>>(),
803 registry
804 .values()
805 .map(|plugin| plugin.id())
806 .collect::<HashSet<_>>(),
807 )
808 });
809
810 let (ctx, allowed_array_encodings) = new_array_context(&session, false);
811 assert_eq!(ctx.to_ids(), registered_ids);
812 assert_eq!(allowed_array_encodings, registered_encodings);
813 assert!(ctx.intern(&Bool.id()).is_some());
814 }
815
816 #[test]
818 fn kind_filters_are_active_when_empty() -> VortexResult<()> {
819 const EDITION: EditionId = EditionId::new("test", 2026, 8, 0);
820 static ARRAYS_ONLY: EditionDeclaration = EditionDeclaration {
821 edition: Edition {
822 id: EDITION,
823 min_library_version: None,
824 },
825 added: &[EditionMember::array(&"vortex.primitive")],
826 };
827
828 let session = array_session().with::<EditionSession>();
829 session.register_edition(&ARRAYS_ONLY)?;
830 session.enable_edition(EDITION)?;
831 assert!(edition_filter(&session, ComponentKind::Layout).is_empty());
832 assert!(edition_filter(&session, ComponentKind::DType).is_empty());
833 assert!(edition_filter(&session, ComponentKind::Aggregate).is_empty());
834 assert!(
835 new_layout_context(&session, true)
836 .intern(&"vortex.flat".into())
837 .is_none()
838 );
839 assert!(
840 new_layout_context(&session, false)
841 .intern(&"vortex.flat".into())
842 .is_some()
843 );
844
845 session.editions().declare_inclusion(EditionInclusion::new(
846 ComponentKind::Aggregate,
847 "vortex.min",
848 EDITION,
849 ))?;
850 let allowed = edition_filter(&session, ComponentKind::Aggregate);
851 assert_eq!(allowed.len(), 1);
852 assert!(allowed.contains(&Id::from("vortex.min")));
853 Ok(())
854 }
855
856 #[test]
857 fn dtype_filter_checks_nested_extension_dtypes() -> VortexResult<()> {
858 use vortex_array::dtype::Nullability;
859 use vortex_array::extension::datetime::Date;
860 use vortex_array::extension::datetime::Time;
861 use vortex_array::extension::datetime::TimeUnit;
862
863 const EDITION: EditionId = EditionId::new("test", 2026, 8, 0);
864 static DECLARATION: EditionDeclaration = EditionDeclaration {
865 edition: Edition {
866 id: EDITION,
867 min_library_version: None,
868 },
869 added: &[EditionMember::dtype(&"vortex.date")],
870 };
871
872 let session = array_session().with::<EditionSession>();
873 session.register_edition(&DECLARATION)?;
874 session.enable_edition(EDITION)?;
875
876 let date = DType::Extension(Date::new(TimeUnit::Days, Nullability::NonNullable).erased());
877 let nested = DType::struct_([("date", date)], Nullability::NonNullable);
878 validate_dtype_editions(&session, &nested)?;
879
880 let time =
881 DType::Extension(Time::new(TimeUnit::Seconds, Nullability::NonNullable).erased());
882 let error = validate_dtype_editions(&session, &time)
883 .expect_err("vortex.time is not in the enabled edition");
884 assert!(error.to_string().contains("vortex.time"));
885 Ok(())
886 }
887
888 fn write_options_with_keys(keys: &[String]) -> VortexWriteOptions {
889 array_session().write_options().with_metadata_segments(
890 keys.iter()
891 .map(|key| (key.clone(), ByteBuffer::copy_from(b"value"))),
892 )
893 }
894
895 #[rstest]
896 #[case::empty_key(vec![String::new()], "non-empty")]
897 #[case::oversized_key(vec!["k".repeat(MAX_METADATA_KEY_BYTES + 1)], "keys must be at most")]
898 #[case::oversized_multibyte_key(
900 vec!["é".repeat(MAX_METADATA_KEY_BYTES / "é".len() + 1)],
901 "keys must be at most"
902 )]
903 #[case::too_many_segments(
904 (0..=MAX_METADATA_SEGMENTS).map(|idx| format!("key-{idx}")).collect(),
905 "at most 16 metadata segments"
906 )]
907 fn validate_metadata_rejects(#[case] keys: Vec<String>, #[case] expected: &str) {
908 let Err(error) = write_options_with_keys(&keys).validate_metadata() else {
909 panic!("metadata must be rejected for {keys:?}");
910 };
911 assert!(
912 error.to_string().contains(expected),
913 "error should mention {expected:?}, got: {error}"
914 );
915 }
916
917 #[test]
918 fn validate_metadata_accepts_the_limits() -> VortexResult<()> {
919 let keys = (0..MAX_METADATA_SEGMENTS)
921 .map(|idx| format!("{idx:0>width$}", width = MAX_METADATA_KEY_BYTES))
922 .collect::<Vec<_>>();
923 write_options_with_keys(&keys).validate_metadata()
924 }
925
926 #[test]
927 fn validate_metadata_accepts_no_metadata() -> VortexResult<()> {
928 write_options_with_keys(&[]).validate_metadata()
929 }
930}