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 = new_array_context(&self.session, enforce_editions);
243 let ctx = LayoutWriterContext::new(array_ctx)
244 .with_buffered_bytes_tracker(self.buffered_bytes.clone());
245 let ctx = if enforce_editions {
246 ctx.with_allowed_aggregates(edition_filter(&self.session, ComponentKind::Aggregate))
247 } else {
248 ctx
249 };
250 let strategy = match self.strategy {
251 Some(strategy) => strategy,
252 None => {
253 let mut compressor = BtrBlocksCompressorBuilder::from_session(&self.session);
254 if !enforce_editions {
255 compressor = compressor.disable_editions();
256 }
257 WriteStrategyBuilder::from_session(&self.session)
258 .with_btrblocks_builder(compressor)
259 .build()
260 }
261 };
262 let dtype = stream.dtype().clone();
263 if enforce_editions {
264 validate_dtype_editions(&self.session, &dtype)?;
265 }
266
267 let (mut ptr, eof) = SequenceId::root().split();
268
269 let stream = SequentialStreamAdapter::new(
270 dtype.clone(),
271 stream
272 .try_filter(|chunk| ready(!chunk.is_empty()))
273 .map(move |result| result.map(|chunk| (ptr.advance(), chunk))),
274 )
275 .sendable();
276 let (file_stats, stream) = accumulate_stats(
277 stream,
278 self.file_statistics.clone().into(),
279 self.max_variable_length_statistics_size,
280 &self.session,
281 );
282
283 write.write_all(ByteBuffer::copy_from(MAGIC_BYTES)).await?;
285 let mut position = MAGIC_BYTES.len() as u64;
286
287 let (send, recv) = kanal::bounded_async(1);
289
290 let segments = Arc::new(BufferedSegmentSink::new(send, position));
291
292 let ctx2 = ctx.clone();
295 let session = self.session.clone();
296 let layout_fut = self.session.handle().spawn_nested(move |_| async move {
297 let layout = strategy
298 .write_stream(
299 ctx2,
300 Arc::<BufferedSegmentSink>::clone(&segments),
301 stream,
302 eof,
303 &session,
304 )
305 .await?;
306 Ok::<_, VortexError>((layout, segments.segment_specs()))
307 });
308
309 let recv_stream = recv.into_stream();
311 pin_mut!(recv_stream);
312 while let Some(buffer) = recv_stream.next().await {
313 if buffer.is_empty() {
314 continue;
315 }
316 position += buffer.len() as u64;
317 write.write_all(buffer).await?;
318 }
319
320 let (layout, segment_specs) = layout_fut.await?;
321
322 let statistics = if self.file_statistics.is_empty() {
324 None
325 } else {
326 Some(FileStatistics::new_with_dtype(
327 file_stats.stats_sets().into(),
328 &dtype,
329 ))
330 };
331 let mut footer = Footer::new(
332 Arc::clone(&layout),
333 segment_specs,
334 statistics,
335 ReadContext::new(ctx.array_ctx().to_ids()),
336 );
337
338 let (footer_buffers, metadata, approx_byte_size) = footer
340 .clone()
341 .into_serializer()
342 .with_layout_context(new_layout_context(&self.session, enforce_editions))
343 .with_metadata_segments(self.metadata)
344 .with_offset(position)
345 .with_exclude_dtype(self.exclude_dtype)
346 .serialize_with_metadata()?;
347 footer = footer
348 .with_metadata_segments(metadata)
349 .with_approx_byte_size(approx_byte_size);
350
351 for buffer in footer_buffers {
352 position += buffer.len() as u64;
353 write.write_all(buffer).await?;
354 }
355
356 write.flush().await?;
357
358 Ok(WriteSummary {
359 footer,
360 size: position,
361 })
362 }
363
364 pub fn writer<'w, W: VortexWrite + Unpin + 'w>(self, write: W, dtype: DType) -> Writer<'w> {
369 let (arrays_send, arrays_recv) = kanal::bounded_async(1);
371
372 let arrays =
373 ArrayStreamExt::boxed(ArrayStreamAdapter::new(dtype, arrays_recv.into_stream()));
374
375 let write = CountingVortexWrite::new(write);
376 let bytes_written = write.counter();
377 let buffered_bytes = self.buffered_bytes.clone();
378 let future = self.write(write, arrays).boxed_local().fuse();
379
380 Writer {
381 arrays: Some(arrays_send),
382 future,
383 bytes_written,
384 buffered_bytes,
385 }
386 }
387}
388
389fn new_array_context(session: &VortexSession, enforce_editions: bool) -> ArrayContext {
390 let registered: HashSet<ArrayId> = session
401 .arrays()
402 .registry()
403 .read(|registry| registry.keys().copied().collect());
404 let serialized_ids: Vec<ArrayId> = if enforce_editions {
405 session
408 .enabled_component_ids(ComponentKind::Array)
409 .into_iter()
410 .filter(|id| registered.contains(id))
411 .collect()
412 } else {
413 registered.into_iter().collect()
414 };
415 let array_ctx = ArrayContext::new(serialized_ids.iter().copied().sorted().collect());
416 if enforce_editions {
417 array_ctx.with_allowed_ids(serialized_ids.into_iter().collect())
419 } else {
420 array_ctx
421 }
422}
423
424fn edition_filter(session: &VortexSession, kind: ComponentKind) -> HashSet<Id> {
426 session.enabled_component_ids(kind).into_iter().collect()
427}
428
429fn validate_dtype_editions(session: &VortexSession, dtype: &DType) -> VortexResult<()> {
431 let allowed = edition_filter(session, ComponentKind::DType);
432
433 fn validate(dtype: &DType, allowed: &HashSet<Id>) -> VortexResult<()> {
434 match dtype {
435 DType::List(element, _) | DType::FixedSizeList(element, ..) => {
436 validate(element, allowed)
437 }
438 DType::Map(map, _) => {
439 validate(&map.key_dtype(), allowed)?;
440 validate(&map.value_dtype(), allowed)
441 }
442 DType::Struct(fields, _) => {
443 for field in fields.fields() {
444 validate(&field, allowed)?;
445 }
446 Ok(())
447 }
448 DType::Union(variants, _) => {
449 for variant in variants.variants() {
450 validate(&variant, allowed)?;
451 }
452 Ok(())
453 }
454 DType::Extension(extension) => {
455 if !allowed.contains(&extension.id()) {
456 vortex_bail!(
457 "Extension DType {} not permitted by enabled editions",
458 extension.id()
459 );
460 }
461 validate(extension.storage_dtype(), allowed)
462 }
463 DType::Null
464 | DType::Bool(_)
465 | DType::Primitive(..)
466 | DType::Decimal(..)
467 | DType::Utf8(_)
468 | DType::Binary(_)
469 | DType::Variant(_) => Ok(()),
470 }
471 }
472
473 validate(dtype, &allowed)
474}
475
476fn new_layout_context(session: &VortexSession, enforce_editions: bool) -> LayoutContext {
478 let context = LayoutContext::default();
479 if enforce_editions {
480 context.with_allowed_ids(edition_filter(session, ComponentKind::Layout))
481 } else {
482 context
483 }
484}
485
486fn validate_metadata_segments(metadata: &HashMap<String, ByteBuffer>) -> VortexResult<()> {
487 if metadata.len() > MAX_METADATA_SEGMENTS {
488 vortex_bail!(
489 "Vortex files may contain at most {} metadata segments; got {} metadata segments. Metadata keys must be non-empty and at most {} bytes",
490 MAX_METADATA_SEGMENTS,
491 metadata.len(),
492 MAX_METADATA_KEY_BYTES
493 );
494 }
495
496 for key in metadata.keys() {
497 if key.is_empty() {
498 vortex_bail!(
499 "Vortex metadata keys must be non-empty and at most {} bytes; files may contain at most {} metadata segments",
500 MAX_METADATA_KEY_BYTES,
501 MAX_METADATA_SEGMENTS
502 );
503 }
504
505 let key_bytes = key.len();
506 if key_bytes > MAX_METADATA_KEY_BYTES {
507 vortex_bail!(
508 "Vortex metadata key {key:?} is {key_bytes} bytes, but keys must be at most {} bytes; files may contain at most {} metadata segments",
509 MAX_METADATA_KEY_BYTES,
510 MAX_METADATA_SEGMENTS
511 );
512 }
513 }
514
515 Ok(())
516}
517
518pub struct Writer<'w> {
520 arrays: Option<kanal::AsyncSender<VortexResult<ArrayRef>>>,
522 future: Fuse<LocalBoxFuture<'w, VortexResult<WriteSummary>>>,
524 bytes_written: Arc<AtomicU64>,
526 buffered_bytes: BufferedBytesTracker,
528}
529
530impl Writer<'_> {
531 pub async fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
533 let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
534 let send_fut = async move { arrays.send(Ok(chunk)).await }.fuse();
535 pin_mut!(send_fut);
536
537 select! {
540 result = send_fut => {
541 if result.is_err() {
543 return Err(self.handle_failed_task().await);
544 }
545 },
546 result = &mut self.future => {
547 match result {
551 Ok(_) => vortex_bail!("Internal error: writer future completed early"),
552 Err(e) => return Err(e),
553 }
554 }
555 }
556
557 Ok(())
558 }
559
560 pub async fn push_stream(&mut self, mut stream: SendableArrayStream) -> VortexResult<()> {
565 let arrays = self.arrays.clone().vortex_expect("missing arrays sender");
566 let stream_fut = async move {
567 while let Some(chunk) = stream.next().await {
568 arrays.send(chunk).await?;
569 }
570 Ok::<_, kanal::SendError>(())
571 }
572 .fuse();
573 pin_mut!(stream_fut);
574
575 select! {
578 result = stream_fut => {
579 if let Err(_send_err) = result {
580 return Err(self.handle_failed_task().await);
582 }
583 }
584
585 result = &mut self.future => {
586 match result {
590 Ok(_) => vortex_bail!("Internal error: writer future completed early"),
591 Err(e) => return Err(e),
592 }
593 }
594 }
595
596 Ok(())
597 }
598
599 pub fn bytes_written(&self) -> u64 {
601 self.bytes_written.load(Ordering::Relaxed)
602 }
603
604 pub fn buffered_bytes(&self) -> u64 {
606 self.buffered_bytes.buffered_bytes()
607 }
608
609 pub async fn finish(mut self) -> VortexResult<WriteSummary> {
612 drop(self.arrays.take());
614
615 self.future.await
617 }
618
619 async fn handle_failed_task(&mut self) -> VortexError {
621 match (&mut self.future).await {
622 Ok(_) => vortex_err!(
623 "Internal error: writer task completed successfully but write future finished early"
624 ),
625 Err(e) => e,
626 }
627 }
628}
629
630pub struct BlockingWrite<'rt, B: BlockingRuntime> {
632 options: VortexWriteOptions,
633 runtime: &'rt B,
634}
635
636impl<'rt, B: BlockingRuntime> BlockingWrite<'rt, B> {
637 pub fn write<W: Write + Unpin + Send>(
642 self,
643 write: W,
644 iter: impl ArrayIterator + Send + 'static,
645 ) -> VortexResult<WriteSummary> {
646 self.runtime.block_on(async move {
647 self.options
648 .write(BlockingWriteAdapter(write), iter.into_array_stream())
649 .await
650 })
651 }
652
653 pub fn writer<'w, W: Write + Unpin + Send + 'w>(
655 self,
656 write: W,
657 dtype: DType,
658 ) -> BlockingWriter<'rt, 'w, B> {
659 BlockingWriter {
660 writer: self.options.writer(BlockingWriteAdapter(write), dtype),
661 runtime: self.runtime,
662 }
663 }
664}
665
666pub struct BlockingWriter<'rt, 'w, B: BlockingRuntime> {
668 runtime: &'rt B,
669 writer: Writer<'w>,
670}
671
672impl<B: BlockingRuntime> BlockingWriter<'_, '_, B> {
673 pub fn push(&mut self, chunk: ArrayRef) -> VortexResult<()> {
675 self.runtime.block_on(self.writer.push(chunk))
676 }
677
678 pub fn bytes_written(&self) -> u64 {
680 self.writer.bytes_written()
681 }
682
683 pub fn buffered_bytes(&self) -> u64 {
685 self.writer.buffered_bytes()
686 }
687
688 pub fn finish(self) -> VortexResult<WriteSummary> {
690 self.runtime.block_on(self.writer.finish())
691 }
692}
693
694struct BlockingWriteAdapter<W>(W);
696
697impl<W: Write + Unpin + Send> VortexWrite for BlockingWriteAdapter<W> {
698 async fn write_all<B: IoBuf>(&mut self, buffer: B) -> io::Result<B> {
699 self.0.write_all(buffer.as_slice())?;
700 Ok(buffer)
701 }
702
703 fn flush(&mut self) -> impl Future<Output = io::Result<()>> + Send {
704 ready(self.0.flush())
705 }
706
707 fn shutdown(&mut self) -> impl Future<Output = io::Result<()>> + Send {
708 ready(Ok(()))
709 }
710}
711
712pub struct WriteSummary {
714 footer: Footer,
715 size: u64,
716 }
718
719impl WriteSummary {
720 pub fn footer(&self) -> &Footer {
722 &self.footer
723 }
724
725 pub fn size(&self) -> u64 {
727 self.size
728 }
729
730 pub fn row_count(&self) -> u64 {
732 self.footer.row_count()
733 }
734
735 pub fn compressed_column_sizes(&self) -> VortexResult<Vec<u64>> {
745 let sizes = self.footer.compressed_field_sizes()?;
746 let Some(fields) = self.footer.dtype().as_struct_fields_opt() else {
747 return Ok(vec![sizes.total()]);
748 };
749 Ok(fields
750 .names()
751 .iter()
752 .map(|name| {
753 sizes
754 .get(&FieldPath::from_name(name.clone()))
755 .unwrap_or_default()
756 })
757 .collect())
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use rstest::rstest;
764 use vortex_array::VTable;
765 use vortex_array::array_session;
766 use vortex_array::arrays::Bool;
767 use vortex_array::arrays::Primitive;
768 use vortex_buffer::ByteBuffer;
769 use vortex_edition::ComponentKind;
770 use vortex_edition::Edition;
771 use vortex_edition::EditionDeclaration;
772 use vortex_edition::EditionId;
773 use vortex_edition::EditionInclusion;
774 use vortex_edition::EditionMember;
775 use vortex_edition::EditionSession;
776 use vortex_edition::EditionSessionExt;
777 use vortex_session::registry::CachedId;
778
779 use super::*;
780
781 #[test]
782 fn array_context_only_permits_enabled_encodings() -> VortexResult<()> {
783 const EDITION: EditionId = EditionId::new("test", 2026, 7, 0);
784 static DECLARATION: EditionDeclaration = EditionDeclaration {
785 edition: Edition {
786 id: EDITION,
787 min_library_version: None,
788 },
789 added: &[EditionMember::array(&"vortex.primitive")],
790 };
791
792 let session = array_session().with::<EditionSession>();
793 session.register_edition(&DECLARATION)?;
794 session.enable_edition(EDITION)?;
795
796 let ctx = new_array_context(&session, true);
797 assert_eq!(ctx.to_ids(), [Primitive.id()]);
798 assert!(ctx.intern(&Bool.id()).is_none());
799 Ok(())
800 }
801
802 #[test]
805 fn array_context_skips_enabled_but_unregistered_encodings() -> VortexResult<()> {
806 const EDITION: EditionId = EditionId::new("test", 2026, 9, 0);
807 static DECLARATION: EditionDeclaration = EditionDeclaration {
808 edition: Edition {
809 id: EDITION,
810 min_library_version: None,
811 },
812 added: &[
813 EditionMember::array(&"vortex.primitive"),
814 EditionMember::array(&"vortex.alp"),
815 ],
816 };
817
818 let session = array_session().with::<EditionSession>();
819 session.register_edition(&DECLARATION)?;
820 session.enable_edition(EDITION)?;
821
822 let ctx = new_array_context(&session, true);
823 assert_eq!(ctx.to_ids(), [Primitive.id()]);
824 static ALP: CachedId = CachedId::new("vortex.alp");
825 assert!(ctx.intern(&ALP).is_none());
826 Ok(())
827 }
828
829 #[test]
830 fn disabling_editions_allows_all_registered_array_ids() {
831 let session = array_session();
832 let registered_ids = session
833 .arrays()
834 .registry()
835 .read(|registry| registry.keys().copied().sorted().collect::<Vec<_>>());
836
837 let ctx = new_array_context(&session, false);
838 assert_eq!(ctx.to_ids(), registered_ids);
839 assert!(ctx.intern(&Bool.id()).is_some());
840 }
841
842 #[test]
844 fn kind_filters_are_active_when_empty() -> VortexResult<()> {
845 const EDITION: EditionId = EditionId::new("test", 2026, 8, 0);
846 static ARRAYS_ONLY: EditionDeclaration = EditionDeclaration {
847 edition: Edition {
848 id: EDITION,
849 min_library_version: None,
850 },
851 added: &[EditionMember::array(&"vortex.primitive")],
852 };
853
854 let session = array_session().with::<EditionSession>();
855 session.register_edition(&ARRAYS_ONLY)?;
856 session.enable_edition(EDITION)?;
857 assert!(edition_filter(&session, ComponentKind::Layout).is_empty());
858 assert!(edition_filter(&session, ComponentKind::DType).is_empty());
859 assert!(edition_filter(&session, ComponentKind::Aggregate).is_empty());
860 assert!(
861 new_layout_context(&session, true)
862 .intern(&"vortex.flat".into())
863 .is_none()
864 );
865 assert!(
866 new_layout_context(&session, false)
867 .intern(&"vortex.flat".into())
868 .is_some()
869 );
870
871 session.editions().declare_inclusion(EditionInclusion::new(
872 ComponentKind::Aggregate,
873 "vortex.min",
874 EDITION,
875 ))?;
876 let allowed = edition_filter(&session, ComponentKind::Aggregate);
877 assert_eq!(allowed.len(), 1);
878 assert!(allowed.contains(&Id::from("vortex.min")));
879 Ok(())
880 }
881
882 #[test]
883 fn dtype_filter_checks_nested_extension_dtypes() -> VortexResult<()> {
884 use vortex_array::dtype::Nullability;
885 use vortex_array::extension::datetime::Date;
886 use vortex_array::extension::datetime::Time;
887 use vortex_array::extension::datetime::TimeUnit;
888
889 const EDITION: EditionId = EditionId::new("test", 2026, 8, 0);
890 static DECLARATION: EditionDeclaration = EditionDeclaration {
891 edition: Edition {
892 id: EDITION,
893 min_library_version: None,
894 },
895 added: &[EditionMember::dtype(&"vortex.date")],
896 };
897
898 let session = array_session().with::<EditionSession>();
899 session.register_edition(&DECLARATION)?;
900 session.enable_edition(EDITION)?;
901
902 let date = DType::Extension(Date::new(TimeUnit::Days, Nullability::NonNullable).erased());
903 let nested = DType::struct_([("date", date)], Nullability::NonNullable);
904 validate_dtype_editions(&session, &nested)?;
905
906 let time =
907 DType::Extension(Time::new(TimeUnit::Seconds, Nullability::NonNullable).erased());
908 let error = validate_dtype_editions(&session, &time)
909 .expect_err("vortex.time is not in the enabled edition");
910 assert!(error.to_string().contains("vortex.time"));
911 let nested_time = DType::struct_([("time", time)], Nullability::NonNullable);
912 let error = validate_dtype_editions(&session, &nested_time)
913 .expect_err("nested vortex.time is not in the enabled edition");
914 assert!(error.to_string().contains("vortex.time"));
915 Ok(())
916 }
917
918 fn write_options_with_keys(keys: &[String]) -> VortexWriteOptions {
919 array_session().write_options().with_metadata_segments(
920 keys.iter()
921 .map(|key| (key.clone(), ByteBuffer::copy_from(b"value"))),
922 )
923 }
924
925 #[rstest]
926 #[case::empty_key(vec![String::new()], "non-empty")]
927 #[case::oversized_key(vec!["k".repeat(MAX_METADATA_KEY_BYTES + 1)], "keys must be at most")]
928 #[case::oversized_multibyte_key(
930 vec!["é".repeat(MAX_METADATA_KEY_BYTES / "é".len() + 1)],
931 "keys must be at most"
932 )]
933 #[case::too_many_segments(
934 (0..=MAX_METADATA_SEGMENTS).map(|idx| format!("key-{idx}")).collect(),
935 "at most 16 metadata segments"
936 )]
937 fn validate_metadata_rejects(#[case] keys: Vec<String>, #[case] expected: &str) {
938 let Err(error) = write_options_with_keys(&keys).validate_metadata() else {
939 panic!("metadata must be rejected for {keys:?}");
940 };
941 assert!(
942 error.to_string().contains(expected),
943 "error should mention {expected:?}, got: {error}"
944 );
945 }
946
947 #[test]
948 fn validate_metadata_accepts_the_limits() -> VortexResult<()> {
949 let keys = (0..MAX_METADATA_SEGMENTS)
951 .map(|idx| format!("{idx:0>width$}", width = MAX_METADATA_KEY_BYTES))
952 .collect::<Vec<_>>();
953 write_options_with_keys(&keys).validate_metadata()
954 }
955
956 #[test]
957 fn validate_metadata_accepts_no_metadata() -> VortexResult<()> {
958 write_options_with_keys(&[]).validate_metadata()
959 }
960}